Add remaining personalized shelf queries, update book libraries home page to use new API endpoint

This commit is contained in:
advplyr 2023-08-05 14:01:16 -05:00
parent 80b3bfea51
commit 09eefae808
20 changed files with 359 additions and 26 deletions

View File

@ -68,6 +68,9 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
libraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
@ -167,8 +170,9 @@ export default {
this.loaded = true
},
async fetchCategories() {
const endpoint = this.currentLibraryMediaType === 'book' ? 'personalized2' : 'personalized'
const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
.$get(`/api/libraries/${this.currentLibraryId}/${endpoint}?include=rssfeed,numEpisodesIncomplete`)
.then((data) => {
return data
})
@ -346,8 +350,6 @@ export default {
})
},
episodeAdded(episodeWithLibraryItem) {
console.log('Podcast episode added', episodeWithLibraryItem)
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
if (!this.search && isThisLibrary) {
this.fetchCategories()

View File

@ -348,6 +348,10 @@ export default {
},
tracks() {
return [
{
id: 'none',
name: this.$strings.LabelTracksNone
},
{
id: 'single',
name: this.$strings.LabelTracksSingleTrack

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Verzeichnis",
"LabelDiscFromFilename": "CD aus dem Dateinamen",
"LabelDiscFromMetadata": "CD aus den Metadaten",
"LabelDiscover": "Discover",
"LabelDownload": "Herunterladen",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Laufzeit",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Titel aus Metadaten",
"LabelTracks": "Dateien",
"LabelTracksMultiTrack": "Mehrfachdatei",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Einzeldatei",
"LabelType": "Typ",
"LabelUnabridged": "Ungekürzt",
@ -702,4 +704,4 @@
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
"ToastUserDeleteSuccess": "Benutzer gelöscht"
}
}

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Track from Metadata",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Directorio",
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
"LabelDiscFromMetadata": "Disco a partir de Metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Descargar",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duración",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Pista desde Metadata",
"LabelTracks": "Pistas",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Tipo",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Répertoire",
"LabelDiscFromFilename": "Disque depuis le fichier",
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
"LabelDiscover": "Discover",
"LabelDownload": "Téléchargement",
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
"LabelDuration": "Durée",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
"LabelTracks": "Pistes",
"LabelTracksMultiTrack": "Piste multiple",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Piste simple",
"LabelType": "Type",
"LabelUnabridged": "Version intégrale",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Track from Metadata",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duration",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Track from Metadata",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Direktorij",
"LabelDiscFromFilename": "CD iz imena datoteke",
"LabelDiscFromMetadata": "CD iz metapodataka",
"LabelDiscover": "Discover",
"LabelDownload": "Preuzmi",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Trajanje",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Track iz metapodataka",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Tip",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Elenco",
"LabelDiscFromFilename": "Disco dal nome file",
"LabelDiscFromMetadata": "Disco dal Metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Durata",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Traccia da Metadata",
"LabelTracks": "Traccia",
"LabelTracksMultiTrack": "Multi-traccia",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Traccia-singola",
"LabelType": "Tipo",
"LabelUnabridged": "Integrale",
@ -702,4 +704,4 @@
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
"ToastUserDeleteFailed": "Errore eliminazione utente",
"ToastUserDeleteSuccess": "Utente eliminato"
}
}

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Katalogas",
"LabelDiscFromFilename": "Diskas pagal failo pavadinimą",
"LabelDiscFromMetadata": "Diskas pagal metaduomenis",
"LabelDiscover": "Discover",
"LabelDownload": "Atsisiųsti",
"LabelDownloadNEpisodes": "Atsisiųsti {0} epizodų",
"LabelDuration": "Trukmė",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Takelis iš metaduomenų",
"LabelTracks": "Takeliai",
"LabelTracksMultiTrack": "Keli takeliai",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Vienas takelis",
"LabelType": "Tipas",
"LabelUnabridged": "Neprikurptas",
@ -702,4 +704,4 @@
"ToastSocketFailedToConnect": "Nepavyko prisijungti prie serverio",
"ToastUserDeleteFailed": "Nepavyko ištrinti naudotojo",
"ToastUserDeleteSuccess": "Naudotojas ištrintas"
}
}

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Map",
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
"LabelDiscFromMetadata": "Schijf uit metadata",
"LabelDiscover": "Discover",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Duur",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Track vanuit metadata",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Onverkort",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Katalog",
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
"LabelDiscover": "Discover",
"LabelDownload": "Pobierz",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Czas trwania",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Ścieżka z metadanych",
"LabelTracks": "Tracks",
"LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Typ",
"LabelUnabridged": "Unabridged",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "Каталог",
"LabelDiscFromFilename": "Диск из Имени файла",
"LabelDiscFromMetadata": "Диск из Метаданных",
"LabelDiscover": "Discover",
"LabelDownload": "Скачать",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Длина",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "Трек из Метаданных",
"LabelTracks": "Треков",
"LabelTracksMultiTrack": "Мультитрек",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Один трек",
"LabelType": "Тип",
"LabelUnabridged": "Полное издание",

