mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 00:31:51 +01:00
Make a protobuf based backup system (#3936)
* Make a protobuf based backup system * Cleanup * More cleanup * Fix restores always loading the full backup restore, even when legacy restore was used * Make offline the default (cherry picked from commit f6fd8a8ddb90869f3e28fd8fcd81a2125f8e0527) * Find chapter based on the url (cherry picked from commit 326dc2700944a60da381d82cd9782c5f0d335902) * Dont break after finding one chapter (cherry picked from commit f91d1af37398619cf371e4920b60f6d309799c74) * Also apply changes to online restore (cherry picked from commit e7c16cd0d14ea5d50ce4a9a3dfa8ca768be702f2) * Rewrite backup categories (cherry picked from commit f4200e2146a9c540675767206ed4664894aa1216) * Dedupe some code, move over read and bookmarks properly (cherry picked from commit d9ce86aca66945c831670a1523d8bc69966312df) * Move some functions to the abstract backup manager (cherry picked from commit b0c658741a2f506bc31823f1f0347772bc119d2e) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt * Fix some backup duplication issues (cherry picked from commit a4a1c2827c4537d2d07a0cb589dc1c3be1d65185) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt * Fix a missed bundleOf * So glad this wasnt merged before now, everything should be working with this commit
This commit is contained in:
parent
a150762c63
commit
682fae12b6
@ -185,7 +185,9 @@ dependencies {
|
|||||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
final kotlin_serialization_version = '1.0.1'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialization_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlin_serialization_version"
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
|
@ -7,4 +7,9 @@ object BackupConst {
|
|||||||
private const val NAME = "BackupRestoreServices"
|
private const val NAME = "BackupRestoreServices"
|
||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||||
|
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
||||||
|
|
||||||
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
|
const val BACKUP_TYPE_FULL = 1
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@ import android.os.PowerManager
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@ -46,11 +49,12 @@ class BackupCreateService : Service() {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
|
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@ -62,7 +66,7 @@ class BackupCreateService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private lateinit var backupManager: BackupManager
|
private lateinit var backupManager: AbstractBackupManager
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -101,7 +105,8 @@ class BackupCreateService : Service() {
|
|||||||
try {
|
try {
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
backupManager = BackupManager(this)
|
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
|
backupManager = if (backupType == BackupConst.BACKUP_TYPE_FULL) FullBackupManager(this) else LegacyBackupManager(this)
|
||||||
|
|
||||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
|
@ -7,6 +7,8 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -17,11 +19,13 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = FullBackupManager(context)
|
||||||
|
val legacyBackupManager = if (preferences.createLegacyBackup().get()) LegacyBackupManager(context) else null
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val uri = preferences.backupsDirectory().get().toUri()
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
backupManager.createBackup(uri, flags, true)
|
backupManager.createBackup(uri, flags, true)
|
||||||
|
legacyBackupManager?.createBackup(uri, flags, true)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
@ -15,7 +15,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
internal class BackupNotifier(private val context: Context) {
|
class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
@ -7,45 +7,17 @@ import android.net.Uri
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from a JSON file.
|
* Restores backup from a JSON file.
|
||||||
@ -69,10 +41,12 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri) {
|
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
|
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@ -95,35 +69,9 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private var job: Job? = null
|
private var backupRestore: AbstractBackupRestore? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* The progress of a backup restore
|
|
||||||
*/
|
|
||||||
private var restoreProgress = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount of manga in Json file (needed for restore)
|
|
||||||
*/
|
|
||||||
private var restoreAmount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of source ID to source name from backup data
|
|
||||||
*/
|
|
||||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List containing errors
|
|
||||||
*/
|
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
|
||||||
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
@ -144,7 +92,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -165,304 +113,30 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
|
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||||
|
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
||||||
|
|
||||||
// Cancel any previous job if needed.
|
// Cancel any previous job if needed.
|
||||||
job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
|
backupRestore = if (mode == BackupConst.BACKUP_TYPE_FULL) FullBackupRestore(this, notifier, online) else LegacyBackupRestore(this, notifier)
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
job = GlobalScope.launch(handler) {
|
backupRestore?.job = GlobalScope.launch(handler) {
|
||||||
if (!restoreBackup(uri)) {
|
if (backupRestore?.restoreBackup(uri) == false) {
|
||||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
job?.invokeOnCompletion {
|
backupRestore?.job?.invokeOnCompletion {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores data from backup file.
|
|
||||||
*
|
|
||||||
* @param uri backup file to restore
|
|
||||||
*/
|
|
||||||
private fun restoreBackup(uri: Uri): Boolean {
|
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
// Get parser version
|
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
|
||||||
|
|
||||||
// Initialize manager
|
|
||||||
backupManager = BackupManager(this, version)
|
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
|
||||||
restoreProgress = 0
|
|
||||||
errors.clear()
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
json.get(CATEGORIES)?.let { restoreCategories(it) }
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
|
||||||
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
mangasJson.forEach {
|
|
||||||
if (job?.isActive != true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
|
|
||||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
|
||||||
db.inTransaction {
|
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
|
||||||
if (source != null) {
|
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
|
||||||
} else {
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a manga restore observable
|
|
||||||
*
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param source source to get manga data from
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
*/
|
|
||||||
private fun restoreMangaData(
|
|
||||||
manga: Manga,
|
|
||||||
source: Source,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
db.inTransaction {
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Manga not in database
|
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
|
||||||
} else { // Manga in database
|
|
||||||
// Copy information from manga already in database
|
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
// Fetch rest of manga information
|
|
||||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
|
||||||
private fun restoreMangaFetch(
|
|
||||||
source: Source,
|
|
||||||
manga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
backupManager.restoreMangaFetchObservable(source, manga)
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.filter { it.id != null }
|
|
||||||
.flatMap {
|
|
||||||
chapterFetchObservable(source, it, chapters)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
trackingFetchObservable(it, tracks)
|
|
||||||
}
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
|
||||||
source: Source,
|
|
||||||
backupManga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
Observable.just(backupManga)
|
|
||||||
.flatMap { manga ->
|
|
||||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
|
||||||
chapterFetchObservable(source, manga, chapters)
|
|
||||||
.map { manga }
|
|
||||||
} else {
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap { manga ->
|
|
||||||
trackingFetchObservable(manga, tracks)
|
|
||||||
}
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches chapter information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
|
||||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
|
||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
|
||||||
// If there's any error, return empty update and continue.
|
|
||||||
.onErrorReturn {
|
|
||||||
val errorMessage = if (it is NoChaptersException) {
|
|
||||||
getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
it.message
|
|
||||||
}
|
|
||||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that refreshes tracking information
|
|
||||||
* @param manga manga that needs updating.
|
|
||||||
* @param tracks list containing tracks from restore file.
|
|
||||||
* @return [Observable] that contains updated track item
|
|
||||||
*/
|
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
|
||||||
return Observable.from(tracks)
|
|
||||||
.flatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
service.refresh(track)
|
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
track
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
|
|
||||||
Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update dialog in [BackupConst]
|
|
||||||
*
|
|
||||||
* @param progress restore progress
|
|
||||||
* @param amount total restoreAmount of manga
|
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
|
||||||
private fun showRestoreProgress(
|
|
||||||
progress: Int,
|
|
||||||
amount: Int,
|
|
||||||
title: String
|
|
||||||
) {
|
|
||||||
notifier.showRestoreProgress(title, progress, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write errors to error log
|
|
||||||
*/
|
|
||||||
private fun writeErrorLog(): File {
|
|
||||||
try {
|
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
|
||||||
errors.forEach { (date, message) ->
|
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return destFile
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
return File("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,442 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
|
/**
|
||||||
|
* Parser
|
||||||
|
*/
|
||||||
|
val parser = ProtoBuf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
|
// Create root object
|
||||||
|
var backup: Backup? = null
|
||||||
|
|
||||||
|
databaseHelper.inTransaction {
|
||||||
|
// Get manga from database
|
||||||
|
val databaseManga = getDatabaseManga()
|
||||||
|
|
||||||
|
backup = Backup(
|
||||||
|
backupManga(databaseManga, flags),
|
||||||
|
backupCategories(),
|
||||||
|
backupExtensionInfo(databaseManga)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When BackupCreatorJob
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file and create
|
||||||
|
var dir = UniFile.fromUri(context, uri)
|
||||||
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_full_\d+-\d+-\d+_\d+-\d+.proto.gz""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
val newFile = dir.createFile(BackupFull.getDefaultFilename())
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
newFile.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
|
||||||
|
return newFile.uri.toString()
|
||||||
|
} else {
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
|
||||||
|
return file.uri.toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDatabaseManga() = getFavoriteManga()
|
||||||
|
|
||||||
|
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
|
return mangas.map {
|
||||||
|
backupMangaObject(it, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map { it.source }
|
||||||
|
.distinct()
|
||||||
|
.map { sourceManager.getOrStub(it) }
|
||||||
|
.map { BackupSource.copyFrom(it) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @return list of [BackupCategory] to be backed up
|
||||||
|
*/
|
||||||
|
private fun backupCategories(): List<BackupCategory> {
|
||||||
|
return databaseHelper.getCategories()
|
||||||
|
.executeAsBlocking()
|
||||||
|
.map { BackupCategory.copyFrom(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @param options options for the backup
|
||||||
|
* @return [BackupManga] containing manga in a serializable form
|
||||||
|
*/
|
||||||
|
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||||
|
// Entry for this manga
|
||||||
|
val mangaObject = BackupManga.copyFrom(manga)
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
if (historyForManga.isNotEmpty()) {
|
||||||
|
val history = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { BackupHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
mangaObject.history = history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaObject
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
|
manga.id = dbManga.id
|
||||||
|
manga.copyFrom(dbManga)
|
||||||
|
insertManga(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
fun restoreMangaFetchObservable(source: Source?, manga: Manga, online: Boolean): Observable<Manga> {
|
||||||
|
return if (online && source != null) {
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
|
.map { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
manga.favorite = manga.favorite
|
||||||
|
manga.initialized = true
|
||||||
|
manga.id = insertManga(manga)
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
.map {
|
||||||
|
it.initialized = it.description != null
|
||||||
|
it.id = insertManga(it)
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters list of chapters in the backup
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return source.fetchChapterList(manga)
|
||||||
|
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||||
|
.doOnNext { pair ->
|
||||||
|
if (pair.first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
updateChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the categories from Json
|
||||||
|
*
|
||||||
|
* @param backupCategories list containing categories
|
||||||
|
*/
|
||||||
|
internal fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
|
// Get categories from file and from db
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupCategories.map { it.getCategoryImpl() }.forEach { category ->
|
||||||
|
// Used to know if the category is already in the db
|
||||||
|
var found = false
|
||||||
|
for (dbCategory in dbCategories) {
|
||||||
|
// If the category is already in the db, assign the id to the file's category
|
||||||
|
// and do nothing
|
||||||
|
if (category.name == dbCategory.name) {
|
||||||
|
category.id = dbCategory.id
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the category isn't in the db, remove the id and insert a new category
|
||||||
|
// Store the inserted id in the category
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
category.id = null
|
||||||
|
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||||
|
category.id = result.insertedId()?.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the categories a manga is in.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose categories have to be restored.
|
||||||
|
* @param categories the categories to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
backupCategories.firstOrNull {
|
||||||
|
it.order == backupCategoryOrder
|
||||||
|
}?.let { backupCategory ->
|
||||||
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
|
dbCategory.name == backupCategory.name
|
||||||
|
}?.let { dbCategory ->
|
||||||
|
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
|
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||||
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val historyToBeUpdated = mutableListOf<History>()
|
||||||
|
for ((url, lastRead) in history) {
|
||||||
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory.apply {
|
||||||
|
last_read = max(lastRead, dbHistory.last_read)
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(dbHistory)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||||
|
val historyToAdd = History.create(it).apply {
|
||||||
|
last_read = lastRead
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(historyToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the sync of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose sync have to be restored.
|
||||||
|
* @param tracks the track list to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
tracks.map { it.manga_id = manga.id!! }
|
||||||
|
|
||||||
|
// Get tracks from database
|
||||||
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
val trackToUpdate = mutableListOf<Track>()
|
||||||
|
|
||||||
|
tracks.forEach { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
var isInDatabase = false
|
||||||
|
for (dbTrack in dbTracks) {
|
||||||
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
|
// The sync is already in the db, only update its fields
|
||||||
|
if (track.media_id != dbTrack.media_id) {
|
||||||
|
dbTrack.media_id = track.media_id
|
||||||
|
}
|
||||||
|
if (track.library_id != dbTrack.library_id) {
|
||||||
|
dbTrack.library_id = track.library_id
|
||||||
|
}
|
||||||
|
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
track.id = null
|
||||||
|
trackToUpdate.add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update database
|
||||||
|
if (trackToUpdate.isNotEmpty()) {
|
||||||
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the chapters for manga if chapters already in database
|
||||||
|
*
|
||||||
|
* @param manga manga of chapters
|
||||||
|
* @param chapters list containing chapters that get restored
|
||||||
|
* @return boolean answering if chapter fetch is not needed
|
||||||
|
*/
|
||||||
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Return if fetch is needed
|
||||||
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
if (dbChapter.read && !chapter.read) {
|
||||||
|
chapter.read = dbChapter.read
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
}
|
||||||
|
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||||
|
chapter.bookmark = dbChapter.bookmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
chapters.filter { it.id != null }
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
updateChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
if (dbChapter.read && !chapter.read) {
|
||||||
|
chapter.read = dbChapter.read
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
||||||
|
chapter.last_page_read = dbChapter.last_page_read
|
||||||
|
}
|
||||||
|
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||||
|
chapter.bookmark = dbChapter.bookmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
updateChapters(chapters.filter { it.id != null })
|
||||||
|
insertChapters(chapters.filter { it.id == null })
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,283 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore(context, notifier) {
|
||||||
|
private lateinit var fullBackupManager: FullBackupManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores data from backup file.
|
||||||
|
*
|
||||||
|
* @param uri backup file to restore
|
||||||
|
*/
|
||||||
|
override fun restoreBackup(uri: Uri): Boolean {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
fullBackupManager = FullBackupManager(context)
|
||||||
|
|
||||||
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = fullBackupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
if (backup.backupCategories.isNotEmpty()) {
|
||||||
|
restoreCategories(backup.backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
|
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
|
||||||
|
backup.backupManga.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it, backup.backupCategories, online)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
|
||||||
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
|
db.inTransaction {
|
||||||
|
fullBackupManager.restoreCategories(backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||||
|
val manga = backupManga.getMangaImpl()
|
||||||
|
val chapters = backupManga.getChaptersImpl()
|
||||||
|
val categories = backupManga.categories
|
||||||
|
val history = backupManga.history
|
||||||
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val source = fullBackupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null || !online) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
} else {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
*/
|
||||||
|
private fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source?,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
val dbManga = fullBackupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
fullBackupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun restoreMangaFetch(
|
||||||
|
source: Source?,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
fullBackupManager.restoreMangaFetchObservable(source, manga, online)
|
||||||
|
.doOnError {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap {
|
||||||
|
if (online && source != null) {
|
||||||
|
chapterFetchObservable(source, it, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
fullBackupManager.restoreChaptersForMangaOffline(it, chapters)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks, backupCategories)
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
trackingFetchObservable(it, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreMangaNoFetch(
|
||||||
|
source: Source?,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (online && source != null) {
|
||||||
|
if (!fullBackupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fullBackupManager.restoreChaptersForMangaOffline(manga, chapters)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks, backupCategories)
|
||||||
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
|
trackingFetchObservable(manga, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
|
// Restore categories
|
||||||
|
fullBackupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
fullBackupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
fullBackupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return fullBackupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
val errorMessage = if (it is NoChaptersException) {
|
||||||
|
context.getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
it.message
|
||||||
|
}
|
||||||
|
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||||
|
Pair(emptyList(), emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that refreshes tracking information
|
||||||
|
* @param manga manga that needs updating.
|
||||||
|
* @param tracks list containing tracks from restore file.
|
||||||
|
* @return [Observable] that contains updated track item
|
||||||
|
*/
|
||||||
|
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||||
|
return Observable.from(tracks)
|
||||||
|
.flatMap { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
track
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [BackupConst]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(
|
||||||
|
progress: Int,
|
||||||
|
amount: Int,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupRestoreValidator
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
/**
|
||||||
|
* Checks for critical backup file data.
|
||||||
|
*
|
||||||
|
* @throws Exception if manga cannot be found.
|
||||||
|
* @return List of missing sources or missing trackers.
|
||||||
|
*/
|
||||||
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
if (backup.backupManga.isEmpty()) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
val missingSources = sources
|
||||||
|
.filter { sourceManager.get(it.key) == null }
|
||||||
|
.values
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
val trackers = backup.backupManga
|
||||||
|
.flatMap { it.tracking }
|
||||||
|
.map { it.syncId }
|
||||||
|
.distinct()
|
||||||
|
val missingTrackers = trackers
|
||||||
|
.mapNotNull { trackManager.getService(it) }
|
||||||
|
.filter { !it.isLogged }
|
||||||
|
.map { it.name }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return Results(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup json model
|
||||||
|
*/
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class Backup(
|
||||||
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
class BackupCategory(
|
||||||
|
@ProtoNumber(1) var name: String,
|
||||||
|
@ProtoNumber(2) var order: Int = 0,
|
||||||
|
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var flags: Int = 0,
|
||||||
|
) {
|
||||||
|
fun getCategoryImpl(): CategoryImpl {
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
name = this@BackupCategory.name
|
||||||
|
flags = this@BackupCategory.flags
|
||||||
|
order = this@BackupCategory.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(category: Category): BackupCategory {
|
||||||
|
return BackupCategory(
|
||||||
|
name = category.name,
|
||||||
|
order = category.order,
|
||||||
|
flags = category.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class BackupChapter(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var name: String,
|
||||||
|
@ProtoNumber(3) var scanlator: String? = null,
|
||||||
|
@ProtoNumber(4) var read: Boolean = false,
|
||||||
|
@ProtoNumber(5) var bookmark: Boolean = false,
|
||||||
|
// lastPageRead is called progress in 1.x
|
||||||
|
@ProtoNumber(6) var lastPageRead: Int = 0,
|
||||||
|
@ProtoNumber(7) var dateFetch: Long = 0,
|
||||||
|
@ProtoNumber(8) var dateUpload: Long = 0,
|
||||||
|
// chapterNumber is called number is 1.x
|
||||||
|
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||||
|
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||||
|
) {
|
||||||
|
fun toChapterImpl(): ChapterImpl {
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
url = this@BackupChapter.url
|
||||||
|
name = this@BackupChapter.name
|
||||||
|
chapter_number = this@BackupChapter.chapterNumber
|
||||||
|
scanlator = this@BackupChapter.scanlator
|
||||||
|
read = this@BackupChapter.read
|
||||||
|
bookmark = this@BackupChapter.bookmark
|
||||||
|
last_page_read = this@BackupChapter.lastPageRead
|
||||||
|
date_fetch = this@BackupChapter.dateFetch
|
||||||
|
date_upload = this@BackupChapter.dateUpload
|
||||||
|
source_order = this@BackupChapter.sourceOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(chapter: Chapter): BackupChapter {
|
||||||
|
return BackupChapter(
|
||||||
|
url = chapter.url,
|
||||||
|
name = chapter.name,
|
||||||
|
chapterNumber = chapter.chapter_number,
|
||||||
|
scanlator = chapter.scanlator,
|
||||||
|
read = chapter.read,
|
||||||
|
bookmark = chapter.bookmark,
|
||||||
|
lastPageRead = chapter.last_page_read,
|
||||||
|
dateFetch = chapter.date_fetch,
|
||||||
|
dateUpload = chapter.date_upload,
|
||||||
|
sourceOrder = chapter.source_order
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object BackupFull {
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_full_$date.proto.gz"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(0) var url: String,
|
||||||
|
@ProtoNumber(1) var lastRead: Long
|
||||||
|
)
|
@ -0,0 +1,89 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class BackupManga(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
@ProtoNumber(1) var source: Long,
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(2) var url: String,
|
||||||
|
@ProtoNumber(3) var title: String = "",
|
||||||
|
@ProtoNumber(4) var artist: String? = null,
|
||||||
|
@ProtoNumber(5) var author: String? = null,
|
||||||
|
@ProtoNumber(6) var description: String? = null,
|
||||||
|
@ProtoNumber(7) var genre: List<String> = emptyList(),
|
||||||
|
@ProtoNumber(8) var status: Int = 0,
|
||||||
|
// thumbnailUrl is called cover in 1.x
|
||||||
|
@ProtoNumber(9) var thumbnailUrl: String? = null,
|
||||||
|
// @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x
|
||||||
|
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||||
|
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||||
|
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||||
|
@ProtoNumber(14) var viewer: Int = 0,
|
||||||
|
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||||
|
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||||
|
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||||
|
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
|
||||||
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
|
) {
|
||||||
|
fun getMangaImpl(): MangaImpl {
|
||||||
|
return MangaImpl().apply {
|
||||||
|
url = this@BackupManga.url
|
||||||
|
title = this@BackupManga.title
|
||||||
|
artist = this@BackupManga.artist
|
||||||
|
author = this@BackupManga.author
|
||||||
|
description = this@BackupManga.description
|
||||||
|
genre = this@BackupManga.genre.joinToString()
|
||||||
|
status = this@BackupManga.status
|
||||||
|
thumbnail_url = this@BackupManga.thumbnailUrl
|
||||||
|
favorite = this@BackupManga.favorite
|
||||||
|
source = this@BackupManga.source
|
||||||
|
date_added = this@BackupManga.dateAdded
|
||||||
|
viewer = this@BackupManga.viewer
|
||||||
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChaptersImpl(): List<ChapterImpl> {
|
||||||
|
return chapters.map {
|
||||||
|
it.toChapterImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTrackingImpl(): List<TrackImpl> {
|
||||||
|
return tracking.map {
|
||||||
|
it.getTrackingImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(manga: Manga): BackupManga {
|
||||||
|
return BackupManga(
|
||||||
|
url = manga.url,
|
||||||
|
title = manga.title,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.getGenres() ?: emptyList(),
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnail_url,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
source = manga.source,
|
||||||
|
dateAdded = manga.date_added,
|
||||||
|
viewer = manga.viewer,
|
||||||
|
chapterFlags = manga.chapter_flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializer(forClass = Backup::class)
|
||||||
|
object BackupSerializer
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(0) var name: String = "",
|
||||||
|
@ProtoNumber(1) var sourceId: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
return BackupSource(
|
||||||
|
name = source.name,
|
||||||
|
sourceId = source.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
@Serializable
|
||||||
|
data class BackupTracking(
|
||||||
|
// in 1.x some of these values have different types or names
|
||||||
|
// syncId is called siteId in 1,x
|
||||||
|
@ProtoNumber(1) var syncId: Int,
|
||||||
|
// LibraryId is not null in 1.x
|
||||||
|
@ProtoNumber(2) var libraryId: Long,
|
||||||
|
@ProtoNumber(3) var mediaId: Int = 0,
|
||||||
|
// trackingUrl is called mediaUrl in 1.x
|
||||||
|
@ProtoNumber(4) var trackingUrl: String = "",
|
||||||
|
@ProtoNumber(5) var title: String = "",
|
||||||
|
// lastChapterRead is called last read, and it has been changed to a float in 1.x
|
||||||
|
@ProtoNumber(6) var lastChapterRead: Float = 0F,
|
||||||
|
@ProtoNumber(7) var totalChapters: Int = 0,
|
||||||
|
@ProtoNumber(8) var score: Float = 0F,
|
||||||
|
@ProtoNumber(9) var status: Int = 0,
|
||||||
|
// startedReadingDate is called startReadTime in 1.x
|
||||||
|
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||||
|
// finishedReadingDate is called endReadTime in 1.x
|
||||||
|
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||||
|
) {
|
||||||
|
fun getTrackingImpl(): TrackImpl {
|
||||||
|
return TrackImpl().apply {
|
||||||
|
sync_id = this@BackupTracking.syncId
|
||||||
|
media_id = this@BackupTracking.mediaId
|
||||||
|
library_id = this@BackupTracking.libraryId
|
||||||
|
title = this@BackupTracking.title
|
||||||
|
// convert from float to int because of 1.x types
|
||||||
|
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
||||||
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
|
score = this@BackupTracking.score
|
||||||
|
status = this@BackupTracking.status
|
||||||
|
started_reading_date = this@BackupTracking.startedReadingDate
|
||||||
|
finished_reading_date = this@BackupTracking.finishedReadingDate
|
||||||
|
tracking_url = this@BackupTracking.trackingUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(track: Track): BackupTracking {
|
||||||
|
return BackupTracking(
|
||||||
|
syncId = track.sync_id,
|
||||||
|
mediaId = track.media_id,
|
||||||
|
// forced not null so its compatible with 1.x backup system
|
||||||
|
libraryId = track.library_id!!,
|
||||||
|
title = track.title,
|
||||||
|
// convert to float for 1.x
|
||||||
|
lastChapterRead = track.last_chapter_read.toFloat(),
|
||||||
|
totalChapters = track.total_chapters,
|
||||||
|
score = track.score,
|
||||||
|
status = track.status,
|
||||||
|
startedReadingDate = track.started_reading_date,
|
||||||
|
finishedReadingDate = track.finished_reading_date,
|
||||||
|
trackingUrl = track.tracking_url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -20,21 +20,21 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
@ -44,24 +44,14 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of parser
|
* Version of parser
|
||||||
*/
|
*/
|
||||||
@ -101,7 +91,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
// Create root object
|
// Create root object
|
||||||
val root = JsonObject()
|
val root = JsonObject()
|
||||||
|
|
||||||
@ -302,7 +292,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
.doOnNext { pair ->
|
.doOnNext { pair ->
|
||||||
if (pair.first.isNotEmpty()) {
|
if (pair.first.isNotEmpty()) {
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
insertChapters(chapters)
|
updateChapters(chapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -469,45 +459,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
chapters.filter { it.id != null }
|
chapters.filter { it.id != null }
|
||||||
chapters.map { it.manga_id = manga.id }
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
insertChapters(chapters)
|
updateChapters(chapters)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns manga
|
|
||||||
*
|
|
||||||
* @return [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
|
||||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list containing manga from library
|
|
||||||
*
|
|
||||||
* @return [Manga] from library
|
|
||||||
*/
|
|
||||||
internal fun getFavoriteManga(): List<Manga> =
|
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun insertManga(manga: Manga): Long? =
|
|
||||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
private fun insertChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return number of backups.
|
|
||||||
*
|
|
||||||
* @return number of backups selected by user
|
|
||||||
*/
|
|
||||||
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
|
||||||
}
|
}
|
@ -0,0 +1,292 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore(context, notifier) {
|
||||||
|
|
||||||
|
private lateinit var backupManager: LegacyBackupManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores data from backup file.
|
||||||
|
*
|
||||||
|
* @param uri backup file to restore
|
||||||
|
*/
|
||||||
|
override fun restoreBackup(uri: Uri): Boolean {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
|
restoreAmount = mangasJson.size() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
mangasJson.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it.asJsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
|
||||||
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
*/
|
||||||
|
private fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun restoreMangaFetch(
|
||||||
|
source: Source,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap {
|
||||||
|
chapterFetchObservable(source, it, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
trackingFetchObservable(it, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreMangaNoFetch(
|
||||||
|
source: Source,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
|
trackingFetchObservable(manga, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
val errorMessage = if (it is NoChaptersException) {
|
||||||
|
context.getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
it.message
|
||||||
|
}
|
||||||
|
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||||
|
Pair(emptyList(), emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that refreshes tracking information
|
||||||
|
* @param manga manga that needs updating.
|
||||||
|
* @param tracks list containing tracks from restore file.
|
||||||
|
* @return [Observable] that contains updated track item
|
||||||
|
*/
|
||||||
|
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||||
|
return Observable.from(tracks)
|
||||||
|
.flatMap { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
track
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [BackupConst]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(
|
||||||
|
progress: Int,
|
||||||
|
amount: Int,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -6,23 +6,17 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.backup.models.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
object BackupRestoreValidator {
|
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
* @throws Exception if version or manga cannot be found.
|
* @throws Exception if version or manga cannot be found.
|
||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
@ -57,16 +51,16 @@ object BackupRestoreValidator {
|
|||||||
return Results(missingSources, missingTrackers)
|
return Results(missingSources, missingTrackers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
companion object {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
return extensionsMapping.asJsonArray
|
||||||
.map {
|
.map {
|
||||||
val items = it.asString.split(":")
|
val items = it.asString.split(":")
|
||||||
items[0].toLong() to items[1]
|
items[0].toLong() to items[1]
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
@ -1,3 +1,3 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
* JSON Serializer used to write / read [DHistory] to / from json
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -0,0 +1,65 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class AbstractBackupManager(protected val context: Context) {
|
||||||
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||||
|
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list containing manga from library
|
||||||
|
*
|
||||||
|
* @return [Manga] from library
|
||||||
|
*/
|
||||||
|
protected fun getFavoriteManga(): List<Manga> =
|
||||||
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun insertManga(manga: Manga): Long? =
|
||||||
|
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
protected fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.insertChapters(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters
|
||||||
|
*/
|
||||||
|
protected fun updateChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
abstract class AbstractBackupRestore(protected val context: Context, protected val notifier: BackupNotifier) {
|
||||||
|
protected val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress of a backup restore
|
||||||
|
*/
|
||||||
|
protected var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of manga in Json file (needed for restore)
|
||||||
|
*/
|
||||||
|
protected var restoreAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of source ID to source name from backup data
|
||||||
|
*/
|
||||||
|
protected var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing errors
|
||||||
|
*/
|
||||||
|
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
abstract fun restoreBackup(uri: Uri): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(context.externalCacheDir, "tachiyomi_restore.txt")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class AbstractBackupRestoreValidator {
|
||||||
|
protected val sourceManager: SourceManager by injectLazy()
|
||||||
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
abstract fun validate(context: Context, uri: Uri): Results
|
||||||
|
|
||||||
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
|
}
|
@ -181,6 +181,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val incognitoMode = "incognito_mode"
|
||||||
|
|
||||||
|
const val createLegacyBackup = "create_legacy_backup"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
@ -271,6 +271,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||||
|
|
||||||
|
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, false)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||||
|
@ -4,6 +4,7 @@ import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -13,13 +14,17 @@ import androidx.core.os.bundleOf
|
|||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||||
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
@ -31,6 +36,7 @@ import eu.kanade.tachiyomi.util.preference.onClick
|
|||||||
import eu.kanade.tachiyomi.util.preference.preference
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||||
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
import eu.kanade.tachiyomi.util.system.getFilePicker
|
import eu.kanade.tachiyomi.util.system.getFilePicker
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -53,36 +59,47 @@ class SettingsBackupController : SettingsController() {
|
|||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
||||||
titleRes = R.string.backup
|
titleRes = R.string.backup
|
||||||
|
|
||||||
preference {
|
preferenceCategory {
|
||||||
key = "pref_create_backup"
|
titleRes = R.string.backup
|
||||||
titleRes = R.string.pref_create_backup
|
|
||||||
summaryRes = R.string.pref_create_backup_summ
|
|
||||||
|
|
||||||
onClick {
|
preference {
|
||||||
if (!BackupCreateService.isRunning(context)) {
|
key = "pref_create_full_backup"
|
||||||
val ctrl = CreateBackupDialog()
|
titleRes = R.string.pref_create_full_backup
|
||||||
ctrl.targetController = this@SettingsBackupController
|
summaryRes = R.string.pref_create_full_backup_summary
|
||||||
ctrl.showDialog(router)
|
|
||||||
} else {
|
onClick {
|
||||||
context.toast(R.string.backup_in_progress)
|
backupClick(context, BackupConst.BACKUP_TYPE_FULL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preference {
|
||||||
|
key = "pref_restore_full_backup"
|
||||||
|
titleRes = R.string.pref_restore_full_backup
|
||||||
|
summaryRes = R.string.pref_restore_full_backup_summary
|
||||||
|
|
||||||
|
onClick {
|
||||||
|
restoreClick(context, CODE_FULL_BACKUP_RESTORE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preferenceCategory {
|
||||||
key = "pref_restore_backup"
|
titleRes = R.string.legacy_backup
|
||||||
titleRes = R.string.pref_restore_backup
|
|
||||||
summaryRes = R.string.pref_restore_backup_summ
|
|
||||||
|
|
||||||
onClick {
|
preference {
|
||||||
if (!BackupRestoreService.isRunning(context)) {
|
key = "pref_create_legacy_backup"
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
titleRes = R.string.pref_create_backup
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
summaryRes = R.string.pref_create_backup_summ
|
||||||
intent.type = "application/*"
|
|
||||||
val title = resources?.getString(R.string.file_select_backup)
|
onClick {
|
||||||
val chooser = Intent.createChooser(intent, title)
|
backupClick(context, BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
|
}
|
||||||
} else {
|
}
|
||||||
context.toast(R.string.restore_in_progress)
|
preference {
|
||||||
|
key = "pref_restore_legacy_backup"
|
||||||
|
titleRes = R.string.pref_restore_backup
|
||||||
|
summaryRes = R.string.pref_restore_backup_summ
|
||||||
|
|
||||||
|
onClick {
|
||||||
|
restoreClick(context, CODE_LEGACY_BACKUP_RESTORE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,6 +160,15 @@ class SettingsBackupController : SettingsController() {
|
|||||||
defaultValue = "1"
|
defaultValue = "1"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
|
preferences.backupInterval().asImmediateFlow { isVisible = it > 0 }
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.createLegacyBackup
|
||||||
|
titleRes = R.string.pref_backup_auto_create_legacy
|
||||||
|
summaryRes = R.string.pref_backup_auto_create_legacy_summary
|
||||||
|
defaultValue = false
|
||||||
|
|
||||||
preferences.backupInterval().asImmediateFlow { isVisible = it > 0 }
|
preferences.backupInterval().asImmediateFlow { isVisible = it > 0 }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
@ -167,7 +193,7 @@ class SettingsBackupController : SettingsController() {
|
|||||||
// Set backup Uri
|
// Set backup Uri
|
||||||
preferences.backupsDirectory().set(uri.toString())
|
preferences.backupsDirectory().set(uri.toString())
|
||||||
}
|
}
|
||||||
CODE_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
CODE_LEGACY_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
|
|
||||||
val uri = data.data
|
val uri = data.data
|
||||||
@ -182,39 +208,111 @@ class SettingsBackupController : SettingsController() {
|
|||||||
|
|
||||||
activity.toast(R.string.creating_backup)
|
activity.toast(R.string.creating_backup)
|
||||||
|
|
||||||
BackupCreateService.start(activity, file.uri, backupFlags)
|
BackupCreateService.start(activity, file.uri, backupFlags, BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
}
|
}
|
||||||
CODE_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
CODE_LEGACY_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
val uri = data.data
|
val uri = data.data
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
RestoreBackupDialog(uri).showDialog(router)
|
RestoreBackupDialog(uri, BackupConst.BACKUP_TYPE_LEGACY, isOnline = true).showDialog(router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CODE_FULL_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
val activity = activity ?: return
|
||||||
|
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = UniFile.fromUri(activity, uri)
|
||||||
|
|
||||||
|
activity.toast(R.string.creating_backup)
|
||||||
|
|
||||||
|
BackupCreateService.start(activity, file.uri, backupFlags, BackupConst.BACKUP_TYPE_FULL)
|
||||||
|
}
|
||||||
|
CODE_FULL_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
val options = arrayOf(
|
||||||
|
R.string.full_restore_offline,
|
||||||
|
R.string.full_restore_online
|
||||||
|
)
|
||||||
|
.map { activity!!.getString(it) }
|
||||||
|
MaterialDialog(activity!!)
|
||||||
|
.title(R.string.full_restore_mode)
|
||||||
|
.listItemsSingleChoice(
|
||||||
|
items = options,
|
||||||
|
initialSelection = 0
|
||||||
|
) { _, index, _ ->
|
||||||
|
RestoreBackupDialog(
|
||||||
|
uri,
|
||||||
|
BackupConst.BACKUP_TYPE_FULL,
|
||||||
|
isOnline = index != 0
|
||||||
|
).showDialog(router)
|
||||||
|
}
|
||||||
|
.positiveButton(R.string.action_restore)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBackup(flags: Int) {
|
private fun backupClick(context: Context, type: Int) {
|
||||||
|
if (!BackupCreateService.isRunning(context)) {
|
||||||
|
val ctrl = CreateBackupDialog(type)
|
||||||
|
ctrl.targetController = this@SettingsBackupController
|
||||||
|
ctrl.showDialog(router)
|
||||||
|
} else {
|
||||||
|
context.toast(R.string.backup_in_progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreClick(context: Context, type: Int) {
|
||||||
|
if (!BackupRestoreService.isRunning(context)) {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
intent.type = "application/*"
|
||||||
|
val title = resources?.getString(R.string.file_select_backup)
|
||||||
|
val chooser = Intent.createChooser(intent, title)
|
||||||
|
startActivityForResult(chooser, type)
|
||||||
|
} else {
|
||||||
|
context.toast(R.string.restore_in_progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBackup(flags: Int, type: Int) {
|
||||||
backupFlags = flags
|
backupFlags = flags
|
||||||
|
|
||||||
// Get dirs
|
// Get dirs
|
||||||
val currentDir = preferences.backupsDirectory().get()
|
val currentDir = preferences.backupsDirectory().get()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
val fileName = if (type == BackupConst.BACKUP_TYPE_FULL) BackupFull.getDefaultFilename() else Backup.getDefaultFilename()
|
||||||
// Use Android's built-in file creator
|
// Use Android's built-in file creator
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
.setType("application/*")
|
.setType("application/*")
|
||||||
.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
|
.putExtra(Intent.EXTRA_TITLE, fileName)
|
||||||
|
|
||||||
startActivityForResult(intent, CODE_BACKUP_CREATE)
|
startActivityForResult(intent, if (type == BackupConst.BACKUP_TYPE_FULL) CODE_FULL_BACKUP_CREATE else CODE_LEGACY_BACKUP_CREATE)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
// Handle errors where the android ROM doesn't support the built in picker
|
// Handle errors where the android ROM doesn't support the built in picker
|
||||||
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE)
|
startActivityForResult(preferences.context.getFilePicker(currentDir), if (type == BackupConst.BACKUP_TYPE_FULL) CODE_FULL_BACKUP_CREATE else CODE_LEGACY_BACKUP_CREATE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateBackupDialog : DialogController() {
|
class CreateBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
constructor(type: Int) : this(
|
||||||
|
bundleOf(
|
||||||
|
KEY_TYPE to type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val type = args.getInt(KEY_TYPE)
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
val options = arrayOf(
|
val options = arrayOf(
|
||||||
R.string.manga,
|
R.string.manga,
|
||||||
@ -226,7 +324,7 @@ class SettingsBackupController : SettingsController() {
|
|||||||
.map { activity.getString(it) }
|
.map { activity.getString(it) }
|
||||||
|
|
||||||
return MaterialDialog(activity)
|
return MaterialDialog(activity)
|
||||||
.title(R.string.pref_create_backup)
|
.title(R.string.create_backup)
|
||||||
.message(R.string.backup_choice)
|
.message(R.string.backup_choice)
|
||||||
.listItemsMultiChoice(
|
.listItemsMultiChoice(
|
||||||
items = options,
|
items = options,
|
||||||
@ -243,26 +341,38 @@ class SettingsBackupController : SettingsController() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(targetController as? SettingsBackupController)?.createBackup(flags)
|
(targetController as? SettingsBackupController)?.createBackup(flags, type)
|
||||||
}
|
}
|
||||||
.positiveButton(R.string.action_create)
|
.positiveButton(R.string.action_create)
|
||||||
.negativeButton(android.R.string.cancel)
|
.negativeButton(android.R.string.cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val KEY_TYPE = "CreateBackupDialog.type"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
constructor(uri: Uri) : this(
|
constructor(uri: Uri, type: Int, isOnline: Boolean) : this(
|
||||||
bundleOf(KEY_URI to uri)
|
bundleOf(
|
||||||
|
KEY_URI to uri,
|
||||||
|
KEY_TYPE to type,
|
||||||
|
KEY_MODE to isOnline
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
val uri: Uri = args.getParcelable(KEY_URI)!!
|
val uri: Uri = args.getParcelable(KEY_URI)!!
|
||||||
|
val type: Int = args.getInt(KEY_TYPE)
|
||||||
|
val isOnline: Boolean = args.getBoolean(KEY_MODE, true)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
var message = activity.getString(R.string.backup_restore_content)
|
var message = activity.getString(R.string.backup_restore_content)
|
||||||
|
|
||||||
val results = BackupRestoreValidator.validate(activity, uri)
|
val validator = if (type == BackupConst.BACKUP_TYPE_FULL) FullBackupRestoreValidator() else LegacyBackupRestoreValidator()
|
||||||
|
|
||||||
|
val results = validator.validate(activity, uri)
|
||||||
if (results.missingSources.isNotEmpty()) {
|
if (results.missingSources.isNotEmpty()) {
|
||||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
||||||
}
|
}
|
||||||
@ -271,10 +381,10 @@ class SettingsBackupController : SettingsController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MaterialDialog(activity)
|
MaterialDialog(activity)
|
||||||
.title(R.string.pref_restore_backup)
|
.title(R.string.restore_backup)
|
||||||
.message(text = message)
|
.message(text = message)
|
||||||
.positiveButton(R.string.action_restore) {
|
.positiveButton(R.string.action_restore) {
|
||||||
BackupRestoreService.start(activity, uri)
|
BackupRestoreService.start(activity, uri, type, isOnline)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
MaterialDialog(activity)
|
MaterialDialog(activity)
|
||||||
@ -286,12 +396,16 @@ class SettingsBackupController : SettingsController() {
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val KEY_URI = "RestoreBackupDialog.uri"
|
const val KEY_URI = "RestoreBackupDialog.uri"
|
||||||
|
const val KEY_TYPE = "RestoreBackupDialog.type"
|
||||||
|
const val KEY_MODE = "RestoreBackupDialog.mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val CODE_BACKUP_CREATE = 501
|
const val CODE_LEGACY_BACKUP_CREATE = 501
|
||||||
const val CODE_BACKUP_RESTORE = 502
|
const val CODE_LEGACY_BACKUP_RESTORE = 502
|
||||||
const val CODE_BACKUP_DIR = 503
|
const val CODE_BACKUP_DIR = 503
|
||||||
|
const val CODE_FULL_BACKUP_CREATE = 504
|
||||||
|
const val CODE_FULL_BACKUP_RESTORE = 505
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,14 +347,26 @@
|
|||||||
|
|
||||||
<!-- Backup section -->
|
<!-- Backup section -->
|
||||||
<string name="backup">Backup</string>
|
<string name="backup">Backup</string>
|
||||||
<string name="pref_create_backup">Create backup</string>
|
<string name="legacy_backup">Legacy Backup</string>
|
||||||
<string name="pref_create_backup_summ">Can be used to restore current library</string>
|
<string name="pref_create_full_backup">Create full backup</string>
|
||||||
<string name="pref_restore_backup">Restore backup</string>
|
<string name="pref_create_full_backup_summary">Can be used to restore current library</string>
|
||||||
<string name="pref_restore_backup_summ">Restore library from backup file</string>
|
<string name="pref_restore_full_backup">Restore full backup</string>
|
||||||
|
<string name="pref_restore_full_backup_summary">Restore library from backup file, only use this if your backup is a full type backup, this can be restored offline as well as online</string>
|
||||||
|
<string name="pref_create_backup">Create legacy backup</string>
|
||||||
|
<string name="pref_create_backup_summ">Can be used to restore current library in older versions of Tachiyomi</string>
|
||||||
|
<string name="pref_restore_backup">Restore legacy backup</string>
|
||||||
|
<string name="pref_restore_backup_summ">Restore library from a legacy backup file</string>
|
||||||
|
<string name="pref_backup_auto_create_legacy">Create legacy backup</string>
|
||||||
|
<string name="pref_backup_auto_create_legacy_summary">Creates a legacy backup alongside the full backup</string>
|
||||||
<string name="pref_backup_directory">Backup location</string>
|
<string name="pref_backup_directory">Backup location</string>
|
||||||
<string name="pref_backup_service_category">Automatic backups</string>
|
<string name="pref_backup_service_category">Automatic backups</string>
|
||||||
<string name="pref_backup_interval">Backup frequency</string>
|
<string name="pref_backup_interval">Backup frequency</string>
|
||||||
<string name="pref_backup_slots">Maximum backups</string>
|
<string name="pref_backup_slots">Maximum backups</string>
|
||||||
|
<string name="full_restore_mode">Network Mode</string>
|
||||||
|
<string name="full_restore_online">Restore online, much slower but gives you more updated info and chapters</string>
|
||||||
|
<string name="full_restore_offline">Restore offline, finishes quickly but contains only what your backup has</string>
|
||||||
|
<string name="create_backup">Create backup</string>
|
||||||
|
<string name="restore_backup">Restore backup</string>
|
||||||
<string name="source_not_found_name">Source not found: %1$s</string>
|
<string name="source_not_found_name">Source not found: %1$s</string>
|
||||||
<string name="tracker_not_logged_in">Not logged in: %1$s</string>
|
<string name="tracker_not_logged_in">Not logged in: %1$s</string>
|
||||||
<string name="backup_created">Backup created</string>
|
<string name="backup_created">Backup created</string>
|
||||||
|
@ -8,8 +8,9 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -37,7 +38,7 @@ import uy.kohesive.injekt.api.InjektRegistrar
|
|||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test class for the [BackupManager].
|
* Test class for the [LegacyBackupManager].
|
||||||
* Note that this does not include the backup create/restore services.
|
* Note that this does not include the backup create/restore services.
|
||||||
*/
|
*/
|
||||||
@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.LOLLIPOP])
|
@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.LOLLIPOP])
|
||||||
@ -59,7 +60,7 @@ class BackupTest {
|
|||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
lateinit var source: HttpSource
|
lateinit var source: HttpSource
|
||||||
|
|
||||||
lateinit var backupManager: BackupManager
|
lateinit var legacyBackupManager: LegacyBackupManager
|
||||||
|
|
||||||
lateinit var db: DatabaseHelper
|
lateinit var db: DatabaseHelper
|
||||||
|
|
||||||
@ -67,8 +68,8 @@ class BackupTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
app = RuntimeEnvironment.application
|
app = RuntimeEnvironment.application
|
||||||
context = app.applicationContext
|
context = app.applicationContext
|
||||||
backupManager = BackupManager(context)
|
legacyBackupManager = LegacyBackupManager(context)
|
||||||
db = backupManager.databaseHelper
|
db = legacyBackupManager.databaseHelper
|
||||||
|
|
||||||
// Mock the source manager
|
// Mock the source manager
|
||||||
val module = object : InjektModule {
|
val module = object : InjektModule {
|
||||||
@ -79,7 +80,7 @@ class BackupTest {
|
|||||||
Injekt.importModule(module)
|
Injekt.importModule(module)
|
||||||
|
|
||||||
source = mock(HttpSource::class.java)
|
source = mock(HttpSource::class.java)
|
||||||
`when`(backupManager.sourceManager.get(anyLong())).thenReturn(source)
|
`when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source)
|
||||||
|
|
||||||
root.add(Backup.MANGAS, mangaEntries)
|
root.add(Backup.MANGAS, mangaEntries)
|
||||||
root.add(Backup.CATEGORIES, categoryEntries)
|
root.add(Backup.CATEGORIES, categoryEntries)
|
||||||
@ -94,10 +95,10 @@ class BackupTest {
|
|||||||
initializeJsonTest(2)
|
initializeJsonTest(2)
|
||||||
|
|
||||||
// Create backup of empty database
|
// Create backup of empty database
|
||||||
backupManager.backupCategories(categoryEntries)
|
legacyBackupManager.backupCategories(categoryEntries)
|
||||||
|
|
||||||
// Restore Json
|
// Restore Json
|
||||||
backupManager.restoreCategories(categoryEntries)
|
legacyBackupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
// Check if empty
|
// Check if empty
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
val dbCats = db.getCategories().executeAsBlocking()
|
||||||
@ -116,10 +117,10 @@ class BackupTest {
|
|||||||
val category = addSingleCategory("category")
|
val category = addSingleCategory("category")
|
||||||
|
|
||||||
// Restore Json
|
// Restore Json
|
||||||
backupManager.restoreCategories(categoryEntries)
|
legacyBackupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
// Check if successful
|
// Check if successful
|
||||||
val dbCats = backupManager.databaseHelper.getCategories().executeAsBlocking()
|
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(1)
|
assertThat(dbCats).hasSize(1)
|
||||||
assertThat(dbCats[0].name).isEqualTo(category.name)
|
assertThat(dbCats[0].name).isEqualTo(category.name)
|
||||||
}
|
}
|
||||||
@ -143,10 +144,10 @@ class BackupTest {
|
|||||||
db.insertCategory(category).executeAsBlocking()
|
db.insertCategory(category).executeAsBlocking()
|
||||||
|
|
||||||
// Restore Json
|
// Restore Json
|
||||||
backupManager.restoreCategories(categoryEntries)
|
legacyBackupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
// Check if successful
|
// Check if successful
|
||||||
val dbCats = backupManager.databaseHelper.getCategories().executeAsBlocking()
|
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(5)
|
assertThat(dbCats).hasSize(5)
|
||||||
assertThat(dbCats[0].name).isEqualTo(category.name)
|
assertThat(dbCats[0].name).isEqualTo(category.name)
|
||||||
assertThat(dbCats[1].name).isEqualTo(category2.name)
|
assertThat(dbCats[1].name).isEqualTo(category2.name)
|
||||||
@ -168,27 +169,27 @@ class BackupTest {
|
|||||||
manga.viewer = 3
|
manga.viewer = 3
|
||||||
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
var favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
var favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(favoriteManga).hasSize(1)
|
assertThat(favoriteManga).hasSize(1)
|
||||||
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
||||||
|
|
||||||
// Update json with all options enabled
|
// Update json with all options enabled
|
||||||
mangaEntries.add(backupManager.backupMangaObject(manga, 1))
|
mangaEntries.add(legacyBackupManager.backupMangaObject(manga, 1))
|
||||||
|
|
||||||
// Change manga in database to default values
|
// Change manga in database to default values
|
||||||
val dbManga = getSingleManga("One Piece")
|
val dbManga = getSingleManga("One Piece")
|
||||||
dbManga.id = manga.id
|
dbManga.id = manga.id
|
||||||
db.insertManga(dbManga).executeAsBlocking()
|
db.insertManga(dbManga).executeAsBlocking()
|
||||||
|
|
||||||
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(favoriteManga).hasSize(1)
|
assertThat(favoriteManga).hasSize(1)
|
||||||
assertThat(favoriteManga[0].viewer).isEqualTo(0)
|
assertThat(favoriteManga[0].viewer).isEqualTo(0)
|
||||||
|
|
||||||
// Restore local manga
|
// Restore local manga
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
legacyBackupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
|
||||||
// Test if restore successful
|
// Test if restore successful
|
||||||
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(favoriteManga).hasSize(1)
|
assertThat(favoriteManga).hasSize(1)
|
||||||
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
||||||
|
|
||||||
@ -196,28 +197,28 @@ class BackupTest {
|
|||||||
clearDatabase()
|
clearDatabase()
|
||||||
|
|
||||||
// Test if successful
|
// Test if successful
|
||||||
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(favoriteManga).hasSize(0)
|
assertThat(favoriteManga).hasSize(0)
|
||||||
|
|
||||||
// Restore Json
|
// Restore Json
|
||||||
// Create JSON from manga to test parser
|
// Create JSON from manga to test parser
|
||||||
val json = backupManager.parser.toJsonTree(manga)
|
val json = legacyBackupManager.parser.toJsonTree(manga)
|
||||||
// Restore JSON from manga to test parser
|
// Restore JSON from manga to test parser
|
||||||
val jsonManga = backupManager.parser.fromJson<MangaImpl>(json)
|
val jsonManga = legacyBackupManager.parser.fromJson<MangaImpl>(json)
|
||||||
|
|
||||||
// Restore manga with fetch observable
|
// Restore manga with fetch observable
|
||||||
val networkManga = getSingleManga("One Piece")
|
val networkManga = getSingleManga("One Piece")
|
||||||
networkManga.description = "This is a description"
|
networkManga.description = "This is a description"
|
||||||
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
||||||
|
|
||||||
val obs = backupManager.restoreMangaFetchObservable(source, jsonManga)
|
val obs = legacyBackupManager.restoreMangaFetchObservable(source, jsonManga)
|
||||||
val testSubscriber = TestSubscriber<Manga>()
|
val testSubscriber = TestSubscriber<Manga>()
|
||||||
obs.subscribe(testSubscriber)
|
obs.subscribe(testSubscriber)
|
||||||
|
|
||||||
testSubscriber.assertNoErrors()
|
testSubscriber.assertNoErrors()
|
||||||
|
|
||||||
// Check if restore successful
|
// Check if restore successful
|
||||||
val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
val dbCats = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(1)
|
assertThat(dbCats).hasSize(1)
|
||||||
assertThat(dbCats[0].viewer).isEqualTo(3)
|
assertThat(dbCats[0].viewer).isEqualTo(3)
|
||||||
assertThat(dbCats[0].description).isEqualTo("This is a description")
|
assertThat(dbCats[0].description).isEqualTo("This is a description")
|
||||||
@ -233,7 +234,7 @@ class BackupTest {
|
|||||||
|
|
||||||
// Insert manga
|
// Insert manga
|
||||||
val manga = getSingleManga("One Piece")
|
val manga = getSingleManga("One Piece")
|
||||||
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
// Create restore list
|
// Create restore list
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
@ -244,8 +245,8 @@ class BackupTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check parser
|
// Check parser
|
||||||
val chaptersJson = backupManager.parser.toJsonTree(chapters)
|
val chaptersJson = legacyBackupManager.parser.toJsonTree(chapters)
|
||||||
val restoredChapters = backupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson)
|
val restoredChapters = legacyBackupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson)
|
||||||
|
|
||||||
// Fetch chapters from upstream
|
// Fetch chapters from upstream
|
||||||
// Create list
|
// Create list
|
||||||
@ -254,13 +255,13 @@ class BackupTest {
|
|||||||
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
||||||
|
|
||||||
// Call restoreChapterFetchObservable
|
// Call restoreChapterFetchObservable
|
||||||
val obs = backupManager.restoreChapterFetchObservable(source, manga, restoredChapters)
|
val obs = legacyBackupManager.restoreChapterFetchObservable(source, manga, restoredChapters)
|
||||||
val testSubscriber = TestSubscriber<Pair<List<Chapter>, List<Chapter>>>()
|
val testSubscriber = TestSubscriber<Pair<List<Chapter>, List<Chapter>>>()
|
||||||
obs.subscribe(testSubscriber)
|
obs.subscribe(testSubscriber)
|
||||||
|
|
||||||
testSubscriber.assertNoErrors()
|
testSubscriber.assertNoErrors()
|
||||||
|
|
||||||
val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbCats = legacyBackupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(10)
|
assertThat(dbCats).hasSize(10)
|
||||||
assertThat(dbCats[0].read).isEqualTo(true)
|
assertThat(dbCats[0].read).isEqualTo(true)
|
||||||
}
|
}
|
||||||
@ -274,13 +275,13 @@ class BackupTest {
|
|||||||
initializeJsonTest(2)
|
initializeJsonTest(2)
|
||||||
|
|
||||||
val manga = getSingleManga("One Piece")
|
val manga = getSingleManga("One Piece")
|
||||||
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
// Create chapter
|
// Create chapter
|
||||||
val chapter = getSingleChapter("Chapter 1")
|
val chapter = getSingleChapter("Chapter 1")
|
||||||
chapter.manga_id = manga.id
|
chapter.manga_id = manga.id
|
||||||
chapter.read = true
|
chapter.read = true
|
||||||
chapter.id = backupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId()
|
chapter.id = legacyBackupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
val historyJson = getSingleHistory(chapter)
|
val historyJson = getSingleHistory(chapter)
|
||||||
|
|
||||||
@ -288,13 +289,13 @@ class BackupTest {
|
|||||||
historyList.add(historyJson)
|
historyList.add(historyJson)
|
||||||
|
|
||||||
// Check parser
|
// Check parser
|
||||||
val historyListJson = backupManager.parser.toJsonTree(historyList)
|
val historyListJson = legacyBackupManager.parser.toJsonTree(historyList)
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(historyListJson)
|
val history = legacyBackupManager.parser.fromJson<List<DHistory>>(historyListJson)
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
backupManager.restoreHistoryForManga(history)
|
legacyBackupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
val historyDB = backupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
val historyDB = legacyBackupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
assertThat(historyDB).hasSize(1)
|
assertThat(historyDB).hasSize(1)
|
||||||
assertThat(historyDB[0].last_read).isEqualTo(1000)
|
assertThat(historyDB[0].last_read).isEqualTo(1000)
|
||||||
}
|
}
|
||||||
@ -310,15 +311,15 @@ class BackupTest {
|
|||||||
// Create mangas
|
// Create mangas
|
||||||
val manga = getSingleManga("One Piece")
|
val manga = getSingleManga("One Piece")
|
||||||
val manga2 = getSingleManga("Bleach")
|
val manga2 = getSingleManga("Bleach")
|
||||||
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
manga2.id = backupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId()
|
manga2.id = legacyBackupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
// Create track and add it to database
|
// Create track and add it to database
|
||||||
// This tests duplicate errors.
|
// This tests duplicate errors.
|
||||||
val track = getSingleTrack(manga)
|
val track = getSingleTrack(manga)
|
||||||
track.last_chapter_read = 5
|
track.last_chapter_read = 5
|
||||||
backupManager.databaseHelper.insertTrack(track).executeAsBlocking()
|
legacyBackupManager.databaseHelper.insertTrack(track).executeAsBlocking()
|
||||||
var trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
var trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
assertThat(trackDB).hasSize(1)
|
assertThat(trackDB).hasSize(1)
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(5)
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(5)
|
||||||
track.last_chapter_read = 7
|
track.last_chapter_read = 7
|
||||||
@ -330,22 +331,22 @@ class BackupTest {
|
|||||||
// Check parser and restore already in database
|
// Check parser and restore already in database
|
||||||
var trackList = listOf(track)
|
var trackList = listOf(track)
|
||||||
// Check parser
|
// Check parser
|
||||||
var trackListJson = backupManager.parser.toJsonTree(trackList)
|
var trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
|
||||||
var trackListRestore = backupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
var trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||||
backupManager.restoreTrackForManga(manga, trackListRestore)
|
legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
|
||||||
|
|
||||||
// Assert if restore works.
|
// Assert if restore works.
|
||||||
trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
assertThat(trackDB).hasSize(1)
|
assertThat(trackDB).hasSize(1)
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
||||||
|
|
||||||
// Check parser and restore already in database with lower chapter_read
|
// Check parser and restore already in database with lower chapter_read
|
||||||
track.last_chapter_read = 5
|
track.last_chapter_read = 5
|
||||||
trackList = listOf(track)
|
trackList = listOf(track)
|
||||||
backupManager.restoreTrackForManga(manga, trackList)
|
legacyBackupManager.restoreTrackForManga(manga, trackList)
|
||||||
|
|
||||||
// Assert if restore works.
|
// Assert if restore works.
|
||||||
trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
assertThat(trackDB).hasSize(1)
|
assertThat(trackDB).hasSize(1)
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
||||||
|
|
||||||
@ -353,12 +354,12 @@ class BackupTest {
|
|||||||
trackList = listOf(track2)
|
trackList = listOf(track2)
|
||||||
|
|
||||||
// Check parser
|
// Check parser
|
||||||
trackListJson = backupManager.parser.toJsonTree(trackList)
|
trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
|
||||||
trackListRestore = backupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||||
backupManager.restoreTrackForManga(manga2, trackListRestore)
|
legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
|
||||||
|
|
||||||
// Assert if restore works.
|
// Assert if restore works.
|
||||||
trackDB = backupManager.databaseHelper.getTracks(manga2).executeAsBlocking()
|
trackDB = legacyBackupManager.databaseHelper.getTracks(manga2).executeAsBlocking()
|
||||||
assertThat(trackDB).hasSize(1)
|
assertThat(trackDB).hasSize(1)
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(10)
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(10)
|
||||||
}
|
}
|
||||||
@ -372,12 +373,12 @@ class BackupTest {
|
|||||||
|
|
||||||
fun initializeJsonTest(version: Int) {
|
fun initializeJsonTest(version: Int) {
|
||||||
clearJson()
|
clearJson()
|
||||||
backupManager.setVersion(version)
|
legacyBackupManager.setVersion(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSingleCategory(name: String): Category {
|
fun addSingleCategory(name: String): Category {
|
||||||
val category = Category.create(name)
|
val category = Category.create(name)
|
||||||
val catJson = backupManager.parser.toJsonTree(category)
|
val catJson = legacyBackupManager.parser.toJsonTree(category)
|
||||||
categoryEntries.add(catJson)
|
categoryEntries.add(catJson)
|
||||||
return category
|
return category
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user