@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.model.copyFrom
import eu.kanade.tachiyomi.source.sourcePreferences
import eu.kanade.tachiyomi.util.BackupUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@ -27,7 +26,6 @@ import tachiyomi.core.preference.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Manga_sync
import tachiyomi.data.Mangas
import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@ -35,6 +33,8 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
import tachiyomi.i18n.MR
@ -50,23 +50,25 @@ import kotlin.math.max
class BackupRestorer (
private val context : Context ,
private val notifier : BackupNotifier ,
) {
private val handler : DatabaseHandler = Injekt . get ( )
private val updateManga : UpdateManga = Injekt . get ( )
private val getCategories : GetCategories = Injekt . get ( )
private val getChaptersByMangaId : GetChaptersByMangaId = Injekt . get ( )
private val fetchInterval : FetchInterval = Injekt . get ( )
private val preferenceStore : PreferenceStore = Injekt . get ( )
private val libraryPreferences : LibraryPreferences = Injekt . get ( )
private val handler : DatabaseHandler = Injekt . get ( ) ,
private val getCategories : GetCategories = Injekt . get ( ) ,
private val getManga : GetManga = Injekt . get ( ) ,
private val getMangaByUrlAndSourceId : GetMangaByUrlAndSourceId = Injekt . get ( ) ,
private val getChaptersByMangaId : GetChaptersByMangaId = Injekt . get ( ) ,
private val updateManga : UpdateManga = Injekt . get ( ) ,
private val fetchInterval : FetchInterval = Injekt . get ( ) ,
private var now = ZonedDateTime . now ( )
private var currentFetchWindow = fetchInterval . getWindow ( now )
private val preferenceStore : PreferenceStore = Injekt . get ( ) ,
private val libraryPreferences : LibraryPreferences = Injekt . get ( ) ,
) {
private var restoreAmount = 0
private var restoreProgress = 0
private var now = ZonedDateTime . now ( )
private var currentFetchWindow = fetchInterval . getWindow ( now )
/ * *
* Mapping of source ID to source name from backup data
* /
@ -76,27 +78,22 @@ class BackupRestorer(
suspend fun syncFromBackup ( uri : Uri , sync : Boolean ) {
val startTime = System . currentTimeMillis ( )
restoreProgress = 0
errors . clear ( )
performRestore ( uri , sync )
prepareState ( )
restoreFromFile ( uri , sync )
val endTime = System . currentTimeMillis ( )
val time = endTime - startTime
val logFile = writeErrorLog ( )
if ( sync ) {
notifier . showRestoreComplete (
time ,
errors . size ,
logFile . parent ,
logFile . name ,
contentTitle = context . s tringResource( MR . strings . library _s ync_complete ) ,
s ync,
)
} else {
notifier . showRestoreComplete ( time , errors . size , logFile . parent , logFile . name )
}
}
private fun writeErrorLog ( ) : File {
@ -118,7 +115,12 @@ class BackupRestorer(
return File ( " " )
}
private suspend fun performRestore ( uri : Uri , sync : Boolean ) {
private fun prepareState ( ) {
now = ZonedDateTime . now ( )
currentFetchWindow = fetchInterval . getWindow ( now )
}
private suspend fun restoreFromFile ( uri : Uri , sync : Boolean ) {
val backup = BackupUtil . decodeBackup ( context , uri )
restoreAmount = backup . backupManga . size + 3 // +3 for categories, app prefs, source prefs
@ -126,8 +128,6 @@ class BackupRestorer(
// Store source mapping for error messages
val backupMaps = backup . backupBrokenSources . map { BackupSource ( it . name , it . sourceId ) } + backup . backupSources
sourceMapping = backupMaps . associate { it . sourceId to it . name }
now = ZonedDateTime . now ( )
currentFetchWindow = fetchInterval . getWindow ( now )
coroutineScope {
ensureActive ( )
@ -139,8 +139,8 @@ class BackupRestorer(
ensureActive ( )
restoreSourcePreferences ( backup . backupSourcePreferences )
// Restore individual manga
backup . backupManga . forEach {
backup . backupManga . sortByNew ( )
. forEach {
ensureActive ( )
restoreManga ( it , backup . backupCategories , sync )
}
@ -149,6 +149,17 @@ class BackupRestorer(
}
}
private suspend fun List < BackupManga > . sortByNew ( ) : List < BackupManga > {
val urlsBySource = handler . awaitList { mangasQueries . getAllMangaSourceAndUrl ( ) }
. groupBy ( { it . source } , { it . url } )
return this
. sortedWith (
compareBy < BackupManga > { it . url in urlsBySource [ it . source ] . orEmpty ( ) }
. then ( compareByDescending { it . lastModifiedAt } ) ,
)
}
private suspend fun restoreCategories ( backupCategories : List < BackupCategory > ) {
if ( backupCategories . isNotEmpty ( ) ) {
val dbCategories = getCategories . await ( )
@ -170,75 +181,72 @@ class BackupRestorer(
}
restoreProgress += 1
showRestoreProgress (
notifier . showRestoreProgress (
context . stringResource ( MR . strings . categories ) ,
restoreProgress ,
restoreAmount ,
context . stringResource ( MR . strings . categories ) ,
context . stringResource ( MR . strings . restoring _backup ) ,
false ,
)
}
private suspend fun restoreManga ( backupManga : BackupManga , backupCategories : List < BackupCategory > , sync : Boolean ) {
val manga = backupManga . getMangaImpl ( )
val chapters = backupManga . getChaptersImpl ( )
val categories = backupManga . categories . map { it . toInt ( ) }
val history =
backupManga . brokenHistory . map { BackupHistory ( it . url , it . lastRead , it . readDuration ) } + backupManga . history
val tracks = backupManga . getTrackingImpl ( )
private suspend fun restoreManga (
backupManga : BackupManga ,
backupCategories : List < BackupCategory > ,
sync : Boolean ,
) {
try {
val dbManga = getMangaFromDatabase ( manga . url , manga . source )
val dbManga = findExistingManga ( backupManga )
val manga = backupManga . getMangaImpl ( )
val restoredManga = if ( dbManga == null ) {
// Manga not in database
restoreExistingManga ( manga , chapters , categories , history , tracks , backupCategories )
restoreNewManga ( manga )
} else {
// Manga in database
// Copy information from manga already in database
val updatedManga = restoreExistingManga ( manga , dbManga )
// Fetch rest of manga information
restoreNewManga ( updatedManga , chapters , categories , history , tracks , backupCategories )
restoreExistingManga ( manga , dbManga )
}
updateManga . awaitUpdateFetchInterval ( restoredManga , now , currentFetchWindow )
restoreMangaDetails (
manga = restoredManga ,
chapters = backupManga . getChaptersImpl ( ) ,
categories = backupManga . categories ,
backupCategories = backupCategories ,
history = backupManga . brokenHistory . map { BackupHistory ( it . url , it . lastRead , it . readDuration ) } +
backupManga . history ,
tracks = backupManga . getTrackingImpl ( ) ,
)
} catch ( e : Exception ) {
val sourceName = sourceMapping [ manga . source ] ?: manga . source . toString ( )
errors . add ( Date ( ) to " ${manga.title} [ $sourceName ]: ${e.message} " )
val sourceName = sourceMapping [ backupManga. source ] ?: backupM anga. source . toString ( )
errors . add ( Date ( ) to " ${ backupM anga.title} [ $sourceName ]: ${e.message} " )
}
restoreProgress += 1
if ( sync ) {
showRestoreProgress (
restoreProgress ,
restoreAmount ,
manga . title ,
context . stringResource ( MR . strings . syncing _library ) ,
)
} else {
showRestoreProgress (
restoreProgress ,
restoreAmount ,
manga . title ,
context . stringResource ( MR . strings . restoring _backup ) ,
)
notifier . showRestoreProgress ( backupManga . title , restoreProgress , restoreAmount , sync )
}
private suspend fun findExistingManga ( backupManga : BackupManga ) : Manga ? {
return getMangaByUrlAndSourceId . await ( backupManga . url , backupManga . source )
}
/ * *
* Returns manga
*
* @return [ Manga ] , null if not found
* /
private suspend fun getMangaFromDatabase ( url : String , source : Long ) : Mangas ? {
return handler . awaitOneOrNull { mangasQueries . getMangaByUrlAndSource ( url , source ) }
private suspend fun restoreExistingManga ( manga : Manga , dbManga : Manga ) : Manga {
return if ( manga . lastModifiedAt > dbManga . lastModifiedAt ) {
updateManga ( dbManga . copyFrom ( manga ) . copy ( id = dbManga . id ) )
} else {
updateManga ( manga . copyFrom ( dbManga ) . copy ( id = dbManga . id ) )
}
}
private suspend fun restoreExistingManga ( manga : Manga , dbManga : Mangas ) : Manga {
var updatedManga = manga . copy ( id = dbManga . _id )
updatedManga = updatedManga . copyFrom ( dbManga )
updateManga ( updatedManga )
return updatedManga
private fun Manga . copyFrom ( newer : Manga ) : Manga {
return this . copy (
favorite = this . favorite || newer . favorite ,
author = newer . author ,
artist = newer . artist ,
description = newer . description ,
genre = newer . genre ,
thumbnailUrl = newer . thumbnailUrl ,
status = newer . status ,
initialized = this . initialized || newer . initialized ,
)
}
private suspend fun updateManga ( manga : Manga ) : Long {
private suspend fun updateManga ( manga : Manga ) : Manga {
handler . await ( true ) {
mangasQueries . update (
source = manga . source ,
@ -263,28 +271,16 @@ class BackupRestorer(
updateStrategy = manga . updateStrategy . let ( UpdateStrategyColumnAdapter :: encode ) ,
)
}
return manga . id
return manga
}
/ * *
* Fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
* /
private suspend fun restoreExistingManga (
private suspend fun restoreNewManga (
manga : Manga ,
chapters : List < Chapter > ,
categories : List < Int > ,
history : List < BackupHistory > ,
tracks : List < Track > ,
backupCategories : List < BackupCategory > ,
) : Manga {
val fetchedManga = restoreNewManga ( manga )
restoreChapters ( fetchedManga , chapters )
restoreExtras ( fetchedManga , categories , history , tracks , backupCategories )
return fetchedManga
return manga . copy (
initialized = manga . description != null ,
id = insertManga ( manga ) ,
)
}
private suspend fun restoreChapters ( manga : Manga , chapters : List < Chapter > ) {
@ -318,13 +314,10 @@ class BackupRestorer(
}
val ( existingChapters , newChapters ) = processed . partition { it . id > 0 }
updateKnownChapters ( existingChapters )
insertChapters ( newChapters )
updateKnownChapters ( existingChapters )
}
/ * *
* Inserts list of chapters
* /
private suspend fun insertChapters ( chapters : List < Chapter > ) {
handler . await ( true ) {
chapters . forEach { chapter ->
@ -345,9 +338,6 @@ class BackupRestorer(
}
}
/ * *
* Updates a list of chapters with known database ids
* /
private suspend fun updateKnownChapters ( chapters : List < Chapter > ) {
handler . await ( true ) {
chapters . forEach { chapter ->
@ -369,19 +359,6 @@ class BackupRestorer(
}
}
/ * *
* Fetches manga information
*
* @param manga manga that needs updating
* @return Updated manga info .
* /
private suspend fun restoreNewManga ( manga : Manga ) : Manga {
return manga . copy (
initialized = manga . description != null ,
id = insertManga ( manga ) ,
)
}
/ * *
* Inserts manga and returns id
*
@ -414,29 +391,20 @@ class BackupRestorer(
}
}
private suspend fun restore New Manga(
backupM anga: Manga ,
private suspend fun restore MangaDetails (
m anga: Manga ,
chapters : List < Chapter > ,
categories : List < Int > ,
history : List < BackupHistory > ,
tracks : List < Track > ,
categories : List < Long > ,
backupCategories : List < BackupCategory > ,
) : Manga {
restoreChapters ( backupManga , chapters )
restoreExtras ( backupManga , categories , history , tracks , backupCategories )
return backupManga
}
private suspend fun restoreExtras (
manga : Manga ,
categories : List < Int > ,
history : List < BackupHistory > ,
tracks : List < Track > ,
backupCategories : List < BackupCategory > ,
) {
) : Manga {
restoreChapters ( manga , chapters )
restoreCategories ( manga , categories , backupCategories )
restoreHistory ( history )
restoreTracking ( manga , tracks )
updateManga . awaitUpdateFetchInterval ( manga , now , currentFetchWindow )
return manga
}
/ * *
@ -445,23 +413,24 @@ class BackupRestorer(
* @param manga the manga whose categories have to be restored .
* @param categories the categories to restore .
* /
private suspend fun restoreCategories ( manga : Manga , categories : List < Int > , backupCategories : List < BackupCategory > ) {
private suspend fun restoreCategories (
manga : Manga ,
categories : List < Long > ,
backupCategories : List < BackupCategory > ,
) {
val dbCategories = getCategories . await ( )
val mangaCategoriesToUpdate = mutableListOf < Pair < Long , Long > > ( )
val dbCategoriesByName = dbCategories . associateBy { it . name }
val backupCategoriesByOrder = backupCategories . associateBy { it . order }
categories . forEach { backupCategoryOrder ->
backupCategories . firstOrNull {
it . order == backupCategoryOrder . toLong ( )
} ?. let { backupCategory ->
dbCategories . firstOrNull { dbCategory ->
dbCategory . name == backupCategory . name
} ?. let { dbCategory ->
mangaCategoriesToUpdate . add ( Pair ( manga . id , dbCategory . id ) )
val mangaCategoriesToUpdate = categories . mapNotNull { backupCategoryOrder ->
backupCategoriesByOrder [ backupCategoryOrder ] ?. let { backupCategory ->
dbCategoriesByName [ backupCategory . name ] ?. let { dbCategory ->
Pair ( manga . id , dbCategory . id )
}
}
}
// Update database
if ( mangaCategoriesToUpdate . isNotEmpty ( ) ) {
handler . await ( true ) {
mangas _categoriesQueries . deleteMangaCategoryByMangaId ( manga . id )
@ -472,11 +441,6 @@ class BackupRestorer(
}
}
/ * *
* Restore history from Json
*
* @param history list containing history to be restored
* /
private suspend fun restoreHistory ( history : List < BackupHistory > ) {
// List containing history to be updated
val toUpdate = mutableListOf < HistoryUpdate > ( )
@ -496,7 +460,7 @@ class BackupRestorer(
) ,
)
} else {
// If not in database create
// If not in database , create
handler
. awaitOneOrNull { chaptersQueries . getChapterByUrl ( url ) }
?. let {
@ -521,12 +485,6 @@ class BackupRestorer(
}
}
/ * *
* Restores the sync of a manga .
*
* @param manga the manga whose sync have to be restored .
* @param tracks the track list to restore .
* /
private suspend fun restoreTracking ( manga : Manga , tracks : List < Track > ) {
// Get tracks from database
val dbTracks = handler . awaitList { manga _syncQueries . getTracksByMangaId ( manga . id ) }
@ -611,11 +569,11 @@ class BackupRestorer(
BackupCreateJob . setupTask ( context )
restoreProgress += 1
showRestoreProgress (
notifier . showRestoreProgress (
context . stringResource ( MR . strings . app _settings ) ,
restoreProgress ,
restoreAmount ,
context . stringResource ( MR . strings . app _settings ) ,
context . stringResource ( MR . strings . restoring _backup ) ,
false ,
)
}
@ -626,11 +584,11 @@ class BackupRestorer(
}
restoreProgress += 1
showRestoreProgress (
notifier . showRestoreProgress (
context . stringResource ( MR . strings . source _settings ) ,
restoreProgress ,
restoreAmount ,
context . stringResource ( MR . strings . source _settings ) ,
context . stringResource ( MR . strings . restoring _backup ) ,
false ,
)
}
@ -674,8 +632,4 @@ class BackupRestorer(
}
}
}
private fun showRestoreProgress ( progress : Int , amount : Int , title : String , contentTitle : String ) {
notifier . showRestoreProgress ( title , contentTitle , progress , amount )
}
}