View File

@ -222,6 +222,7 @@
"LabelDirectory": "目录",
"LabelDiscFromFilename": "从文件名获取光盘",
"LabelDiscFromMetadata": "从元数据获取光盘",
"LabelDiscover": "Discover",
"LabelDownload": "下载",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "持续时间",
@ -474,6 +475,7 @@
"LabelTrackFromMetadata": "从源数据获取音轨",
"LabelTracks": "音轨",
"LabelTracksMultiTrack": "多轨",
"LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "单轨",
"LabelType": "类型",
"LabelUnabridged": "未删节",

View File

@ -1,4 +1,4 @@
const { DataTypes, Model, literal } = require('sequelize')
const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger')
const oldLibraryItem = require('../objects/LibraryItem')
const libraryFilters = require('../utils/queries/libraryFilters')
@ -447,32 +447,40 @@ module.exports = (sequelize) => {
*/
static async getPersonalizedShelves(library, userId, include, limit) {
const isPodcastLibrary = library.mediaType === 'podcast'
const fullStart = Date.now() // Used for testing load times
const shelves = []
const itemsInProgressPayload = await libraryFilters.getLibraryItemsInProgress(library, userId, include, limit, false)
if (itemsInProgressPayload.libraryItems.length) {
const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, userId, include, limit, false)
if (itemsInProgressPayload.items.length) {
shelves.push({
id: 'continue-listening',
label: 'Continue Listening',
labelStringKey: 'LabelContinueListening',
type: isPodcastLibrary ? 'episode' : 'book',
entities: itemsInProgressPayload.libraryItems,
entities: itemsInProgressPayload.items,
total: itemsInProgressPayload.count
})
}
Logger.debug(`Loaded ${itemsInProgressPayload.items.length} items for "Continue Listening" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)
let start = Date.now()
if (library.mediaType === 'book') {
const ebooksInProgressPayload = await libraryFilters.getLibraryItemsInProgress(library, userId, include, limit, true)
if (ebooksInProgressPayload.libraryItems.length) {
const ebooksInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, userId, include, limit, true)
if (ebooksInProgressPayload.items.length) {
shelves.push({
id: 'continue-reading',
label: 'Continue Reading',
labelStringKey: 'LabelContinueReading',
type: 'book',
entities: ebooksInProgressPayload.libraryItems,
entities: ebooksInProgressPayload.items,
total: ebooksInProgressPayload.count
})
}
Logger.debug(`Loaded ${ebooksInProgressPayload.items.length} items for "Continue Reading" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, userId, include, limit)
if (continueSeriesPayload.libraryItems.length) {
shelves.push({
@ -484,6 +492,8 @@ module.exports = (sequelize) => {
total: continueSeriesPayload.count
})
}
Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
}
const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, userId, include, limit)
@ -497,7 +507,9 @@ module.exports = (sequelize) => {
total: mostRecentPayload.count
})
}
Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, include, 5)
if (seriesMostRecentPayload.series.length) {
shelves.push({
@ -509,6 +521,65 @@ module.exports = (sequelize) => {
total: seriesMostRecentPayload.count
})
}
Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} items for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, userId, include, limit)
if (discoverLibraryItemsPayload.libraryItems.length) {
shelves.push({
id: 'discover',
label: 'Discover',
labelStringKey: 'LabelDiscover',
type: library.mediaType,
entities: discoverLibraryItemsPayload.libraryItems,
total: discoverLibraryItemsPayload.count
})
}
Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const listenAgainPayload = await libraryFilters.getMediaFinished(library, userId, include, limit, false)
if (listenAgainPayload.items.length) {
shelves.push({
id: 'listen-again',
label: 'Listen Again',
labelStringKey: 'LabelListenAgain',
type: isPodcastLibrary ? 'episode' : 'book',
entities: listenAgainPayload.items,
total: listenAgainPayload.count
})
}
Logger.debug(`Loaded ${listenAgainPayload.items.length} items for "Listen Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const readAgainPayload = await libraryFilters.getMediaFinished(library, userId, include, limit, true)
if (readAgainPayload.items.length) {
shelves.push({
id: 'read-again',
label: 'Read Again',
labelStringKey: 'LabelReadAgain',
type: 'book',
entities: readAgainPayload.items,
total: readAgainPayload.count
})
}
Logger.debug(`Loaded ${readAgainPayload.items.length} items for "Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, limit)
if (newestAuthorsPayload.authors.length) {
shelves.push({
id: 'newest-authors',
label: 'Newest Authors',
labelStringKey: 'LabelNewestAuthors',
type: 'authors',
entities: newestAuthorsPayload.authors,
total: newestAuthorsPayload.count
})
}
Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)
return shelves
}

