diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt
index 35b9d5a2c..d15279222 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt
@@ -49,7 +49,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
 
     override suspend fun update(track: Track): Track {
         val mdex = mdex ?: throw Exception("Mangadex not enabled")
-        val mangaMetadata = db.getFlatMetadataForManga(track.manga_id).executeAsBlocking()
+        val mangaMetadata = db.getFlatMetadataForManga(track.manga_id).executeOnIO()
             ?.raise<MangaDexSearchMetadata>()
             ?: throw Exception("Invalid manga metadata")
         val followStatus = FollowStatus.fromInt(track.status)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
index 11b5483a8..5e4a361df 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
@@ -20,6 +20,7 @@ import exh.search.QueryComponent
 import exh.search.SearchEngine
 import exh.search.Text
 import exh.util.cancellable
+import exh.util.executeOnIO
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -114,7 +115,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
                     // Prepare filter object
                     val parsedQuery = searchEngine.parseQuery(savedSearchText)
 
-                    val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().executeAsBlocking()
+                    val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().executeOnIO()
                     val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
                     if (mangaWithMetaIds.isNotEmpty()) {
                         val mangaIdCol = mangaWithMetaIdsQuery.getColumnIndex(MangaTable.COL_ID)
@@ -137,8 +138,8 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
                                 // No meta? Filter using title
                                 filterManga(parsedQuery, item.manga)
                             } else {
-                                val tags = db.getSearchTagsForManga(mangaId).executeAsBlocking()
-                                val titles = db.getSearchTitlesForManga(mangaId).executeAsBlocking()
+                                val tags = db.getSearchTagsForManga(mangaId).executeOnIO()
+                                val titles = db.getSearchTitlesForManga(mangaId).executeOnIO()
                                 filterManga(parsedQuery, item.manga, false, tags, titles)
                             }
                         } else {
@@ -166,7 +167,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC
 
     private suspend fun filterManga(queries: List<QueryComponent>, manga: LibraryManga, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null): Boolean {
         val mappedQueries = queries.groupBy { it.excluded }
-        val tracks = if (hasLoggedServices) db.getTracks(manga).executeAsBlocking().toList() else null
+        val tracks = if (hasLoggedServices) db.getTracks(manga).executeOnIO() else null
         val source = sourceManager.get(manga.source)
         val genre = if (checkGenre) manga.getGenres().orEmpty() else emptyList()
         val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
index 694e7e67e..6b2323fc1 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt
@@ -643,7 +643,7 @@ class SettingsEhController : SettingsController() {
                             val allMeta = db.getFavoriteMangaWithMetadata().executeAsBlocking().filter {
                                 it.source == EH_SOURCE_ID || it.source == EXH_SOURCE_ID
                             }.mapNotNull {
-                                db.getFlatMetadataForManga(it.id!!).executeAsBlocking()
+                                db.getFlatMetadataForManga(it.id!!).executeOnIO()
                                     ?.raise<EHentaiSearchMetadata>()
                             }.toList()
 
diff --git a/app/src/main/java/exh/debug/DebugFunctions.kt b/app/src/main/java/exh/debug/DebugFunctions.kt
index 0ea8f5239..1e0fa9318 100644
--- a/app/src/main/java/exh/debug/DebugFunctions.kt
+++ b/app/src/main/java/exh/debug/DebugFunctions.kt
@@ -60,7 +60,7 @@ object DebugFunctions {
             }.toList()
 
             allManga.forEach { manga ->
-                val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach
+                val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
                 // remove age flag
                 meta.aged = false
                 db.insertFlatMetadataAsync(meta.flatten()).await()
@@ -95,7 +95,7 @@ object DebugFunctions {
                     )?.getMangaDetails(manga.toMangaInfo())?.let { networkManga ->
                     manga.copyFrom(networkManga.toSManga())
                     manga.initialized = true
-                    db.insertManga(manga).executeAsBlocking()
+                    db.insertManga(manga).executeOnIO()
                 }
             }
         }
@@ -113,7 +113,7 @@ object DebugFunctions {
             }.toList()
 
             allManga.forEach { manga ->
-                val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach
+                val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
                 galleries += "Aged: ${meta.aged}\t Title: ${manga.title}"
             }
         }
@@ -123,7 +123,7 @@ object DebugFunctions {
     fun countAgedFlagInEXHManga(): Int {
         var agedAmount = 0
         runBlocking {
-            val metadataManga = db.getFavoriteMangaWithMetadata().executeAsBlocking()
+            val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO()
 
             val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga ->
                 if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
@@ -132,7 +132,7 @@ object DebugFunctions {
             }.toList()
 
             allManga.forEach { manga ->
-                val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>() ?: return@forEach
+                val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>() ?: return@forEach
                 if (meta.aged) {
                     // remove age flag
                     agedAmount++
@@ -203,7 +203,7 @@ object DebugFunctions {
         EHentaiUpdateWorker.scheduleBackground(app)
     }
 
-    fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j ->
+    fun listScheduledJobs() = app.jobScheduler.allPendingJobs.joinToString(",\n") { j ->
         """
         {
             info: ${j.id},
@@ -212,7 +212,7 @@ object DebugFunctions {
             intervalMillis: ${j.intervalMillis},
         }
         """.trimIndent()
-    }.joinToString(",\n")
+    }
 
     fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()
 
diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt
index 54e473edf..bb095f6bc 100644
--- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt
+++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt
@@ -7,6 +7,7 @@ import android.app.job.JobService
 import android.content.ComponentName
 import android.content.Context
 import android.os.Build
+import com.elvishew.xlog.Logger
 import com.elvishew.xlog.XLog
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -45,20 +46,18 @@ import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 import java.util.ArrayList
-import kotlin.coroutines.CoroutineContext
 import kotlin.time.ExperimentalTime
 import kotlin.time.days
 import kotlin.time.hours
 
-class EHentaiUpdateWorker : JobService(), CoroutineScope {
-    override val coroutineContext: CoroutineContext
-        get() = Dispatchers.Default + Job()
+class EHentaiUpdateWorker : JobService() {
+    private val scope = CoroutineScope(Dispatchers.Default + Job())
 
     private val db: DatabaseHelper by injectLazy()
     private val prefs: PreferencesHelper by injectLazy()
     private val sourceManager: SourceManager by injectLazy()
     private val updateHelper: EHentaiUpdateHelper by injectLazy()
-    private val logger = XLog.tag("EHUpdater")
+    private val logger: Logger = XLog.tag("EHUpdater").build()
 
     private val updateNotifier by lazy { LibraryUpdateNotifier(this) }
 
@@ -86,7 +85,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
      * to end the job entirely.  Regardless of the value returned, your job must stop executing.
      */
     override fun onStopJob(params: JobParameters?): Boolean {
-        runBlocking { this@EHentaiUpdateWorker.coroutineContext[Job]?.cancelAndJoin() }
+        runBlocking { scope.coroutineContext[Job]?.cancelAndJoin() }
         return false
     }
 
@@ -122,7 +121,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
      * extras configured with [     This object serves to identify this specific running job instance when calling][JobInfo.Builder.setExtras]
      */
     override fun onStartJob(params: JobParameters): Boolean {
-        launch {
+        scope.launch {
             startUpdating()
             logger.d("Update job completed!")
             jobFinished(params, false)
@@ -145,7 +144,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
                 return@mapNotNull null
             }
 
-            val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()
+            val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()
                 ?: return@mapNotNull null
 
             val raisedMeta = meta.raise<EHentaiSearchMetadata>()
@@ -274,7 +273,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
             return new to db.getChapters(manga).executeOnIO()
         } catch (t: Throwable) {
             if (t is EHentai.GalleryNotFoundException) {
-                val meta = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise<EHentaiSearchMetadata>()
+                val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise<EHentaiSearchMetadata>()
                 if (meta != null) {
                     // Age dead galleries
                     logger.d("Aged %s - notfound", manga.id)
@@ -345,8 +344,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
         }
 
         fun launchBackgroundTest(context: Context): String {
-            val jobScheduler = context.jobScheduler
-            return if (jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
+            return if (context.jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
                 logger.e("Failed to schedule background test job!")
                 "Failed"
             } else {
diff --git a/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt b/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt
index fd1dc2861..249e2a3fb 100644
--- a/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt
+++ b/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt
@@ -2,6 +2,8 @@ package exh.metadata.metadata.base
 
 import com.pushtorefresh.storio.operations.PreparedOperation
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.util.lang.runAsObservable
+import eu.kanade.tachiyomi.util.lang.withIOContext
 import exh.metadata.sql.models.SearchMetadata
 import exh.metadata.sql.models.SearchTag
 import exh.metadata.sql.models.SearchTitle
@@ -9,6 +11,9 @@ import exh.util.executeOnIO
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
 import kotlinx.serialization.InternalSerializationApi
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.serializer
@@ -32,23 +37,35 @@ data class FlatMetadata(
             }
 }
 
-fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> {
-    // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
-    val single = Single.fromCallable {
-        val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
+interface PreparedSuspendOperation<T> : PreparedOperation<T> {
+    /**
+     * Creates a [Flow] that emits the result of of Operation
+     *
+     * Example:
+     *  override fun asFlow(): Flow<T> = flow { emit(operation()) }
+     *
+     */
+    fun asFlow(): Flow<T>
+
+    /**
+     * Executes operation asynchronously in the I/O thread pool.
+     */
+    suspend fun executeOnIO(): T
+}
+
+fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedSuspendOperation<FlatMetadata?> =
+    preparedOperationFromSuspend {
+        val meta = getSearchMetadataForManga(mangaId).executeOnIO()
         if (meta != null) {
-            val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
-            val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
+            val tags = getSearchTagsForManga(mangaId).executeOnIO()
+            val titles = getSearchTitlesForManga(mangaId).executeOnIO()
 
             FlatMetadata(meta, tags, titles)
         } else null
     }
 
-    return preparedOperationFromSingle(single)
-}
-
-private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperation<T> {
-    return object : PreparedOperation<T> {
+private fun <T> preparedOperationFromSuspend(operation: suspend () -> T): PreparedSuspendOperation<T> {
+    return object : PreparedSuspendOperation<T> {
         /**
          * Creates [rx.Observable] that emits result of Operation.
          *
@@ -57,7 +74,7 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
          *
          * @return observable result of operation with only one [rx.Observer.onNext] call.
          */
-        override fun createObservable() = single.toObservable()
+        override fun createObservable() = runAsObservable(operation)
 
         /**
          * Executes operation synchronously in current thread.
@@ -66,11 +83,11 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
          * Notice: Blocking I/O operation should not be executed on the Main Thread,
          * it can cause ANR (Activity Not Responding dialog), block the UI and drop animations frames.
          * So please, execute blocking I/O operation only from background thread.
-         * See [WorkerThread].
+         * See [androidx.annotation.WorkerThread].
          *
          * @return nullable result of operation.
          */
-        override fun executeAsBlocking() = single.toBlocking().value()
+        override fun executeAsBlocking() = runBlocking { operation() }
 
         /**
          * Creates [rx.Observable] that emits result of Operation.
@@ -80,7 +97,7 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
          *
          * @return observable result of operation with only one [rx.Observer.onNext] call.
          */
-        override fun asRxObservable() = single.toObservable()
+        override fun asRxObservable() = runAsObservable(operation)
 
         /**
          * Creates [rx.Single] that emits result of Operation lazily when somebody subscribes to it.
@@ -89,7 +106,21 @@ private fun <T> preparedOperationFromSingle(single: Single<T>): PreparedOperatio
          *
          * @return single result of operation.
          */
-        override fun asRxSingle() = single
+        override fun asRxSingle(): Single<T> = runAsObservable(operation).toSingle()
+
+        /**
+         * Creates a [Flow] that emits the result of of Operation
+         *
+         * Example:
+         *  override fun asFlow(): Flow<T> = flow { emit(operation()) }
+         *
+         */
+        override fun asFlow(): Flow<T> = flow { emit(operation()) }
+
+        /**
+         * Executes operation asynchronously in the I/O thread pool.
+         */
+        override suspend fun executeOnIO(): T = withIOContext { operation() }
     }
 }
 
diff --git a/app/src/main/java/exh/ui/metadata/MetadataViewPresenter.kt b/app/src/main/java/exh/ui/metadata/MetadataViewPresenter.kt
index fcedf2c61..4338c651a 100644
--- a/app/src/main/java/exh/ui/metadata/MetadataViewPresenter.kt
+++ b/app/src/main/java/exh/ui/metadata/MetadataViewPresenter.kt
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.online.MetadataSource
-import eu.kanade.tachiyomi.util.lang.asFlow
 import exh.metadata.metadata.base.FlatMetadata
 import exh.metadata.metadata.base.RaisedSearchMetadata
 import exh.metadata.metadata.base.getFlatMetadataForManga
@@ -16,7 +15,6 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.plus
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -50,6 +48,6 @@ class MetadataViewPresenter(
     }
 
     private fun getMangaMetaObservable(): Flow<FlatMetadata?> {
-        return db.getFlatMetadataForManga(manga.id!!).asRxObservable().asFlow()
+        return db.getFlatMetadataForManga(manga.id!!).asFlow()
     }
 }
diff --git a/app/src/main/java/exh/util/DatabaseExtensions.kt b/app/src/main/java/exh/util/DatabaseExtensions.kt
index ba11915d9..f3d94c1a2 100644
--- a/app/src/main/java/exh/util/DatabaseExtensions.kt
+++ b/app/src/main/java/exh/util/DatabaseExtensions.kt
@@ -1,6 +1,7 @@
 package exh.util
 
-import com.pushtorefresh.storio.operations.PreparedOperation
+import android.database.Cursor
+import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetCursor
 import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
 import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
 import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects
@@ -18,4 +19,4 @@ suspend fun <T> PreparedPutObject<T>.executeOnIO(): PutResult = withContext(Disp
 
 suspend fun <T> PreparedPutCollectionOfObjects<T>.executeOnIO(): PutResults<T> = withContext(Dispatchers.IO) { executeAsBlocking() }
 
-suspend fun <T> PreparedOperation<T>.executeOnIO(): T? = withContext(Dispatchers.IO) { executeAsBlocking() }
+suspend fun PreparedGetCursor.executeOnIO(): Cursor = withContext(Dispatchers.IO) { executeAsBlocking() }