Minor cleanup

- Remove some unused StorIO queries
- Clean up tall image splitting a bit (no need for creating an unscaled scaled bitmap copy, or tracking coordinates)
- Clean up library updater a bit (still needs a lot of work though)
This commit is contained in:
arkon 2022-05-03 22:23:28 -04:00
parent aa11902aa1
commit a9e629aea6
9 changed files with 72 additions and 126 deletions

View File

@ -72,12 +72,8 @@ interface ChapterQueries : DbProvider {
) )
.prepare() .prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare() fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare() fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put() fun updateChaptersBackup(chapters: List<Chapter>) = db.put()

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
@ -50,14 +49,4 @@ interface HistoryQueries : DbProvider {
.objects(historyList) .objects(historyList)
.withPutResolver(HistoryUpsertResolver()) .withPutResolver(HistoryUpsertResolver())
.prepare() .prepare()
fun deleteHistoryNoLastRead() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build(),
)
.prepare()
} }

View File

@ -131,10 +131,6 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaCoverLastModifiedPutResolver()) .withPutResolver(MangaCoverLastModifiedPutResolver())
.prepare() .prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete() fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
@ -145,14 +141,6 @@ interface MangaQueries : DbProvider {
) )
.prepare() .prepare()
fun deleteMangas() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.build(),
)
.prepare()
fun getLastReadManga() = db.get() fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery( .withQuery(

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.graphics.BitmapCompat
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
@ -44,7 +43,6 @@ import uy.kohesive.injekt.injectLazy
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@ -354,7 +352,7 @@ class Downloader(
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { page -> .doOnNext { page ->
if (preferences.splitTallImages().get()) { if (preferences.splitTallImages().get()) {
splitTallImage(page, download, tmpDir) splitTallImage(page, tmpDir)
} }
notifier.onProgressChange(download) notifier.onProgressChange(download)
} }
@ -560,53 +558,41 @@ class Downloader(
/** /**
* Splits tall images to improve performance of reader * Splits tall images to improve performance of reader
*/ */
private fun splitTallImage(page: Page, download: Download, tmpDir: UniFile) { private fun splitTallImage(page: Page, tmpDir: UniFile) {
val filename = String.format("%03d", page.number) val filename = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith("$filename.") }
if (imageFile == null) { ?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
notifier.onError("Error: imageFile was not found", download.chapter.name, download.manga.title)
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
return return
} }
if (!isAnimatedAndSupported(imageFile.openInputStream()) && isTallImage(imageFile.openInputStream())) {
// Getting the scaled bitmap of the source image
val bitmap = BitmapFactory.decodeFile(imageFile.filePath) val bitmap = BitmapFactory.decodeFile(imageFile.filePath)
val scaledBitmap: Bitmap = val splitsCount = bitmap.height / context.resources.displayMetrics.heightPixels + 1
BitmapCompat.createScaledBitmap(bitmap, bitmap.width, bitmap.height, null, true) val heightPerSplit = bitmap.height / splitsCount
val splitsCount: Int = bitmap.height / context.resources.displayMetrics.heightPixels + 1
val splitHeight = bitmap.height / splitsCount
// xCoord and yCoord are the pixel positions of the image splits
val xCoord = 0
var yCoord = 0
try { try {
for (i in 0 until splitsCount) { (0..splitsCount).forEach { split ->
val splitPath = imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(i + 1)}.jpg" val splitPath = imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(split + 1)}.jpg"
// Compress the bitmap and save in jpg format FileOutputStream(splitPath).use { stream ->
val stream: OutputStream = FileOutputStream(splitPath)
stream.use {
Bitmap.createBitmap( Bitmap.createBitmap(
scaledBitmap, bitmap,
xCoord, 0,
yCoord, split * heightPerSplit,
bitmap.width, bitmap.width,
splitHeight, heightPerSplit,
).compress(Bitmap.CompressFormat.JPEG, 100, stream) ).compress(Bitmap.CompressFormat.JPEG, 100, stream)
} }
yCoord += splitHeight
} }
imageFile.delete() imageFile.delete()
} catch (e: Exception) { } catch (e: Exception) {
// Image splits were not successfully saved so delete them and keep the original image // Image splits were not successfully saved so delete them and keep the original image
for (i in 0 until splitsCount) { (0..splitsCount)
val splitPath = imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(i + 1)}.jpg" .map { imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(it + 1)}.jpg" }
File(splitPath).delete() .forEach { File(it).delete() }
}
throw e throw e
} }
} }
}
/** /**
* Completes a download. This method is called in the main thread. * Completes a download. This method is called in the main thread.

View File

@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
@ -80,7 +81,7 @@ class LibraryUpdateService(
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: LibraryUpdateNotifier private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope private var ioScope: CoroutineScope? = null
private var mangaToUpdate: List<LibraryManga> = mutableListOf() private var mangaToUpdate: List<LibraryManga> = mutableListOf()
private var updateJob: Job? = null private var updateJob: Job? = null
@ -90,10 +91,8 @@ class LibraryUpdateService(
*/ */
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
COVERS, // Manga covers COVERS, // Manga covers
TRACKING, // Tracking metadata
TRACKING // Tracking metadata
} }
companion object { companion object {
@ -161,7 +160,6 @@ class LibraryUpdateService(
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = LibraryUpdateNotifier(this) notifier = LibraryUpdateNotifier(this)
wakeLock = acquireWakeLock(javaClass.name) wakeLock = acquireWakeLock(javaClass.name)
@ -174,8 +172,6 @@ class LibraryUpdateService(
*/ */
override fun onDestroy() { override fun onDestroy() {
updateJob?.cancel() updateJob?.cancel()
// Despite what Android Studio
// states this can be null
ioScope?.cancel() ioScope?.cancel()
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
@ -189,9 +185,7 @@ class LibraryUpdateService(
/** /**
* This method needs to be implemented, but it's not used/needed. * This method needs to be implemented, but it's not used/needed.
*/ */
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? = null
return null
}
/** /**
* Method called when the service receives an intent. * Method called when the service receives an intent.
@ -210,6 +204,7 @@ class LibraryUpdateService(
// Unsubscribe from any previous subscription if needed // Unsubscribe from any previous subscription if needed
updateJob?.cancel() updateJob?.cancel()
ioScope?.cancel()
// Update favorite manga // Update favorite manga
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
@ -220,7 +215,8 @@ class LibraryUpdateService(
logcat(LogPriority.ERROR, exception) logcat(LogPriority.ERROR, exception)
stopSelf(startId) stopSelf(startId)
} }
updateJob = ioScope.launch(handler) { ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
updateJob = ioScope?.launch(handler) {
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList() Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers() Target.COVERS -> updateCovers()
@ -344,16 +340,10 @@ class LibraryUpdateService(
} }
} catch (e: Throwable) { } catch (e: Throwable) {
val errorMessage = when (e) { val errorMessage = when (e) {
is NoChaptersException -> { is NoChaptersException -> getString(R.string.no_chapters_error)
getString(R.string.no_chapters_error)
}
is SourceManager.SourceNotInstalledException -> {
// failedUpdates will already have the source, don't need to copy it into the message // failedUpdates will already have the source, don't need to copy it into the message
getString(R.string.loader_not_implemented_error) is SourceManager.SourceNotInstalledException -> getString(R.string.loader_not_implemented_error)
} else -> e.message
else -> {
e.message
}
} }
failedUpdates.add(mangaWithNotif to errorMessage) failedUpdates.add(mangaWithNotif to errorMessage)
} }
@ -407,11 +397,12 @@ class LibraryUpdateService(
private suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> { private suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
var networkSManga: SManga? = null var updatedManga: SManga = manga
// Update manga details metadata // Update manga details metadata
if (preferences.autoUpdateMetadata()) { if (preferences.autoUpdateMetadata()) {
val updatedManga = source.getMangaDetails(manga.toMangaInfo()) val updatedMangaDetails = source.getMangaDetails(manga.toMangaInfo())
val sManga = updatedManga.toSManga() val sManga = updatedMangaDetails.toSManga()
// Avoid "losing" existing cover // Avoid "losing" existing cover
if (!sManga.thumbnail_url.isNullOrEmpty()) { if (!sManga.thumbnail_url.isNullOrEmpty()) {
manga.prepUpdateCover(coverCache, sManga, false) manga.prepUpdateCover(coverCache, sManga, false)
@ -419,25 +410,22 @@ class LibraryUpdateService(
sManga.thumbnail_url = manga.thumbnail_url sManga.thumbnail_url = manga.thumbnail_url
} }
networkSManga = sManga updatedManga = sManga
} }
val chapters = source.getChapterList(manga.toMangaInfo()) val chapters = source.getChapterList(updatedManga.toMangaInfo())
.map { it.toSChapter() } .map { it.toSChapter() }
// Get manga from database to account for if it was removed // Get manga from database to account for if it was removed during the update
// from library or database
val dbManga = db.getManga(manga.id!!).executeAsBlocking() val dbManga = db.getManga(manga.id!!).executeAsBlocking()
?: return Pair(emptyList(), emptyList()) ?: return Pair(emptyList(), emptyList())
// Copy into [dbManga] to retain favourite value // Copy into [dbManga] to retain favourite value
networkSManga?.let { dbManga.copyFrom(updatedManga)
dbManga.copyFrom(it)
db.insertManga(dbManga).executeAsBlocking() db.insertManga(dbManga).executeAsBlocking()
}
// [dbmanga] was used so that manga data doesn't get overwritten // [dbmanga] was used so that manga data doesn't get overwritten
// incase manga gets new chapter // in case manga gets new chapter
return syncChaptersWithSource(db, chapters, dbManga, source) return syncChaptersWithSource(db, chapters, dbManga, source)
} }

View File

@ -247,7 +247,7 @@ class PagerPageHolder(
return splitInHalf(imageStream) return splitInHalf(imageStream)
} }
val isDoublePage = ImageUtil.isDoublePage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageStream)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageStream
} }

View File

@ -277,7 +277,7 @@ class WebtoonPageHolder(
return imageStream return imageStream
} }
val isDoublePage = ImageUtil.isDoublePage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageStream)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageStream
} }

View File

@ -99,38 +99,24 @@ object ImageUtil {
} }
/** /**
* Check whether the image is a double-page spread * Check whether the image is wide (which we consider a double-page spread).
*
* @return true if the width is greater than the height * @return true if the width is greater than the height
*/ */
fun isDoublePage(imageStream: InputStream): Boolean { fun isWideImage(imageStream: InputStream): Boolean {
imageStream.mark(imageStream.available() + 1) val options = extractImageOptions(imageStream)
val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
imageStream.reset() imageStream.reset()
return options.outWidth > options.outHeight return options.outWidth > options.outHeight
} }
/** /**
* Check whether the image is considered a tall image * Check whether the image is considered a tall image.
* @return true if the height:width ratio is greater than the 3:! *
* @return true if the height:width ratio is greater than 3.
*/ */
fun isTallImage(imageStream: InputStream): Boolean { fun isTallImage(imageStream: InputStream): Boolean {
imageStream.mark(imageStream.available() + 1) val options = extractImageOptions(imageStream)
return (options.outHeight / options.outWidth) > 3
val imageBytes = imageStream.readBytes()
// Checking the image dimensions without loading it in the memory.
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
val width = options.outWidth
val height = options.outHeight
val ratio = height / width
return ratio > 3
} }
/** /**
@ -410,4 +396,16 @@ object ImageUtil {
private fun Int.isWhite(): Boolean = private fun Int.isWhite(): Boolean =
red + blue + green > 740 red + blue + green > 740
/**
* Used to check an image's dimensions without loading it in the memory.
*/
private fun extractImageOptions(imageStream: InputStream): BitmapFactory.Options {
imageStream.mark(imageStream.available() + 1)
val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
return options
}
} }

View File

@ -814,6 +814,7 @@
<string name="download_notifier_no_network">No network connection available</string> <string name="download_notifier_no_network">No network connection available</string>
<string name="download_notifier_download_paused">Download paused</string> <string name="download_notifier_download_paused">Download paused</string>
<string name="download_notifier_download_finish">Download completed</string> <string name="download_notifier_download_finish">Download completed</string>
<string name="download_notifier_split_page_not_found">Page %d not found while splitting</string>
<!-- Notification channels --> <!-- Notification channels -->
<string name="channel_common">Common</string> <string name="channel_common">Common</string>