View File

@ -62,7 +62,8 @@ module.exports = {
} else if (group === 'languages') {
filtered = filtered.filter(li => li.media.metadata.language === filter)
} else if (group === 'tracks') {
if (filter === 'single') filtered = filtered.filter(li => li.isBook && li.media.numTracks === 1)
if (filter === 'none') filtered = filtered.filter(li => li.isBook && !li.media.numTracks)
else if (filter === 'single') filtered = filtered.filter(li => li.isBook && li.media.numTracks === 1)
else if (filter === 'multi') filtered = filtered.filter(li => li.isBook && li.media.numTracks > 1)
} else if (group === 'ebooks') {
if (filter === 'ebook') filtered = filtered.filter(li => li.media.ebookFile)

View File

@ -1,3 +1,4 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
const Logger = require('../../Logger')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
@ -41,14 +42,14 @@ module.exports = {
* @param {string[]} include
* @param {number} limit
* @param {boolean} ebook true if continue reading shelf
* @returns {object} { libraryItems:LibraryItem[], count:number }
* @returns {object} { items:LibraryItem[], count:number }
*/
async getLibraryItemsInProgress(library, userId, include, limit, ebook = false) {
async getMediaItemsInProgress(library, userId, include, limit, ebook = false) {
if (library.mediaType === 'book') {
const filterValue = ebook ? 'ebook-in-progress' : 'audio-in-progress'
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, 'progress', filterValue, 'progress', true, false, include, limit, 0)
return {
libraryItems: libraryItems.map(li => {
items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
@ -58,9 +59,10 @@ module.exports = {
count
}
} else {
// TODO: Get episodes in progress
return {
count: 0,
libraryItems: []
items: []
}
}
},
@ -132,6 +134,40 @@ module.exports = {
}
},
/**
* Get library items or podcast episodes for the "Listen Again" or "Read Again" shelf
* @param {oldLibrary} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @param {boolean} ebook true if "Read Again" shelf
* @returns {object} { items:object[], count:number }
*/
async getMediaFinished(library, userId, include, limit, ebook = false) {
if (ebook && library.mediaType !== 'book') return { items: [], count: 0 }
if (library.mediaType === 'book') {
const filterValue = ebook ? 'ebook-finished' : 'finished'
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, 'progress', filterValue, 'progress', true, false, include, limit, 0)
return {
items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
}
return oldLibraryItem
}),
count
}
} else {
// TODO: Get podcast episodes finished
return {
items: [],
count: 0
}
}
},
/**
* Get series for recent series shelf
* @param {oldLibrary} library
@ -140,6 +176,8 @@ module.exports = {
* @returns {object} { series:oldSeries[], count:number}
*/
async getSeriesMostRecentlyAdded(library, include, limit) {
if (library.mediaType !== 'book') return { series: [], count: 0 }
const seriesIncludes = []
if (include.includes('rssfeed')) {
seriesIncludes.push({
@ -172,8 +210,6 @@ module.exports = {
]
})
Logger.debug(`Found ${series.length} series recently added (${count} total)`)
const allOldSeries = []
for (const s of series) {
const oldSeries = s.getOldSeries().toJSON()
@ -205,5 +241,63 @@ module.exports = {
series: allOldSeries,
count
}
},
/**
* Get most recently created authors for "Newest Authors" shelf
* Author must be linked to at least 1 book
* @param {oldLibrary} library
* @param {number} limit
* @returns {object} { authors:oldAuthor[], count:number }
*/
async getNewestAuthors(library, limit) {
if (library.mediaType !== 'book') return { authors: [], count: 0 }
const { rows: authors, count } = await Database.models.author.findAndCountAll({
where: {
libraryId: library.id
},
include: {
model: Database.models.bookAuthor,
required: true // Must belong to a book
},
limit,
distinct: true,
order: [
['createdAt', 'DESC']
]
})
return {
authors: authors.map((au) => {
const numBooks = au.bookAuthors?.length || 0
return au.getOldAuthor().toJSONExpanded(numBooks)
}),
count
}
},
/**
* Get book library items for the "Discover" shelf
* @param {oldLibrary} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @returns {object} {libraryItems:oldLibraryItem[], count:number}
*/
async getLibraryItemsToDiscover(library, userId, include, limit) {
if (library.mediaType !== 'book') return { libraryItems: [], count: 0 }
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, userId, include, limit)
return {
libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
}
return oldLibraryItem
}),
count
}
}
}

