package exh import android.content.Context import com.elvishew.xlog.XLog import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.Hitomi import eu.kanade.tachiyomi.source.online.all.NHentai import exh.merged.sql.models.MergedMangaReference import exh.source.BlacklistedSources import uy.kohesive.injekt.injectLazy import java.io.File import java.net.URI import java.net.URISyntaxException object EXHMigrations { private val db: DatabaseHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() private val gson: Gson by injectLazy() private val logger = XLog.tag("EXHMigrations") /** * Performs a migration when the application is updated. * * @param preferences Preferences of the application. * @return true if a migration is performed, false otherwise. */ fun upgrade(preferences: PreferencesHelper): Boolean { val context = preferences.context val oldVersion = preferences.eh_lastVersionCode().get() try { if (oldVersion < BuildConfig.VERSION_CODE) { // Fresh install if (oldVersion == 0) { // Set up default background tasks UpdaterJob.setupTask(context) ExtensionUpdateJob.setupTask(context) LibraryUpdateJob.setupTask(context) return false } if (oldVersion < 4) { db.inTransaction { db.lowLevel().executeSQL( RawQuery.builder() .query( """ UPDATE ${MangaTable.TABLE} SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID WHERE ${MangaTable.COL_SOURCE} = 6912 """.trimIndent() ) .affectsTables(MangaTable.TABLE) .build() ) // Migrate BHrowse URLs val hBrowseManga = db.db.get() .listOfObjects(Manga::class.java) .withQuery( Query.builder() .table(MangaTable.TABLE) .where("${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID") .build() ) .prepare() .executeAsBlocking() hBrowseManga.forEach { it.url = it.url + "/c00001/" } db.db.put() .objects(hBrowseManga) // Extremely slow without the resolver :/ .withPutResolver(MangaUrlPutResolver()) .prepare() .executeAsBlocking() } } if (oldVersion < 5) { db.inTransaction { // Migrate Tsumino source IDs db.lowLevel().executeSQL( RawQuery.builder() .query( """ UPDATE ${MangaTable.TABLE} SET ${MangaTable.COL_SOURCE} = ${Hitomi.otherId} WHERE ${MangaTable.COL_SOURCE} = 6910 """.trimIndent() ) .affectsTables(MangaTable.TABLE) .build() ) } } if (oldVersion < 6) { db.inTransaction { db.lowLevel().executeSQL( RawQuery.builder() .query( """ UPDATE ${MangaTable.TABLE} SET ${MangaTable.COL_SOURCE} = $PERV_EDEN_EN_SOURCE_ID WHERE ${MangaTable.COL_SOURCE} = 6905 """.trimIndent() ) .affectsTables(MangaTable.TABLE) .build() ) db.lowLevel().executeSQL( RawQuery.builder() .query( """ UPDATE ${MangaTable.TABLE} SET ${MangaTable.COL_SOURCE} = $PERV_EDEN_IT_SOURCE_ID WHERE ${MangaTable.COL_SOURCE} = 6906 """.trimIndent() ) .affectsTables(MangaTable.TABLE) .build() ) db.lowLevel().executeSQL( RawQuery.builder() .query( """ UPDATE ${MangaTable.TABLE} SET ${MangaTable.COL_SOURCE} = ${NHentai.otherId} WHERE ${MangaTable.COL_SOURCE} = 6907 """.trimIndent() ) .affectsTables(MangaTable.TABLE) .build() ) } } if (oldVersion < 7) { db.inTransaction { val mergedMangas = db.db.get() .listOfObjects(Manga::class.java) .withQuery( Query.builder() .table(MangaTable.TABLE) .where("${MangaTable.COL_SOURCE} = $MERGED_SOURCE_ID") .build() ) .prepare() .executeAsBlocking() if (mergedMangas.isNotEmpty()) { val mangaConfigs = mergedMangas.mapNotNull { mergedManga -> readMangaConfig(mergedManga, gson)?.let { mergedManga to it } } if (mangaConfigs.isNotEmpty()) { val mangaToUpdate = mutableListOf() val mergedMangaReferences = mutableListOf() mangaConfigs.onEach { mergedManga -> mergedManga.second.children.firstOrNull()?.url?.let { if (db.getManga(it, MERGED_SOURCE_ID).executeAsBlocking() != null) return@onEach mergedManga.first.url = it } mangaToUpdate += mergedManga.first mergedMangaReferences += MergedMangaReference( id = null, isInfoManga = false, getChapterUpdates = false, chapterSortMode = 0, chapterPriority = 0, downloadChapters = false, mergeId = mergedManga.first.id!!, mergeUrl = mergedManga.first.url, mangaId = mergedManga.first.id!!, mangaUrl = mergedManga.first.url, mangaSourceId = MERGED_SOURCE_ID ) mergedManga.second.children.distinct().forEachIndexed { index, mangaSource -> val load = mangaSource.load(db, sourceManager) ?: return@forEachIndexed mergedMangaReferences += MergedMangaReference( id = null, isInfoManga = index == 0, getChapterUpdates = true, chapterSortMode = 0, chapterPriority = 0, downloadChapters = true, mergeId = mergedManga.first.id!!, mergeUrl = mergedManga.first.url, mangaId = load.manga.id!!, mangaUrl = load.manga.url, mangaSourceId = load.source.id ) } } db.db.put() .objects(mangaToUpdate) // Extremely slow without the resolver :/ .withPutResolver(MangaUrlPutResolver()) .prepare() .executeAsBlocking() db.insertMergedMangas(mergedMangaReferences).executeAsBlocking() val loadedMangaList = mangaConfigs.map { it.second.children }.flatten().mapNotNull { it.load(db, sourceManager) }.distinct() val chapters = db.db.get() .listOfObjects(Chapter::class.java) .withQuery( Query.builder() .table(ChapterTable.TABLE) .where("${ChapterTable.COL_MANGA_ID} IN (${mergedMangas.filter { it.id != null }.joinToString { it.id.toString() }})") .build() ) .prepare() .executeAsBlocking() val mergedMangaChapters = db.db.get() .listOfObjects(Chapter::class.java) .withQuery( Query.builder() .table(ChapterTable.TABLE) .where("${ChapterTable.COL_MANGA_ID} IN (${loadedMangaList.filter { it.manga.id != null }.joinToString { it.manga.id.toString() }})") .build() ) .prepare() .executeAsBlocking() val mergedMangaChaptersMatched = mergedMangaChapters.mapNotNull { chapter -> loadedMangaList.firstOrNull { it.manga.id == chapter.id }?.let { it to chapter } } val parsedChapters = chapters.filter { it.read || it.last_page_read != 0 }.mapNotNull { chapter -> readUrlConfig(chapter.url, gson)?.let { chapter to it } } val chaptersToUpdate = mutableListOf() parsedChapters.forEach { parsedChapter -> mergedMangaChaptersMatched.firstOrNull { it.second.url == parsedChapter.second.url && it.first.source.id == parsedChapter.second.source && it.first.manga.url == parsedChapter.second.mangaUrl }?.let { chaptersToUpdate += it.second.apply { read = parsedChapter.first.read last_page_read = parsedChapter.first.last_page_read } } } db.deleteChapters(mergedMangaChapters).executeAsBlocking() db.updateChaptersProgress(chaptersToUpdate).executeAsBlocking() } } } } // if (oldVersion < 1) { } (1 is current release version) // do stuff here when releasing changed crap // TODO BE CAREFUL TO NOT FUCK UP MergedSources IF CHANGING URLs preferences.eh_lastVersionCode().set(BuildConfig.VERSION_CODE) return true } } catch (e: Exception) { logger.e("Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e) } return false } fun migrateBackupEntry(manga: MangaImpl): MangaImpl { if (manga.source == 6905L) { manga.source = PERV_EDEN_EN_SOURCE_ID } if (manga.source == 6906L) { manga.source = PERV_EDEN_IT_SOURCE_ID } if (manga.source == 6907L) { // Migrate the old source to the delegated one manga.source = NHentai.otherId // Migrate nhentai URLs manga.url = getUrlWithoutDomain(manga.url) } // Migrate HentaiCafe source IDs if (manga.source == 6908L) { manga.source = HENTAI_CAFE_SOURCE_ID } // Migrate Tsumino source IDs if (manga.source == 6909L) { manga.source = TSUMINO_SOURCE_ID } if (manga.source == 6910L) { manga.source = Hitomi.otherId } if (manga.source == 6912L) { manga.source = HBROWSE_SOURCE_ID } // Allow importing of EHentai extension backups if (manga.source in BlacklistedSources.EHENTAI_EXT_SOURCES) { manga.source = EH_SOURCE_ID } return manga } private fun backupDatabase(context: Context, oldMigrationVersion: Int) { val backupLocation = File(File(context.filesDir, "exh_db_bck"), "$oldMigrationVersion.bck.db") if (backupLocation.exists()) return // Do not backup same version twice val dbLocation = context.getDatabasePath(db.lowLevel().sqliteOpenHelper().databaseName) try { dbLocation.copyTo(backupLocation, overwrite = true) } catch (t: Throwable) { XLog.w("Failed to backup database!") } } private fun getUrlWithoutDomain(orig: String): String { return try { val uri = URI(orig) var out = uri.path if (uri.query != null) { out += "?" + uri.query } if (uri.fragment != null) { out += "#" + uri.fragment } out } catch (e: URISyntaxException) { orig } } private data class UrlConfig( @SerializedName("s") val source: Long, @SerializedName("u") val url: String, @SerializedName("m") val mangaUrl: String ) private data class MangaConfig( @SerializedName("c") val children: List ) { companion object { fun readFromUrl(gson: Gson, url: String): MangaConfig? { return try { gson.fromJson(url) } catch (e: Exception) { null } } } } private fun readMangaConfig(manga: SManga, gson: Gson): MangaConfig? { return MangaConfig.readFromUrl(gson, manga.url) } private data class MangaSource( @SerializedName("s") val source: Long, @SerializedName("u") val url: String ) { fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? { val manga = db.getManga(url, source).executeAsBlocking() ?: return null val source = sourceManager.getOrStub(source) return LoadedMangaSource(source, manga) } } private fun readUrlConfig(url: String, gson: Gson): UrlConfig? { return try { gson.fromJson(url) } catch (e: Exception) { null } } private data class LoadedMangaSource(val source: Source, val manga: Manga) } data class BackupEntry( val manga: Manga, val chapters: List, val categories: List, val history: List, val tracks: List )