mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	More backup/restore code cleanup
This commit is contained in:
		| @@ -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() | ||||
| } | ||||
| @@ -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("") | ||||
|     } | ||||
| } | ||||
| @@ -5,9 +5,12 @@ import android.net.Uri | ||||
| import com.hippo.unifile.UniFile | ||||
| import data.Manga_sync | ||||
| 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.history.model.HistoryUpdate | ||||
| import eu.kanade.domain.manga.interactor.GetFavorites | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY | ||||
| 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.Manga | ||||
| 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 kotlinx.serialization.protobuf.ProtoBuf | ||||
| import logcat.LogPriority | ||||
| import okio.buffer | ||||
| import okio.gzip | ||||
| import okio.sink | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.FileOutputStream | ||||
| import java.util.Date | ||||
| import kotlin.math.max | ||||
| 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 isAutoBackup backup called from scheduled backup job | ||||
|      */ | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     override suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { | ||||
|         // Create root object | ||||
|         var backup: Backup? = null | ||||
|  | ||||
|         val databaseManga = getFavoriteManga() | ||||
|  | ||||
|         backup = Backup( | ||||
|     suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { | ||||
|         val databaseManga = getFavorites.await() | ||||
|         val backup = Backup( | ||||
|             backupMangas(databaseManga, flags), | ||||
|             backupCategories(flags), | ||||
|             emptyList(), | ||||
| @@ -73,7 +84,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|                     dir = dir.createDirectory("automatic") | ||||
|  | ||||
|                     // Delete older backups | ||||
|                     val numberOfBackups = numberOfBackups() | ||||
|                     val numberOfBackups = preferences.numberOfBackups().get() | ||||
|                     val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""") | ||||
|                     dir.listFiles { _, filename -> backupRegex.matches(filename) } | ||||
|                         .orEmpty() | ||||
| @@ -93,7 +104,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|                 throw IllegalStateException("Failed to get handle on file") | ||||
|             } | ||||
|  | ||||
|             val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!) | ||||
|             val byteArray = parser.encodeToByteArray(BackupSerializer, backup) | ||||
|             if (byteArray.isEmpty()) { | ||||
|                 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> { | ||||
|         // Check if user wants category information in backup | ||||
|         return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { | ||||
|             handler.awaitList { categoriesQueries.getCategories(categoryMapper) } | ||||
|             getCategories.await() | ||||
|                 .filterNot(Category::isSystemCategory) | ||||
|                 .map(backupCategoryMapper) | ||||
|         } else { | ||||
| @@ -170,7 +181,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         // Check if user wants category information in backup | ||||
|         if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { | ||||
|             // Backup categories for this manga | ||||
|             val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga.id) } | ||||
|             val categoriesForManga = getCategories.await(manga.id) | ||||
|             if (categoriesForManga.isNotEmpty()) { | ||||
|                 mangaObject.categories = categoriesForManga.map { it.order } | ||||
|             } | ||||
| @@ -201,7 +212,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         return mangaObject | ||||
|     } | ||||
|  | ||||
|     suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) { | ||||
|     internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) { | ||||
|         manga.id = dbManga._id | ||||
|         manga.copyFrom(dbManga) | ||||
|         updateManga(manga) | ||||
| @@ -213,7 +224,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|      * @param manga manga that needs updating | ||||
|      * @return Updated manga info. | ||||
|      */ | ||||
|     suspend fun restoreNewManga(manga: Manga): Manga { | ||||
|     internal suspend fun restoreNewManga(manga: Manga): Manga { | ||||
|         return manga.also { | ||||
|             it.initialized = it.description != null | ||||
|             it.id = insertManga(it) | ||||
| @@ -227,7 +238,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|      */ | ||||
|     internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) { | ||||
|         // Get categories from file and from db | ||||
|         val dbCategories = handler.awaitList { categoriesQueries.getCategories(categoryMapper) } | ||||
|         val dbCategories = getCategories.await() | ||||
|  | ||||
|         val categories = backupCategories.map { | ||||
|             var category = it.getCategory() | ||||
| @@ -267,7 +278,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|      * @param categories the categories to restore. | ||||
|      */ | ||||
|     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>>() | ||||
|  | ||||
|         categories.forEach { backupCategoryOrder -> | ||||
| @@ -353,7 +364,6 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         tracks.map { it.manga_id = manga.id!! } | ||||
|  | ||||
|         // Get tracks from database | ||||
|  | ||||
|         val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) } | ||||
|         val toUpdate = mutableListOf<Manga_sync>() | ||||
|         val toInsert = mutableListOf<Track>() | ||||
| @@ -452,4 +462,139 @@ class BackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         newChapters[true]?.let { updateKnownChapters(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!!, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ class BackupRestoreService : Service() { | ||||
|     private lateinit var wakeLock: PowerManager.WakeLock | ||||
|  | ||||
|     private lateinit var ioScope: CoroutineScope | ||||
|     private var restorer: AbstractBackupRestore<*>? = null | ||||
|     private var restorer: BackupRestorer? = null | ||||
|     private lateinit var notifier: BackupNotifier | ||||
|  | ||||
|     override fun onCreate() { | ||||
|   | ||||
| @@ -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.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.util.system.createFileInCacheDir | ||||
| import kotlinx.coroutines.Job | ||||
| import okio.buffer | ||||
| import okio.gzip | ||||
| import okio.source | ||||
| import java.io.File | ||||
| import java.text.SimpleDateFormat | ||||
| 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") | ||||
|     override suspend fun performRestore(uri: Uri): Boolean { | ||||
|         backupManager = BackupManager(context) | ||||
|  | ||||
|     private suspend fun performRestore(uri: Uri): Boolean { | ||||
|         val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() } | ||||
|         val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) | ||||
|  | ||||
| @@ -125,4 +182,15 @@ class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBacku | ||||
|         backupManager.restoreHistory(history) | ||||
|         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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -68,7 +68,7 @@ open class BrowseSourcePresenter( | ||||
|     private val sourceId: Long, | ||||
|     searchQuery: String? = null, | ||||
|     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 getManga: GetManga = Injekt.get(), | ||||
|     private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), | ||||
| @@ -153,7 +153,7 @@ open class BrowseSourcePresenter( | ||||
|         pager = createPager(query, filters) | ||||
|  | ||||
|         val sourceId = source.id | ||||
|         val sourceDisplayMode = prefs.sourceDisplayMode() | ||||
|         val sourceDisplayMode = preferences.sourceDisplayMode() | ||||
|  | ||||
|         pagerJob?.cancel() | ||||
|         pagerJob = presenterScope.launchIO { | ||||
|   | ||||
| @@ -18,11 +18,11 @@ import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class MorePresenter( | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     preferencesHelper: PreferencesHelper = Injekt.get(), | ||||
|     preferences: PreferencesHelper = Injekt.get(), | ||||
| ) : BasePresenter<MoreController>() { | ||||
|  | ||||
|     val downloadedOnly = preferencesHelper.downloadedOnly().asState() | ||||
|     val incognitoMode = preferencesHelper.incognitoMode().asState() | ||||
|     val downloadedOnly = preferences.downloadedOnly().asState() | ||||
|     val incognitoMode = preferences.incognitoMode().asState() | ||||
|  | ||||
|     private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped) | ||||
|     val downloadQueueState: StateFlow<DownloadQueueState> = _state.asStateFlow() | ||||
|   | ||||
| @@ -50,17 +50,17 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int { | ||||
|     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 | ||||
|  | ||||
|     val categories = dbCategories.ifEmpty { listOf(0L) } | ||||
|  | ||||
|     // 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 | ||||
|  | ||||
|     val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toLong() } | ||||
|     val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toLong() } | ||||
|     val includedCategories = preferences.downloadNewChapterCategories().get().map { it.toLong() } | ||||
|     val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() } | ||||
|  | ||||
|     // Default: Download from all categories | ||||
|     if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| object ChapterSettingsHelper { | ||||
|  | ||||
|     private val prefs: PreferencesHelper by injectLazy() | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|     private val getFavorites: GetFavorites by injectLazy() | ||||
|     private val setMangaChapterFlags: SetMangaChapterFlags by injectLazy() | ||||
|  | ||||
| @@ -18,7 +18,7 @@ object ChapterSettingsHelper { | ||||
|      * Updates the global Chapter Settings in Preferences. | ||||
|      */ | ||||
|     fun setGlobalSettings(manga: Manga) { | ||||
|         prefs.setChapterSettingsDefault(manga.toDbManga()) | ||||
|         preferences.setChapterSettingsDefault(manga.toDbManga()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -28,12 +28,12 @@ object ChapterSettingsHelper { | ||||
|         launchIO { | ||||
|             setMangaChapterFlags.awaitSetAllFlags( | ||||
|                 mangaId = manga.id, | ||||
|                 unreadFilter = prefs.filterChapterByRead().toLong(), | ||||
|                 downloadedFilter = prefs.filterChapterByDownloaded().toLong(), | ||||
|                 bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), | ||||
|                 sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), | ||||
|                 sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), | ||||
|                 displayMode = prefs.displayChapterByNameOrNumber().toLong(), | ||||
|                 unreadFilter = preferences.filterChapterByRead().toLong(), | ||||
|                 downloadedFilter = preferences.filterChapterByDownloaded().toLong(), | ||||
|                 bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(), | ||||
|                 sortingMode = preferences.sortChapterBySourceOrNumber().toLong(), | ||||
|                 sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(), | ||||
|                 displayMode = preferences.displayChapterByNameOrNumber().toLong(), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| @@ -41,12 +41,12 @@ object ChapterSettingsHelper { | ||||
|     suspend fun applySettingDefaults(mangaId: Long) { | ||||
|         setMangaChapterFlags.awaitSetAllFlags( | ||||
|             mangaId = mangaId, | ||||
|             unreadFilter = prefs.filterChapterByRead().toLong(), | ||||
|             downloadedFilter = prefs.filterChapterByDownloaded().toLong(), | ||||
|             bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), | ||||
|             sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), | ||||
|             sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), | ||||
|             displayMode = prefs.displayChapterByNameOrNumber().toLong(), | ||||
|             unreadFilter = preferences.filterChapterByRead().toLong(), | ||||
|             downloadedFilter = preferences.filterChapterByDownloaded().toLong(), | ||||
|             bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(), | ||||
|             sortingMode = preferences.sortChapterBySourceOrNumber().toLong(), | ||||
|             sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(), | ||||
|             displayMode = preferences.displayChapterByNameOrNumber().toLong(), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -59,12 +59,12 @@ object ChapterSettingsHelper { | ||||
|                 .map { manga -> | ||||
|                     setMangaChapterFlags.awaitSetAllFlags( | ||||
|                         mangaId = manga.id, | ||||
|                         unreadFilter = prefs.filterChapterByRead().toLong(), | ||||
|                         downloadedFilter = prefs.filterChapterByDownloaded().toLong(), | ||||
|                         bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(), | ||||
|                         sortingMode = prefs.sortChapterBySourceOrNumber().toLong(), | ||||
|                         sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(), | ||||
|                         displayMode = prefs.displayChapterByNameOrNumber().toLong(), | ||||
|                         unreadFilter = preferences.filterChapterByRead().toLong(), | ||||
|                         downloadedFilter = preferences.filterChapterByDownloaded().toLong(), | ||||
|                         bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(), | ||||
|                         sortingMode = preferences.sortChapterBySourceOrNumber().toLong(), | ||||
|                         sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(), | ||||
|                         displayMode = preferences.displayChapterByNameOrNumber().toLong(), | ||||
|                     ) | ||||
|                 } | ||||
|         } | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
| fun Context.createReaderThemeContext(): Context { | ||||
|     val prefs = Injekt.get<PreferencesHelper>() | ||||
|     val isDarkBackground = when (prefs.readerTheme().get()) { | ||||
|     val preferences = Injekt.get<PreferencesHelper>() | ||||
|     val isDarkBackground = when (preferences.readerTheme().get()) { | ||||
|         1, 2 -> true // Black, Gray | ||||
|         3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default | ||||
|         else -> false // White | ||||
| @@ -333,7 +333,7 @@ fun Context.createReaderThemeContext(): Context { | ||||
|  | ||||
|         val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi) | ||||
|         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) } | ||||
|         return wrappedContext | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user