More backup/restore code cleanup

This commit is contained in:
arkon 2022-08-06 15:27:25 -04:00
parent 19eb4aaac9
commit 9f0052eceb
10 changed files with 270 additions and 415 deletions

View File

@ -1,205 +0,0 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.toSChapter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import data.Mangas as DbManga
import eu.kanade.domain.manga.model.Manga as DomainManga
abstract class AbstractBackupManager(protected val context: Context) {
protected val handler: DatabaseHandler = Injekt.get()
internal val sourceManager: SourceManager = Injekt.get()
internal val trackManager: TrackManager = Injekt.get()
protected val preferences: PreferencesHelper = Injekt.get()
private val getFavorites: GetFavorites = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
/**
* Returns manga
*
* @return [Manga], null if not found
*/
internal suspend fun getMangaFromDatabase(url: String, source: Long): DbManga? {
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
}
/**
* Fetches chapter information.
*
* @param source source of manga
* @param manga manga that needs updating
* @param chapters list of chapters in the backup
* @return Updated manga chapters.
*/
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
val syncedChapters = syncChaptersWithSource.await(fetchedChapters, manga.toDomainManga()!!, source)
if (syncedChapters.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters)
}
return syncedChapters.first.map { it.toDbChapter() } to syncedChapters.second.map { it.toDbChapter() }
}
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
protected suspend fun getFavoriteManga(): List<DomainManga> {
return getFavorites.await()
}
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal suspend fun insertManga(manga: Manga): Long {
return handler.awaitOne(true) {
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.getGenres(),
title = manga.title,
status = manga.status.toLong(),
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite,
lastUpdate = manga.last_update,
nextUpdate = 0L,
initialized = manga.initialized,
viewerFlags = manga.viewer_flags.toLong(),
chapterFlags = manga.chapter_flags.toLong(),
coverLastModified = manga.cover_last_modified,
dateAdded = manga.date_added,
)
mangasQueries.selectLastInsertedRowId()
}
}
internal suspend fun updateManga(manga: Manga): Long {
handler.await(true) {
mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status.toLong(),
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite.toLong(),
lastUpdate = manga.last_update,
initialized = manga.initialized.toLong(),
viewer = manga.viewer_flags.toLong(),
chapterFlags = manga.chapter_flags.toLong(),
coverLastModified = manga.cover_last_modified,
dateAdded = manga.date_added,
mangaId = manga.id!!,
)
}
return manga.id!!
}
/**
* Inserts list of chapters
*/
protected suspend fun insertChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.insert(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read,
chapter.bookmark,
chapter.last_page_read.toLong(),
chapter.chapter_number,
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
)
}
}
}
/**
* Updates a list of chapters
*/
protected suspend fun updateChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read.toLong(),
chapter.bookmark.toLong(),
chapter.last_page_read.toLong(),
chapter.chapter_number.toDouble(),
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
chapter.id!!,
)
}
}
}
/**
* Updates a list of chapters with known database ids
*/
protected suspend fun updateKnownChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
mangaId = null,
url = null,
name = null,
scanlator = null,
read = chapter.read.toLong(),
bookmark = chapter.bookmark.toLong(),
lastPageRead = chapter.last_page_read.toLong(),
chapterNumber = null,
sourceOrder = null,
dateFetch = null,
dateUpload = null,
chapterId = chapter.id!!,
)
}
}
}
/**
* Return number of backups.
*
* @return number of backups selected by user
*/
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
}

View File

