diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 802b7b12..40793610 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -170,9 +170,8 @@ export default { this.loaded = true }, async fetchCategories() { - const endpoint = this.currentLibraryMediaType === 'book' ? 'personalized2' : 'personalized' const categories = await this.$axios - .$get(`/api/libraries/${this.currentLibraryId}/${endpoint}?include=rssfeed,numEpisodesIncomplete`) + .$get(`/api/libraries/${this.currentLibraryId}/personalized2?include=rssfeed,numEpisodesIncomplete`) .then((data) => { return data }) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index bf4ba697..fba30ec9 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -133,12 +133,15 @@ export default { this.rendition.spread(settings.spread || 'auto') }, prev() { + if (!this.rendition?.manager) return return this.rendition?.prev() }, next() { + if (!this.rendition?.manager) return return this.rendition?.next() }, goToChapter(href) { + if (!this.rendition?.manager) return return this.rendition?.display(href) }, keyUp(e) { diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index e01b779f..d1ed2232 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -446,27 +446,27 @@ module.exports = (sequelize) => { * @returns {object[]} array of shelf objects */ static async getPersonalizedShelves(library, userId, include, limit) { - const isPodcastLibrary = library.mediaType === 'podcast' - const fullStart = Date.now() // Used for testing load times const shelves = [] + // "Continue Listening" shelf 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', + type: library.isPodcast ? 'episode' : 'book', entities: itemsInProgressPayload.items, total: itemsInProgressPayload.count }) } - Logger.debug(`Loaded ${itemsInProgressPayload.items.length} items for "Continue Listening" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) let start = Date.now() - if (library.mediaType === 'book') { + if (library.isBook) { + // "Continue Reading" shelf const ebooksInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, userId, include, limit, true) if (ebooksInProgressPayload.items.length) { shelves.push({ @@ -478,9 +478,10 @@ module.exports = (sequelize) => { total: ebooksInProgressPayload.count }) } - Logger.debug(`Loaded ${ebooksInProgressPayload.items.length} items for "Continue Reading" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${ebooksInProgressPayload.items.length} of ${ebooksInProgressPayload.count} items for "Continue Reading" in ${((Date.now() - start) / 1000).toFixed(2)}s`) start = Date.now() + // "Continue Series" shelf const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, userId, include, limit) if (continueSeriesPayload.libraryItems.length) { shelves.push({ @@ -492,10 +493,25 @@ 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() + Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + } else if (library.isPodcast) { + // "Newest Episodes" shelf + const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, userId, limit) + if (newestEpisodesPayload.libraryItems.length) { + shelves.push({ + id: 'newest-episodes', + label: 'Newest Episodes', + labelStringKey: 'LabelNewestEpisodes', + type: 'episode', + entities: newestEpisodesPayload.libraryItems, + total: newestEpisodesPayload.count + }) + } + Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } + start = Date.now() + // "Recently Added" shelf const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, userId, include, limit) if (mostRecentPayload.libraryItems.length) { shelves.push({ @@ -507,77 +523,86 @@ module.exports = (sequelize) => { total: mostRecentPayload.count }) } - Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} 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({ - id: 'recent-series', - label: 'Recent Series', - labelStringKey: 'LabelRecentSeries', - type: 'series', - entities: seriesMostRecentPayload.series, - total: seriesMostRecentPayload.count - }) + if (library.isBook) { + start = Date.now() + // "Recent Series" shelf + const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, include, 5) + if (seriesMostRecentPayload.series.length) { + shelves.push({ + id: 'recent-series', + label: 'Recent Series', + labelStringKey: 'LabelRecentSeries', + type: 'series', + entities: seriesMostRecentPayload.series, + total: seriesMostRecentPayload.count + }) + } + Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + + start = Date.now() + // "Discover" shelf + 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} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } - 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() + // "Listen Again" shelf 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', + type: library.isPodcast ? '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`) + Logger.debug(`Loaded ${listenAgainPayload.items.length} of ${listenAgainPayload.count} 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`) + if (library.isBook) { + start = Date.now() + // "Read Again" shelf + 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} of ${readAgainPayload.count} 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 - }) + start = Date.now() + // "Newest Authors" shelf + 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} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) } - 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`) diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 26381c99..5e612184 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -59,10 +59,14 @@ module.exports = { count } } else { - // TODO: Get episodes in progress + const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, 'progress', 'in-progress', 'progress', true, limit, 0) return { - count: 0, - items: [] + count, + items: libraryItems.map(li => { + const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() + oldLibraryItem.recentEpisode = li.recentEpisode + return oldLibraryItem + }) } } }, @@ -160,10 +164,14 @@ module.exports = { count } } else { - // TODO: Get podcast episodes finished + const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, 'progress', 'finished', 'progress', true, limit, 0) return { - items: [], - count: 0 + count, + items: libraryItems.map(li => { + const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() + oldLibraryItem.recentEpisode = li.recentEpisode + return oldLibraryItem + }) } } }, @@ -299,5 +307,26 @@ module.exports = { }), count } + }, + + /** + * Get podcast episodes most recently added + * @param {oldLibrary} library + * @param {string} userId + * @param {number} limit + * @returns {object} {libraryItems:oldLibraryItem[], count:number} + */ + async getNewestPodcastEpisodes(library, userId, limit) { + if (library.mediaType !== 'podcast') return { libraryItems: [], count: 0 } + + const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, null, null, 'createdAt', true, limit, 0) + return { + count, + libraryItems: libraryItems.map(li => { + const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() + oldLibraryItem.recentEpisode = li.recentEpisode + return oldLibraryItem + }) + } } } \ No newline at end of file diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 3c0d345e..8ed8c19c 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -230,7 +230,7 @@ module.exports = { } } else if (sortBy === 'sequence') { const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' - return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS INTEGER) COLLATE NOCASE ${nullDir}`)]] + return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS INTEGER) ${nullDir}`)]] } else if (sortBy === 'progress') { return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]] } @@ -268,7 +268,7 @@ module.exports = { } ], order: [ - Sequelize.literal('CAST(`books.bookSeries.sequence` AS INTEGER) COLLATE NOCASE ASC NULLS LAST') + Sequelize.literal('CAST(`books.bookSeries.sequence` AS INTEGER) ASC NULLS LAST') ] }) const bookSeriesToInclude = [] @@ -426,7 +426,7 @@ module.exports = { }) if (sortBy !== 'sequence') { // Secondary sort by sequence - sortOrder.push([Sequelize.literal('CAST(`series.bookSeries.sequence` AS INTEGER) COLLATE NOCASE ASC NULLS LAST')]) + sortOrder.push([Sequelize.literal('CAST(`series.bookSeries.sequence` AS INTEGER) ASC NULLS LAST')]) } } else if (filterGroup === 'issues') { libraryItemWhere[Sequelize.Op.or] = [ diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 1fea46c3..ad163482 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -148,6 +148,96 @@ module.exports = { return libraryItem }) + return { + libraryItems, + count + } + }, + + /** + * Get podcast episodes filtered and sorted + * @param {string} libraryId + * @param {string} userId + * @param {[string]} filterGroup + * @param {[string]} filterValue + * @param {string} sortBy + * @param {string} sortDesc + * @param {number} limit + * @param {number} offset + * @returns {object} {libraryItems:LibraryItem[], count:number} + */ + async getFilteredPodcastEpisodes(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) { + if (sortBy === 'progress' && filterGroup !== 'progress') { + Logger.warn('Cannot sort podcast episodes by progress without filtering by progress') + sortBy = 'createdAt' + } + + const podcastEpisodeIncludes = [] + let podcastEpisodeWhere = {} + if (filterGroup === 'progress') { + podcastEpisodeIncludes.push({ + model: Database.models.mediaProgress, + where: { + userId + }, + attributes: ['id', 'isFinished', 'currentTime', 'updatedAt'] + }) + + if (filterValue === 'in-progress') { + podcastEpisodeWhere = [ + { + '$mediaProgresses.isFinished$': false + }, + { + '$mediaProgresses.currentTime$': { + [Sequelize.Op.gt]: 0 + } + } + ] + } else if (filterValue === 'finished') { + podcastEpisodeWhere['$mediaProgresses.isFinished$'] = true + } + } + + const podcastEpisodeOrder = [] + if (sortBy === 'createdAt') { + podcastEpisodeOrder.push(['createdAt', sortDesc ? 'DESC' : 'ASC']) + } else if (sortBy === 'progress') { + podcastEpisodeOrder.push([Sequelize.literal('mediaProgresses.updatedAt'), sortDesc ? 'DESC' : 'ASC']) + } + + const { rows: podcastEpisodes, count } = await Database.models.podcastEpisode.findAndCountAll({ + where: podcastEpisodeWhere, + include: [ + { + model: Database.models.podcast, + include: [ + { + model: Database.models.libraryItem, + where: { + libraryId + } + } + ] + }, + ...podcastEpisodeIncludes + ], + distinct: true, + subQuery: false, + order: podcastEpisodeOrder, + limit, + offset + }) + + const libraryItems = podcastEpisodes.map((ep) => { + const libraryItem = ep.podcast.libraryItem.toJSON() + const podcast = ep.podcast.toJSON() + delete podcast.libraryItem + libraryItem.media = podcast + libraryItem.recentEpisode = ep.getOldPodcastEpisode(libraryItem.id) + return libraryItem + }) + return { libraryItems, count