refactor: Migrate GetLibraryManga to use SQLDelight

This commit is contained in:
Ahmad Ansori Palembani 2024-06-07 07:15:31 +07:00
parent d2f493ab57
commit b17578365d
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
14 changed files with 177 additions and 79 deletions

View file

@ -3,6 +3,7 @@ package dev.yokai.core.di
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
import dev.yokai.data.extension.repo.ExtensionRepoRepositoryImpl
import dev.yokai.data.library.custom.CustomMangaRepositoryImpl
import dev.yokai.data.manga.MangaRepositoryImpl
import dev.yokai.domain.extension.interactor.TrustExtension
import dev.yokai.domain.extension.repo.interactor.CreateExtensionRepo
import dev.yokai.domain.extension.repo.interactor.DeleteExtensionRepo
@ -14,6 +15,8 @@ import dev.yokai.domain.library.custom.CustomMangaRepository
import dev.yokai.domain.library.custom.interactor.CreateCustomManga
import dev.yokai.domain.library.custom.interactor.DeleteCustomManga
import dev.yokai.domain.library.custom.interactor.GetCustomManga
import dev.yokai.domain.manga.MangaRepository
import dev.yokai.domain.manga.interactor.GetLibraryManga
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
@ -36,5 +39,8 @@ class DomainModule : InjektModule {
addFactory { CreateCustomManga(get()) }
addFactory { DeleteCustomManga(get()) }
addFactory { GetCustomManga(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetLibraryManga(get()) }
}
}

View file

@ -0,0 +1,14 @@
package dev.yokai.data.manga
import dev.yokai.data.DatabaseHandler
import dev.yokai.domain.manga.MangaRepository
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import kotlinx.coroutines.flow.Flow
class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepository {
override suspend fun getLibraryManga(): List<LibraryManga> =
handler.awaitList { library_viewQueries.findAll(LibraryManga::mapper) }
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> =
handler.subscribeToList { library_viewQueries.findAll(LibraryManga::mapper) }
}

View file

@ -0,0 +1,9 @@
package dev.yokai.domain.manga
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import kotlinx.coroutines.flow.Flow
interface MangaRepository {
suspend fun getLibraryManga(): List<LibraryManga>
fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>>
}

View file

@ -0,0 +1,12 @@
package dev.yokai.domain.manga.interactor
import dev.yokai.domain.manga.MangaRepository
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import kotlinx.coroutines.flow.Flow
class GetLibraryManga(
private val mangaRepository: MangaRepository,
) {
suspend fun await(): List<LibraryManga> = mangaRepository.getLibraryManga()
fun subscribe(): Flow<List<LibraryManga>> = mangaRepository.getLibraryMangaAsFlow()
}

View file

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.data.database.updateStrategyAdapter
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlin.math.roundToInt
class LibraryManga : MangaImpl() {
var unread: Int = 0
@ -28,5 +32,55 @@ class LibraryManga : MangaImpl() {
status = -1
read = hideCount
}
fun mapper(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: String?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Long,
lastUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
hideTitle: Long,
chapterFlags: Long,
dateAdded: Long?,
filteredScanlators: String?,
updateStrategy: Long,
total: Long,
readCount: Double,
bookmarkCount: Double,
categoryId: Long
): LibraryManga = createBlank(categoryId.toInt()).apply {
this.id = id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite > 0
this.last_update = lastUpdate ?: 0L
this.initialized = initialized
this.viewer_flags = viewerFlags.toInt()
this.hide_title = hideTitle > 0
this.chapter_flags = chapterFlags.toInt()
this.date_added = dateAdded ?: 0L
this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode)
this.read = readCount.roundToInt()
this.unread = (total - this.read).toInt()
this.bookmarkCount = bookmarkCount.roundToInt()
}
}
}

View file