@ -1,153 +0,0 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.chapter.NoChaptersException
import eu.kanade.tachiyomi.R
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.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
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<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
protected val handler: DatabaseHandler by injectLazy()
protected val trackManager: TrackManager by injectLazy()
var job: Job? = null
protected lateinit var backupManager: T
protected var restoreAmount = 0
protected var restoreProgress = 0
/**
* Mapping of source ID to source name from backup data
*/
protected var sourceMapping: Map<Long, String> = emptyMap()
protected val errors = mutableListOf<Pair<Date, String>>()
abstract suspend fun performRestore(uri: Uri): Boolean
suspend fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
restoreProgress = 0
errors.clear()
if (!performRestore(uri)) {
return false
}
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
return true
}
/**
* Fetches chapter information.
*
* @param source source of manga
* @param manga manga that needs updating
* @return Updated manga chapters.
*/
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
return try {
backupManager.restoreChapters(source, manga, chapters)
} catch (e: Exception) {
// If there's any error, return empty update and continue.
val errorMessage = if (e is NoChaptersException) {
context.getString(R.string.no_chapters_error)
} else {
e.message
}
errors.add(Date() to "${manga.title} - $errorMessage")
Pair(emptyList(), emptyList())
}
}
/**
* Refreshes tracking information.
*
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
*/
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id.toLong())
if (service != null && service.isLogged) {
try {
val updatedTrack = service.refresh(track)
handler.await {
manga_syncQueries.insert(
updatedTrack.manga_id,
updatedTrack.sync_id.toLong(),
updatedTrack.media_id,
updatedTrack.library_id,
updatedTrack.title,
updatedTrack.last_chapter_read.toDouble(),
updatedTrack.total_chapters.toLong(),
updatedTrack.status.toLong(),
updatedTrack.score,
updatedTrack.tracking_url,
updatedTrack.started_reading_date,
updatedTrack.finished_reading_date,
)
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
} else {
val serviceName = service?.nameRes()?.let { context.getString(it) }
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
}
}
}
/**
* Called to update dialog in [BackupConst]
*
* @param progress restore progress
* @param amount total restoreAmount of manga
* @param title title of restored manga
*/
internal fun showRestoreProgress(
progress: Int,
amount: Int,
title: String,
) {
notifier.showRestoreProgress(title, progress, amount)
}
internal fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
file.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return file
}
} catch (e: Exception) {
// Empty
}
return File("")
}
}

View File