View File

@ -119,7 +119,9 @@ module.exports = {
}
]
} else if (value === 'ebook-in-progress') {
mediaWhere[Sequelize.Op.and] = [
// Filters for ebook only
mediaWhere = [
Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 0),
{
'$mediaProgresses.ebookProgress$': {
[Sequelize.Op.gt]: 0
@ -129,6 +131,17 @@ module.exports = {
'$mediaProgresses.isFinished$': false
}
]
} else if (value === 'ebook-finished') {
// Filters for ebook only
mediaWhere = [
Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 0),
{
'$mediaProgresses.isFinished$': true,
'ebookFile': {
[Sequelize.Op.not]: null
}
}
]
}
} else if (group === 'series' && value === 'no-series') {
mediaWhere['$series.id$'] = null
@ -144,7 +157,9 @@ module.exports = {
} else if (group === 'languages') {
mediaWhere['language'] = value
} else if (group === 'tracks') {
if (value === 'multi') {
if (value === 'none') {
mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 0)
} else if (value === 'multi') {
mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), {
[Sequelize.Op.gt]: 1
})
@ -542,7 +557,7 @@ module.exports = {
return libraryItem
})
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
return {
libraryItems,
count
@ -663,8 +678,6 @@ module.exports = {
offset
})
Logger.debug('Found', series.length, 'series to continue', 'total=', count)
// Step 3: Map series to library items by selecting the first unfinished book in the series
const libraryItems = series.map(s => {
// Natural sort sequence, nulls last
@ -695,6 +708,128 @@ module.exports = {
libraryItem.media = bookSeries.book
return libraryItem
})
return {
libraryItems,
count
}
},
/**
* Get book library items for the "Discover" shelf
* Random selection of books that are not started
* - only includes the first book of a not-started series
* @param {string} libraryId
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @returns {object} {libraryItems:LibraryItem, count:number}
*/
async getDiscoverLibraryItems(libraryId, userId, include, limit) {
// Step 1: Get the first book of every series that hasnt been started yet
const seriesNotStarted = await Database.models.series.findAll({
where: [
{
libraryId
},
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId WHERE bs.seriesId = series.id AND mp.userId = :userId AND (mp.isFinished = 1 OR mp.currentTime > 0))`), 0)
],
replacements: {
userId
},
attributes: ['id'],
include: {
model: Database.models.bookSeries,
attributes: ['bookId', 'sequence'],
separate: true,
required: true,
order: [
[Sequelize.literal('CAST(sequence AS INTEGER) ASC NULLS LAST')]
],
limit: 1
},
subQuery: false
})
const booksFromSeriesToInclude = seriesNotStarted.map(se => se.bookSeries?.[0]?.bookId).filter(bid => bid)
// optional include rssFeed
const libraryItemIncludes = []
if (include.includes('rssfeed')) {
libraryItemIncludes.push({
model: Database.models.feed
})
}
// Step 2: Get books not started and not in a series OR is the first book of a series not started (ordered randomly)
const { rows: books, count } = await Database.models.book.findAndCountAll({
where: {
'$mediaProgresses.isFinished$': {
[Sequelize.Op.or]: [null, 0]
},
'$mediaProgresses.currentTime$': {
[Sequelize.Op.or]: [null, 0]
},
[Sequelize.Op.or]: [
Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0),
{
id: {
[Sequelize.Op.in]: booksFromSeriesToInclude
}
}
]
},
include: [
{
model: Database.models.libraryItem,
where: {
libraryId
},
include: libraryItemIncludes
},
{
model: Database.models.mediaProgress,
where: {
userId
},
required: false
},
{
model: Database.models.bookAuthor,
attributes: ['authorId'],
include: {
model: Database.models.author
},
separate: true
},
{
model: Database.models.bookSeries,
attributes: ['seriesId', 'sequence'],
include: {
model: Database.models.series
},
separate: true
}
],
subQuery: false,
distinct: true,
limit,
order: Database.sequelize.random()
})
// Step 3: Map books to library items
const libraryItems = books.map((bookExpanded) => {
const libraryItem = bookExpanded.libraryItem.toJSON()
const book = bookExpanded.toJSON()
delete book.libraryItem
libraryItem.media = book
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]
}
return libraryItem
})
return {
libraryItems,
count

View File

@ -147,7 +147,7 @@ module.exports = {
return libraryItem
})
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
return {
libraryItems,
count