@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
import android.content.Context
import android.net.Uri
import android.net.Uri
import com.hippo.unifile.UniFile
import com.hippo.unifile.UniFile
import data.Manga_sync
import data.Mangas
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
@ -15,17 +18,16 @@ import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
import eu.kanade.tachiyomi.data.backup.full.models.backupCategoryMapper
import eu.kanade.tachiyomi.data.backup.full.models.backupChapterMapper
import eu.kanade.tachiyomi.data.backup.full.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.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoBuf
@ -34,6 +36,7 @@ import okio.buffer
import okio.gzip
import okio.gzip
import okio.sink
import okio.sink
import java.io.FileOutputStream
import java.io.FileOutputStream
import java.util.Date
import kotlin.math.max
import kotlin.math.max
class FullBackupManager ( context : Context ) : AbstractBackupManager ( context ) {
class FullBackupManager ( context : Context ) : AbstractBackupManager ( context ) {
@ -46,20 +49,18 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @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
* /
* /
override fun createBackup ( uri : Uri , flags : Int , isAutoBackup : Boolean ) : String {
override suspend fun createBackup ( uri : Uri , flags : Int , isAutoBackup : Boolean ) : String {
// Create root object
// Create root object
var backup : Backup ? = null
var backup : Backup ? = null
db . inTransaction {
val databaseManga = getFavoriteManga ( )
val databaseManga = getFavoriteManga ( )
backup = Backup (
backup = Backup (
backupManga ( databaseManga , flags ) ,
backupManga ( databaseManga , flags ) ,
backupCategories ( flags ) ,
backupCategories ( flags ) ,
emptyList ( ) ,
emptyList ( ) ,
backupExtensionInfo ( databaseManga ) ,
backupExtensionInfo ( databaseManga ) ,
)
)
}
var file : UniFile ? = null
var file : UniFile ? = null
try {
try {
@ -112,13 +113,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
}
}
}
private fun backupManga ( mangas : List < Manga > , flags : Int ) : List < BackupManga > {
private suspend fun backupManga ( mangas : List < Manga s > , flags : Int ) : List < BackupManga > {
return mangas . map {
return mangas . map {
backupMangaObject ( it , flags )
backupMangaObject ( it , flags )
}
}
}
}
private fun backupExtensionInfo ( mangas : List < Manga > ) : List < BackupSource > {
private fun backupExtensionInfo ( mangas : List < Manga s > ) : List < BackupSource > {
return mangas
return mangas
. asSequence ( )
. asSequence ( )
. map { it . source }
. map { it . source }
@ -133,12 +134,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*
*
* @return list of [ BackupCategory ] to be backed up
* @return list of [ BackupCategory ] to be backed up
* /
* /
private 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 ) {
db . getCategories ( )
handler . awaitList { categoriesQueries . getCategories ( backupCategoryMapper ) }
. executeAsBlocking ( )
. map { BackupCategory . copyFrom ( it ) }
} else {
} else {
emptyList ( )
emptyList ( )
}
}
@ -151,43 +150,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param options options for the backup
* @param options options for the backup
* @return [ BackupManga ] containing manga in a serializable form
* @return [ BackupManga ] containing manga in a serializable form
* /
* /
private fun backupMangaObject ( manga : Manga , options : Int ) : BackupManga {
private suspend fun backupMangaObject ( manga : Manga s , options : Int ) : BackupManga {
// Entry for this manga
// Entry for this manga
val mangaObject = BackupManga . copyFrom ( manga )
val mangaObject = BackupManga . copyFrom ( manga )
// Check if user wants chapter information in backup
// Check if user wants chapter information in backup
if ( options and BACKUP _CHAPTER _MASK == BACKUP _CHAPTER ) {
if ( options and BACKUP _CHAPTER _MASK == BACKUP _CHAPTER ) {
// Backup all the chapters
// Backup all the chapters
val chapters = db. getChapters ( manga ) . executeAsBlocking ( )
val chapters = handler. awaitList { chaptersQueries . getChaptersByMangaId ( manga . _id , backupChapterMapper ) }
if ( chapters . isNotEmpty ( ) ) {
if ( chapters . isNotEmpty ( ) ) {
mangaObject . chapters = chapters . map { BackupChapter . copyFrom ( it ) }
mangaObject . chapters = chapters
}
}
}
}
// 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 = db. getCategoriesForManga ( manga ) . executeAsBlocking ( )
val categoriesForManga = handler. awaitList { categoriesQueries . getCategoriesByMangaId ( manga . _id ) }
if ( categoriesForManga . isNotEmpty ( ) ) {
if ( categoriesForManga . isNotEmpty ( ) ) {
mangaObject . categories = categoriesForManga . map NotNull { it . order }
mangaObject . categories = categoriesForManga . map { it . order }
}
}
}
}
// Check if user wants track information in backup
// Check if user wants track information in backup
if ( options and BACKUP _TRACK _MASK == BACKUP _TRACK ) {
if ( options and BACKUP _TRACK _MASK == BACKUP _TRACK ) {
val tracks = db. getTracks ( manga . id ) . executeAsBlocking ( )
val tracks = handler. awaitList { manga _syncQueries . getTracksByMangaId ( manga . _id , backupTrackMapper ) }
if ( tracks . isNotEmpty ( ) ) {
if ( tracks . isNotEmpty ( ) ) {
mangaObject . tracking = tracks . map { BackupTracking . copyFrom ( it ) }
mangaObject . tracking = tracks
}
}
}
}
// Check if user wants history information in backup
// Check if user wants history information in backup
if ( options and BACKUP _HISTORY _MASK == BACKUP _HISTORY ) {
if ( options and BACKUP _HISTORY _MASK == BACKUP _HISTORY ) {
val history ForManga = db . getHistoryByMangaId ( manga . id !! ) . executeAsBlocking ( )
val history ByMangaId = handler . awaitList ( true ) { historyQueries . getHistoryByMangaId ( manga . _id ) }
if ( history ForManga . isNotEmpty ( ) ) {
if ( history ByMangaId . isNotEmpty ( ) ) {
val history = history ForManga. mapNotNull { history ->
val history = history ByMangaId. map { history ->
val url = db . getChapter ( history . chapter _id ) . executeAsBlocking ( ) ?. url
val chapter = handler . awaitOne { chaptersQueries . getChapterById ( history . chapter _id ) }
url?. let { BackupHistory( url, history . last _read ) }
BackupHistory( chapter. url, history . last _read ?. time ?: 0L )
}
}
if ( history . isNotEmpty ( ) ) {
if ( history . isNotEmpty ( ) ) {
mangaObject . history = history
mangaObject . history = history
@ -198,10 +197,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
return mangaObject
return mangaObject
}
}
fun restoreMangaNoFetch ( manga : Manga , dbManga : Manga ) {
suspend fun restoreMangaNoFetch ( manga : Manga , dbManga : Manga s ) {
manga . id = dbManga . id
manga . id = dbManga . _ id
manga . copyFrom ( dbManga )
manga . copyFrom ( dbManga )
insert Manga( manga )
update Manga( manga )
}
}
/ * *
/ * *
@ -210,7 +209,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga manga that needs updating
* @param manga manga that needs updating
* @return Updated manga info .
* @return Updated manga info .
* /
* /
fun restoreManga ( manga : Manga ) : Manga {
suspend fun restoreManga ( 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 )
@ -222,32 +221,36 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*
*
* @param backupCategories list containing categories
* @param backupCategories list containing categories
* /
* /
internal 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 = db. getCategories ( ) . executeAsBlocking ( )
val dbCategories = handler. awaitList { categoriesQueries . getCategories ( ) }
// Iterate over them
// Iterate over them
backupCategories . map { it . getCategoryImpl ( ) } . forEach { category ->
backupCategories
// Used to know if the category is already in the db
. map { it . getCategoryImpl ( ) }
var found = false
. forEach { category ->
for ( dbCategory in dbCategories ) {
// Used to know if the category is already in the db
// If the category is already in the db, assign the id to the file's category
var found = false
// and do nothing
for ( dbCategory in dbCategories ) {
if ( category . name == dbCategory . name ) {
// If the category is already in the db, assign the id to the file's category
category . id = dbCategory . id
// and do nothing
found = true
if ( category . name == dbCategory . name ) {
break
category . id = dbCategory . id . toInt ( )
found = true
break
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if ( ! found ) {
// Let the db assign the id
category . id = null
category . id = handler . awaitOne {
categoriesQueries . insert ( category . name , category . order . toLong ( ) , category . flags . toLong ( ) )
categoriesQueries . selectLastInsertedRowId ( )
} . toInt ( )
}
}
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if ( ! found ) {
// Let the db assign the id
category . id = null
val result = db . insertCategory ( category ) . executeAsBlocking ( )
category . id = result . insertedId ( ) ?. toInt ( )
}
}
}
}
/ * *
/ * *
@ -256,25 +259,30 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga the manga whose categories have to be restored .
* @param manga the manga whose categories have to be restored .
* @param categories the categories to restore .
* @param categories the categories to restore .
* /
* /
internal fun restoreCategoriesForManga ( manga : Manga , categories : List < Int > , backupCategories : List < BackupCategory > ) {
internal suspend fun restoreCategoriesForManga ( manga : Manga , categories : List < Int > , backupCategories : List < BackupCategory > ) {
val dbCategories = db . getCategories ( ) . executeAsBlocking ( )
val dbCategories = handler . awaitList { categoriesQueries . getCategories ( ) }
val mangaCategoriesToUpdate = ArrayList < MangaCategory > ( categories . size )
val mangaCategoriesToUpdate = mutableListOf < Pair < Long , Long > > ( )
categories . forEach { backupCategoryOrder ->
categories . forEach { backupCategoryOrder ->
backupCategories . firstOrNull {
backupCategories . firstOrNull {
it . order == backupCategoryOrder
it . order == backupCategoryOrder . toLong ( )
} ?. let { backupCategory ->
} ?. let { backupCategory ->
dbCategories . firstOrNull { dbCategory ->
dbCategories . firstOrNull { dbCategory ->
dbCategory . name == backupCategory . name
dbCategory . name == backupCategory . name
} ?. let { dbCategory ->
} ?. let { dbCategory ->
mangaCategoriesToUpdate += MangaCategory . create ( manga , dbCategory )
mangaCategoriesToUpdate . add ( Pair ( manga . id !! , dbCategory . id ) )
}
}
}
}
}
}
// Update database
// Update database
if ( mangaCategoriesToUpdate . isNotEmpty ( ) ) {
if ( mangaCategoriesToUpdate . isNotEmpty ( ) ) {
db . deleteOldMangasCategories ( listOf ( manga ) ) . executeAsBlocking ( )
handler . await ( true ) {
db . insertMangasCategories ( mangaCategoriesToUpdate ) . executeAsBlocking ( )
mangas _categoriesQueries . deleteMangaCategoryByMangaId ( manga . id !! )
mangaCategoriesToUpdate . forEach { ( mangaId , categoryId ) ->
mangas _categoriesQueries . insert ( mangaId , categoryId )
}
}
}
}
}
}
@ -283,28 +291,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*
*
* @param history list containing history to be restored
* @param history list containing history to be restored
* /
* /
internal fun restoreHistoryForManga ( history : List < BackupHistory > ) {
internal suspend fun restoreHistoryForManga ( history : List < BackupHistory > ) {
// List containing history to be updated
// List containing history to be updated
val historyToBeUpdated = ArrayList < History > ( history . size )
val toUpdate = mutableListOf < HistoryUpdate > ( )
for ( ( url , lastRead ) in history ) {
for ( ( url , lastRead ) in history ) {
va l dbHistory = db . getHistoryByChapterUrl ( url ) . executeAsBlocking ( )
va r dbHistory = handler . awaitOneOrNull { historyQueries . getHistoryByChapterUrl ( url ) }
// Check if history already in database and update
// Check if history already in database and update
if ( dbHistory != null ) {
if ( dbHistory != null ) {
dbHistory . apply {
dbHistory = dbHistory . copy ( last _read = Date ( max ( lastRead , dbHistory . last _read ?. time ?: 0L ) ) )
last _read = max ( lastRead , dbHistory . last _read )
toUpdate . add (
}
HistoryUpdate (
historyToBeUpdated . add ( dbHistory )
chapterId = dbHistory . chapter _id ,
readAt = dbHistory . last _read !! ,
sessionReadDuration = dbHistory . time _read ,
) ,
)
} else {
} else {
// If not in database create
// If not in database create
db . getChapter ( url ) . executeAsBlocking ( ) ?. let {
handler
val historyToAdd = History . create ( it ) . apply {
. awaitOneOrNull { chaptersQueries . getChapterByUrl ( url ) }
last _read = lastRead
?. let {
HistoryUpdate (
chapterId = it . _id ,
readAt = Date ( lastRead ) ,
sessionReadDuration = 0 ,
)
}
}
historyToBeUpdated . add ( historyToAdd )
}
}
}
}
}
db . upsertHistoryLastRead ( historyToBeUpdated ) . executeAsBlocking ( )
handler . await ( true ) {
toUpdate . forEach { payload ->
historyQueries . upsert (
payload . chapterId ,
payload . readAt ,
payload . sessionReadDuration ,
)
}
}
}
}
/ * *
/ * *
@ -313,56 +336,97 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga the manga whose sync have to be restored .
* @param manga the manga whose sync have to be restored .
* @param tracks the track list to restore .
* @param tracks the track list to restore .
* /
* /
internal fun restoreTrackForManga ( manga : Manga , tracks : List < Track > ) {
internal suspend fun restoreTrackForManga ( manga : Manga , tracks : List < Track > ) {
// Fix foreign keys with the current manga id
// Fix foreign keys with the current manga id
tracks . map { it . manga _id = manga . id !! }
tracks . map { it . manga _id = manga . id !! }
// Get tracks from database
// Get tracks from database
val dbTracks = db . getTracks ( manga . id ) . executeAsBlocking ( )
val trackToUpdate = mutableListOf < Track > ( )
val dbTracks = handler . awaitList { manga _syncQueries . getTracksByMangaId ( manga . id !! ) }
val toUpdate = mutableListOf < Manga _sync > ( )
val toInsert = mutableListOf < Track > ( )
tracks . forEach { track ->
tracks . forEach { track ->
var isInDatabase = false
var isInDatabase = false
for ( dbTrack in dbTracks ) {
for ( dbTrack in dbTracks ) {
if ( track . sync _id == dbTrack . sync _id ) {
if ( track . sync _id == dbTrack . sync _id .toInt ( ) ) {
// The sync is already in the db, only update its fields
// The sync is already in the db, only update its fields
if ( track . media _id != dbTrack . media _id ) {
var temp = dbTrack
dbTrack . media _id = track . media _id
if ( track . media _id != dbTrack . remote _id ) {
temp = temp . copy ( remote _id = track . media _id )
}
}
if ( track . library _id != dbTrack . library _id ) {
if ( track . library _id != dbTrack . library _id ) {
dbTrack. library _id = track . library _id
temp = temp . copy ( library _id = track . library _id )
}
}
dbTrack. last _chapter _read = max ( dbTrack . last _chapter _read , track . last _chapter _read )
temp = temp . copy ( last _chapter _read = max ( dbTrack . last _chapter _read , track . last _chapter _read . toDouble ( ) ) )
isInDatabase = true
isInDatabase = true
t rackToUpdate. add ( dbTrack )
t oUpdate. add ( temp )
break
break
}
}
}
}
if ( !is InDatabase ) {
if ( !is InDatabase ) {
// Insert new sync. Let the db assign the id
// Insert new sync. Let the db assign the id
track . id = null
track . id = null
t rackToUpda te . add ( track )
t oInse rt. add ( track )
}
}
}
}
// Update database
// Update database
if ( trackToUpdate . isNotEmpty ( ) ) {
if ( toUpdate . isNotEmpty ( ) ) {
db . insertTracks ( trackToUpdate ) . executeAsBlocking ( )
handler . await ( true ) {
toUpdate . forEach { track ->
manga _syncQueries . update (
track . manga _id ,
track . sync _id ,
track . remote _id ,
track . library _id ,
track . title ,
track . last _chapter _read ,
track . total _chapters ,
track . status ,
track . score . toDouble ( ) ,
track . remote _url ,
track . start _date ,
track . finish _date ,
track . _id ,
)
}
}
}
if ( toInsert . isNotEmpty ( ) ) {
handler . await ( true ) {
toInsert . forEach { track ->
manga _syncQueries . insert (
track . manga _id ,
track . sync _id . toLong ( ) ,
track . media _id ,
track . library _id ,
track . title ,
track . last _chapter _read . toDouble ( ) ,
track . total _chapters . toLong ( ) ,
track . status . toLong ( ) ,
track . score ,
track . tracking _url ,
track . started _reading _date ,
track . finished _reading _date ,
)
}
}
}
}
}
}
internal fun restoreChaptersForManga ( manga : Manga , chapters : List < Chapter > ) {
internal suspend fun restoreChaptersForManga ( manga : Manga , chapters : List < Chapter > ) {
val dbChapters = db . getChapters ( manga ) . executeAsBlocking ( )
val dbChapters = handler. awaitList { chaptersQueries . getChaptersByMangaId ( manga . id !! ) }
chapters . forEach { chapter ->
chapters . forEach { chapter ->
val dbChapter = dbChapters . find { it . url == chapter . url }
val dbChapter = dbChapters . find { it . url == chapter . url }
if ( dbChapter != null ) {
if ( dbChapter != null ) {
chapter . id = dbChapter . id
chapter . id = dbChapter . _ id
chapter . copyFrom ( dbChapter )
chapter . copyFrom ( dbChapter )
if ( dbChapter . read && ! chapter . read ) {
if ( dbChapter . read && ! chapter . read ) {
chapter . read = dbChapter . read
chapter . read = dbChapter . read
chapter . last _page _read = dbChapter . last _page _read
chapter . last _page _read = dbChapter . last _page _read . toInt ( )
} else if ( chapter . last _page _read == 0 && dbChapter . last _page _read != 0 ) {
} else if ( chapter . last _page _read == 0 && dbChapter . last _page _read != 0 L ) {
chapter . last _page _read = dbChapter . last _page _read
chapter . last _page _read = dbChapter . last _page _read . toInt ( )
}
}
if ( ! chapter . bookmark && dbChapter . bookmark ) {
if ( ! chapter . bookmark && dbChapter . bookmark ) {
chapter . bookmark = dbChapter . bookmark
chapter . bookmark = dbChapter . bookmark