@ -5,9 +5,12 @@ import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import data.Manga_sync import data.Manga_sync
import data.Mangas import data.Mangas
import eu.kanade.data.category.categoryMapper import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.history.model.HistoryUpdate import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
@ -29,35 +32,43 @@ import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority import logcat.LogPriority
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.sink import okio.sink
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Date import java.util.Date
import kotlin.math.max import kotlin.math.max
import eu.kanade.domain.manga.model.Manga as DomainManga import eu.kanade.domain.manga.model.Manga as DomainManga
class BackupManager(context: Context) : AbstractBackupManager(context) { class BackupManager(
private val context: Context,
) {
val parser = ProtoBuf private val handler: DatabaseHandler = Injekt.get()
private val sourceManager: SourceManager = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get()
private val getCategories: GetCategories = Injekt.get()
private val getFavorites: GetFavorites = Injekt.get()
internal val parser = ProtoBuf
/** /**
* Create backup Json file from database * Create backup file from database
* *
* @param uri path of Uri * @param uri path of Uri
* @param isAutoBackup backup called from scheduled backup job * @param isAutoBackup backup called from scheduled backup job
*/ */
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
// Create root object val databaseManga = getFavorites.await()
var backup: Backup? = null val backup = Backup(
val databaseManga = getFavoriteManga()
backup = Backup(
backupMangas(databaseManga, flags), backupMangas(databaseManga, flags),
backupCategories(flags), backupCategories(flags),
emptyList(), emptyList(),
@ -73,7 +84,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
dir = dir.createDirectory("automatic") dir = dir.createDirectory("automatic")
// Delete older backups // Delete older backups
val numberOfBackups = numberOfBackups() val numberOfBackups = preferences.numberOfBackups().get()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""") val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
dir.listFiles { _, filename -> backupRegex.matches(filename) } dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty() .orEmpty()
@ -93,7 +104,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
throw IllegalStateException("Failed to get handle on file") throw IllegalStateException("Failed to get handle on file")
} }
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!) val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
if (byteArray.isEmpty()) { if (byteArray.isEmpty()) {
throw IllegalStateException(context.getString(R.string.empty_backup_error)) throw IllegalStateException(context.getString(R.string.empty_backup_error))
} }
@ -133,7 +144,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
private suspend fun backupCategories(options: Int): List<BackupCategory> { private suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup // Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
handler.awaitList { categoriesQueries.getCategories(categoryMapper) } getCategories.await()
.filterNot(Category::isSystemCategory) .filterNot(Category::isSystemCategory)
.map(backupCategoryMapper) .map(backupCategoryMapper)
} else { } else {
@ -170,7 +181,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants category information in backup // Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga.id) } val categoriesForManga = getCategories.await(manga.id)
if (categoriesForManga.isNotEmpty()) { if (categoriesForManga.isNotEmpty()) {
mangaObject.categories = categoriesForManga.map { it.order } mangaObject.categories = categoriesForManga.map { it.order }
} }
@ -201,7 +212,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
return mangaObject return mangaObject
} }
suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) { internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) {
manga.id = dbManga._id manga.id = dbManga._id
manga.copyFrom(dbManga) manga.copyFrom(dbManga)
updateManga(manga) updateManga(manga)
@ -213,7 +224,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga manga that needs updating * @param manga manga that needs updating
* @return Updated manga info. * @return Updated manga info.
*/ */
suspend fun restoreNewManga(manga: Manga): Manga { internal suspend fun restoreNewManga(manga: Manga): Manga {
return manga.also { return manga.also {
it.initialized = it.description != null it.initialized = it.description != null
it.id = insertManga(it) it.id = insertManga(it)
@ -227,7 +238,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
*/ */
internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) { internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
// Get categories from file and from db // Get categories from file and from db
val dbCategories = handler.awaitList { categoriesQueries.getCategories(categoryMapper) } val dbCategories = getCategories.await()
val categories = backupCategories.map { val categories = backupCategories.map {
var category = it.getCategory() var category = it.getCategory()
@ -267,7 +278,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
* @param categories the categories to restore. * @param categories the categories to restore.
*/ */
internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) { internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = handler.awaitList { categoriesQueries.getCategories() } val dbCategories = getCategories.await()
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>() val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
categories.forEach { backupCategoryOrder -> categories.forEach { backupCategoryOrder ->
@ -353,7 +364,6 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
tracks.map { it.manga_id = manga.id!! } tracks.map { it.manga_id = manga.id!! }
// Get tracks from database // Get tracks from database
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) } val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) }
val toUpdate = mutableListOf<Manga_sync>() val toUpdate = mutableListOf<Manga_sync>()
val toInsert = mutableListOf<Track>() val toInsert = mutableListOf<Track>()
@ -452,4 +462,139 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
newChapters[true]?.let { updateKnownChapters(it) } newChapters[true]?.let { updateKnownChapters(it) }
newChapters[false]?.let { insertChapters(it) } newChapters[false]?.let { insertChapters(it) }
} }
/**
* Returns manga
*
* @return [Manga], null if not found
*/
internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
}
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
private suspend fun insertManga(manga: Manga): Long {
return handler.awaitOne(true) {
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.getGenres(),
title = manga.title,
status = manga.status.toLong(),
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite,
lastUpdate = manga.last_update,
nextUpdate = 0L,
initialized = manga.initialized,
viewerFlags = manga.viewer_flags.toLong(),
chapterFlags = manga.chapter_flags.toLong(),
coverLastModified = manga.cover_last_modified,
dateAdded = manga.date_added,
)
mangasQueries.selectLastInsertedRowId()
}
}
private suspend fun updateManga(manga: Manga): Long {
handler.await(true) {
mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status.toLong(),
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite.toLong(),
lastUpdate = manga.last_update,
initialized = manga.initialized.toLong(),
viewer = manga.viewer_flags.toLong(),
chapterFlags = manga.chapter_flags.toLong(),
coverLastModified = manga.cover_last_modified,
dateAdded = manga.date_added,
mangaId = manga.id!!,
)
}
return manga.id!!
}
/**
* Inserts list of chapters
*/
private suspend fun insertChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.insert(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read,
chapter.bookmark,
chapter.last_page_read.toLong(),
chapter.chapter_number,
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
)
}
}
}
/**
* Updates a list of chapters
*/
private suspend fun updateChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read.toLong(),
chapter.bookmark.toLong(),
chapter.last_page_read.toLong(),
chapter.chapter_number.toDouble(),
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
chapter.id!!,
)
}
}
}
/**
* Updates a list of chapters with known database ids
*/
private suspend fun updateKnownChapters(chapters: List<Chapter>) {
handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
mangaId = null,
url = null,
name = null,
scanlator = null,
read = chapter.read.toLong(),
bookmark = chapter.bookmark.toLong(),
lastPageRead = chapter.last_page_read.toLong(),
chapterNumber = null,
sourceOrder = null,
dateFetch = null,
dateUpload = null,
chapterId = chapter.id!!,
)
}
}
}
} }

View File

@ -69,7 +69,7 @@ class BackupRestoreService : Service() {
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var ioScope: CoroutineScope private lateinit var ioScope: CoroutineScope
private var restorer: AbstractBackupRestore<*>? = null private var restorer: BackupRestorer? = null
private lateinit var notifier: BackupNotifier private lateinit var notifier: BackupNotifier
override fun onCreate() { override fun onCreate() {

View File

@ -11,17 +11,74 @@ import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.Job
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.source import okio.source
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale
class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<BackupManager>(context, notifier) { class BackupRestorer(
private val context: Context,
private val notifier: BackupNotifier,
) {
var job: Job? = null
private var backupManager = BackupManager(context)
private var restoreAmount = 0
private var restoreProgress = 0
/**
* Mapping of source ID to source name from backup data
*/
private var sourceMapping: Map<Long, String> = emptyMap()
private val errors = mutableListOf<Pair<Date, String>>()
suspend fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
restoreProgress = 0
errors.clear()
if (!performRestore(uri)) {
return false
}
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
return true
}
fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
file.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return file
}
} catch (e: Exception) {
// Empty
}
return File("")
}
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun performRestore(uri: Uri): Boolean { private suspend fun performRestore(uri: Uri): Boolean {
backupManager = BackupManager(context)
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() } val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
@ -125,4 +182,15 @@ class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBacku
backupManager.restoreHistory(history) backupManager.restoreHistory(history)
backupManager.restoreTracking(manga, tracks) backupManager.restoreTracking(manga, tracks)
} }
/**
* 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)
}
} }

View File

@ -68,7 +68,7 @@ open class BrowseSourcePresenter(
private val sourceId: Long, private val sourceId: Long,
searchQuery: String? = null, searchQuery: String? = null,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
@ -153,7 +153,7 @@ open class BrowseSourcePresenter(
pager = createPager(query, filters) pager = createPager(query, filters)
val sourceId = source.id val sourceId = source.id
val sourceDisplayMode = prefs.sourceDisplayMode() val sourceDisplayMode = preferences.sourceDisplayMode()
pagerJob?.cancel() pagerJob?.cancel()
pagerJob = presenterScope.launchIO { pagerJob = presenterScope.launchIO {

View File

@ -18,11 +18,11 @@ import uy.kohesive.injekt.api.get
class MorePresenter( class MorePresenter(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
preferencesHelper: PreferencesHelper = Injekt.get(), preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<MoreController>() { ) : BasePresenter<MoreController>() {
val downloadedOnly = preferencesHelper.downloadedOnly().asState() val downloadedOnly = preferences.downloadedOnly().asState()
val incognitoMode = preferencesHelper.incognitoMode().asState() val incognitoMode = preferences.incognitoMode().asState()
private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped) private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
val downloadQueueState: StateFlow<DownloadQueueState> = _state.asStateFlow() val downloadQueueState: StateFlow<DownloadQueueState> = _state.asStateFlow()

View File

@ -50,17 +50,17 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
return coverCache.deleteFromCache(this, true) return coverCache.deleteFromCache(this, true)
} }
fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, prefs: PreferencesHelper): Boolean { fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: PreferencesHelper): Boolean {
if (!favorite) return false if (!favorite) return false
val categories = dbCategories.ifEmpty { listOf(0L) } val categories = dbCategories.ifEmpty { listOf(0L) }
// Boolean to determine if user wants to automatically download new chapters. // Boolean to determine if user wants to automatically download new chapters.
val downloadNewChapter = prefs.downloadNewChapter().get() val downloadNewChapter = preferences.downloadNewChapter().get()
if (!downloadNewChapter) return false if (!downloadNewChapter) return false
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toLong() } val includedCategories = preferences.downloadNewChapterCategories().get().map { it.toLong() }
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toLong() } val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() }
// Default: Download from all categories // Default: Download from all categories
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true

View File

@ -10,7 +10,7 @@ import uy.kohesive.injekt.injectLazy
object ChapterSettingsHelper { object ChapterSettingsHelper {
private val prefs: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val getFavorites: GetFavorites by injectLazy() private val getFavorites: GetFavorites by injectLazy()
private val setMangaChapterFlags: SetMangaChapterFlags by injectLazy() private val setMangaChapterFlags: SetMangaChapterFlags by injectLazy()
@ -18,7 +18,7 @@ object ChapterSettingsHelper {
* Updates the global Chapter Settings in Preferences. * Updates the global Chapter Settings in Preferences.
*/ */
fun setGlobalSettings(manga: Manga) { fun setGlobalSettings(manga: Manga) {
prefs.setChapterSettingsDefault(manga.toDbManga()) preferences.setChapterSettingsDefault(manga.toDbManga())
} }
/** /**
@ -28,12 +28,12 @@ object ChapterSettingsHelper {
launchIO { launchIO {
setMangaChapterFlags.awaitSetAllFlags( setMangaChapterFlags.awaitSetAllFlags(
mangaId = manga.id, mangaId = manga.id,
unreadFilter = prefs.filterChapterByRead().toLong(), unreadFilter = preferences.filterChapterByRead().toLong(),
downloadedFilter = prefs.filterChapterByDownloaded().toLong(), downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
displayMode = prefs.displayChapterByNameOrNumber().toLong(), displayMode = preferences.displayChapterByNameOrNumber().toLong(),
) )
} }
} }
@ -41,12 +41,12 @@ object ChapterSettingsHelper {
suspend fun applySettingDefaults(mangaId: Long) { suspend fun applySettingDefaults(mangaId: Long) {
setMangaChapterFlags.awaitSetAllFlags( setMangaChapterFlags.awaitSetAllFlags(
mangaId = mangaId, mangaId = mangaId,
unreadFilter = prefs.filterChapterByRead().toLong(), unreadFilter = preferences.filterChapterByRead().toLong(),
downloadedFilter = prefs.filterChapterByDownloaded().toLong(), downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
displayMode = prefs.displayChapterByNameOrNumber().toLong(), displayMode = preferences.displayChapterByNameOrNumber().toLong(),
) )
} }
@ -59,12 +59,12 @@ object ChapterSettingsHelper {
.map { manga -> .map { manga ->
setMangaChapterFlags.awaitSetAllFlags( setMangaChapterFlags.awaitSetAllFlags(
mangaId = manga.id, mangaId = manga.id,
unreadFilter = prefs.filterChapterByRead().toLong(), unreadFilter = preferences.filterChapterByRead().toLong(),
downloadedFilter = prefs.filterChapterByDownloaded().toLong(), downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
displayMode = prefs.displayChapterByNameOrNumber().toLong(), displayMode = preferences.displayChapterByNameOrNumber().toLong(),
) )
} }
} }

View File

@ -319,8 +319,8 @@ fun Context.isNightMode(): Boolean {
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
*/ */
fun Context.createReaderThemeContext(): Context { fun Context.createReaderThemeContext(): Context {
val prefs = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val isDarkBackground = when (prefs.readerTheme().get()) { val isDarkBackground = when (preferences.readerTheme().get()) {
1, 2 -> true // Black, Gray 1, 2 -> true // Black, Gray
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
else -> false // White else -> false // White
@ -333,7 +333,7 @@ fun Context.createReaderThemeContext(): Context {
val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi) val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
wrappedContext.applyOverrideConfiguration(overrideConf) wrappedContext.applyOverrideConfiguration(overrideConf)
ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get()) ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get())
.forEach { wrappedContext.theme.applyStyle(it, true) } .forEach { wrappedContext.theme.applyStyle(it, true) }
return wrappedContext return wrappedContext
} }