@ -5,10 +5,8 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
@ -17,9 +15,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
interface MangaQueries : DbProvider {
@ -33,17 +29,6 @@ interface MangaQueries : DbProvider {
)
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java)
.withQuery(
RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build(),
)
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getDuplicateLibraryManga(manga: Manga) = db.get()
.`object`(Manga::class.java)
.withQuery(

View file

@ -8,45 +8,6 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
*/
val libraryQuery =
"""
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, '') AS ${Manga.COL_UNREAD}, COALESCE(R.hasread, '') AS ${Manga.COL_HAS_READ}, COALESCE(B.bookmarkCount, 0) AS ${Manga.COL_BOOKMARK_COUNT}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, GROUP_CONCAT(IFNULL(${Chapter.TABLE}.${Chapter.COL_SCANLATOR}, "N/A"), " [.] ") AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, GROUP_CONCAT(IFNULL(${Chapter.TABLE}.${Chapter.COL_SCANLATOR}, "N/A"), " [.] ") AS hasread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS R
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS bookmarkCount
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_BOOKMARK} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS B
ON ${Manga.COL_ID} = B.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}
) AS M
LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
"""
/**
* Query to get the recent chapters of manga from the library up to a date.
*/

View file

@ -19,6 +19,7 @@ import androidx.work.WorkerParameters
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -67,6 +68,7 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import timber.log.Timber
@ -89,6 +91,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val downloadManager: DownloadManager = Injekt.get()
private val trackManager: TrackManager = Injekt.get()
private val mangaShortcutManager: MangaShortcutManager = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private var extraDeferredJobs = mutableListOf<Deferred<Any>>()
@ -155,7 +158,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val mangaList = (
if (savedMangasList != null) {
val mangas = db.getLibraryMangas().executeAsBlocking().filter {
val mangas = getLibraryManga.await().filter {
it.id in savedMangasList
}.distinctBy { it.id }
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
@ -466,7 +469,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
}
private fun getMangaToUpdate(): List<LibraryManga> {
private suspend fun getMangaToUpdate(): List<LibraryManga> {
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
return getMangaToUpdate(categoryId)
}
@ -477,8 +480,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
* @param categoryId the category to update
* @return a list of manga to update
*/
private fun getMangaToUpdate(categoryId: Int): List<LibraryManga> {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
private suspend fun getMangaToUpdate(categoryId: Int): List<LibraryManga> {
val libraryManga = getLibraryManga.await()
val listToUpdate = if (categoryId != -1) {
categoryIds.add(categoryId)
@ -555,7 +558,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
private fun addCategory(categoryId: Int) {
val mangas = filterMangaToUpdate(getMangaToUpdate(categoryId)).sortedBy { it.title }
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title }
categoryIds.add(categoryId)
addManga(mangas)
}

View file

@ -1,7 +1,10 @@
package eu.kanade.tachiyomi.ui.library
import dev.yokai.domain.manga.interactor.GetLibraryManga
import dev.yokai.util.isLewd
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.minusAssign
import eu.kanade.tachiyomi.core.preference.plusAssign
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -14,8 +17,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.core.preference.minusAssign
import eu.kanade.tachiyomi.core.preference.plusAssign
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
@ -51,9 +52,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
import java.util.Locale
import uy.kohesive.injekt.injectLazy
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.random.Random
@ -70,6 +70,7 @@ class LibraryPresenter(
private val chapterFilter: ChapterFilter = Injekt.get(),
private val trackManager: TrackManager = Injekt.get(),
) : BaseCoroutinePresenter<LibraryController>() {
private val getLibraryManga: GetLibraryManga by injectLazy()
private val context = preferences.context
private val viewContext
@ -162,7 +163,7 @@ class LibraryPresenter(
) {
// Doing this instead of a job in case the app isn't used often
presenterScope.launchIO {
setSearchSuggestion(preferences, db, sourceManager)
setSearchSuggestion(preferences, getLibraryManga, sourceManager)
withUIContext { view?.setTitle() }
}
}
@ -713,10 +714,10 @@ class LibraryPresenter(
*
* @return an list of all the manga in a itemized form.
*/
private fun getLibraryFromDB(): Pair<List<LibraryItem>, List<LibraryItem>> {
private suspend fun getLibraryFromDB(): Pair<List<LibraryItem>, List<LibraryItem>> {
removeArticles = preferences.removeArticles().get()
val categories = db.getCategories().executeAsBlocking().toMutableList()
var libraryManga = db.getLibraryMangas().executeAsBlocking()
var libraryManga = getLibraryManga.await()
val showAll = showAllCategories
if (groupType > BY_DEFAULT) {
libraryManga = libraryManga.distinctBy { it.id }
@ -1381,7 +1382,7 @@ class LibraryPresenter(
suspend fun setSearchSuggestion(
preferences: PreferencesHelper,
db: DatabaseHelper,
getLibraryManga: GetLibraryManga,
sourceManager: SourceManager,
) {
val random: Random = run {
@ -1398,7 +1399,7 @@ class LibraryPresenter(
RecentsPresenter.getRecentManga(true).map { it.first }
}
}
val libraryManga by lazy { db.getLibraryMangas().executeAsBlocking() }
val libraryManga by lazy { runBlocking { getLibraryManga.await() } }
preferences.librarySearchSuggestion().set(
when (val value = random.nextInt(0, 5)) {
randomSource -> {
@ -1450,8 +1451,9 @@ class LibraryPresenter(
/** Give library manga to a date added based on min chapter fetch */
fun updateDB() {
val db: DatabaseHelper = Injekt.get()
val getLibraryManga: GetLibraryManga by injectLazy()
db.inTransaction {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
val libraryManga = runBlocking { getLibraryManga.await() }
libraryManga.forEach { manga ->
if (manga.date_added == 0L) {
val chapters = db.getChapters(manga).executeAsBlocking()
@ -1474,8 +1476,9 @@ class LibraryPresenter(
fun updateCustoms() {
val db: DatabaseHelper = Injekt.get()
val cc: CoverCache = Injekt.get()
val getLibraryManga: GetLibraryManga by injectLazy()
db.inTransaction {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
val libraryManga = runBlocking { getLibraryManga.await() }
libraryManga.forEach { manga ->
if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) {
val file = cc.getCoverFile(manga)

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.more.stats
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.LibraryManga
@ -16,8 +17,10 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.more.stats.StatsHelper.getReadDuration
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
* Presenter of [StatsController].
@ -29,12 +32,13 @@ class StatsPresenter(
private val downloadManager: DownloadManager = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) {
private val getLibraryManga: GetLibraryManga by injectLazy()
private val libraryMangas = getLibrary()
val mangaDistinct = libraryMangas.distinct()
private fun getLibrary(): MutableList<LibraryManga> {
return db.getLibraryMangas().executeAsBlocking()
return runBlocking { getLibraryManga.await() }.toMutableList()
}
fun getTracks(manga: Manga): MutableList<Track> {

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.more.stats.details
import android.graphics.drawable.Drawable
import android.text.format.DateUtils
import androidx.annotation.DrawableRes
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -27,6 +28,7 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.roundToTwoDecimal
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -41,6 +43,7 @@ class StatsDetailsPresenter(
val trackManager: TrackManager = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) : BaseCoroutinePresenter<StatsDetailsController>() {
private val getLibraryManga: GetLibraryManga by injectLazy()
private val context
get() = view?.view?.context ?: prefs.context
@ -555,7 +558,7 @@ class StatsDetailsPresenter(
}
fun getLibrary(): MutableList<LibraryManga> {
return db.getLibraryMangas().executeAsBlocking()
return runBlocking { getLibraryManga.await() }.toMutableList()
}
private fun getCategories(): MutableList<Category> {

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.controllers
import androidx.preference.PreferenceScreen
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -28,11 +29,13 @@ import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsLibraryController : SettingsLegacyController() {
private val db: DatabaseHelper = Injekt.get()
private val db: DatabaseHelper by injectLazy()
private val getLibraryManga: GetLibraryManga by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.library
@ -54,7 +57,7 @@ class SettingsLibraryController : SettingsLegacyController() {
it as Boolean
if (it) {
launchIO {
LibraryPresenter.setSearchSuggestion(preferences, db, Injekt.get())
LibraryPresenter.setSearchSuggestion(preferences, getLibraryManga, Injekt.get())
}
} else {
DelayedLibrarySuggestionsJob.setupTask(context, false)

View file

@ -0,0 +1,37 @@
CREATE VIEW library_view AS
SELECT
M.*,
coalesce(C.total, 0) AS total,
coalesce(C.read_count, 0) AS has_read,
coalesce(C.bookmark_count, 0) AS bookmark_count,
coalesce(MC.category_id, 0) AS category
FROM mangas AS M
LEFT JOIN (
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS read_count,
sum(bookmark) AS bookmark_count
FROM chapters
LEFT JOIN (
WITH RECURSIVE split(seq, _id, name, str) AS (
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas
UNION ALL SELECT
seq+1,
_id,
substr(str, 0, instr(str, '[.]')),
substr(str, instr(str, '[.]')+3)
FROM split WHERE str != ''
) SELECT _id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
) AS filtered_scanlators
ON chapters.manga_id = filtered_scanlators._id
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/')
WHERE filtered_scanlators.name IS NULL
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1
GROUP BY M._id
ORDER BY M.title;

View file

@ -14,13 +14,13 @@ LEFT JOIN (
sum(bookmark) AS bookmark_count
FROM chapters
LEFT JOIN (
WITH RECURSIVE split(seq, _id, name, str) AS (
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', ',')||',' FROM mangas
WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas
UNION ALL SELECT
seq+1,
_id,
substr(str, 0, instr(str, ',')),
substr(str, instr(str, ',')+1)
substr(str, 0, instr(str, '[.]')),
substr(str, instr(str, '[.]')+3)
FROM split WHERE str != ''
) SELECT _id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
) AS filtered_scanlators
@ -35,3 +35,7 @@ ON MC.manga_id = M._id
WHERE M.favorite = 1
GROUP BY M._id
ORDER BY M.title;
findAll:
SELECT *
FROM library_view;