diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index dc7175bc..171b72cd 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -820,7 +820,6 @@ export default { return null }) if (!libraryItem) return - console.log('Got library itemn', libraryItem) this.store.commit('showEReader', libraryItem) }, selectBtnClick(evt) { diff --git a/client/strings/de.json b/client/strings/de.json index 81c8c910..0996bfdf 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Jahr", "LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentSeries": "Aktuelle Serien", + "LabelRecommended": "Recommended", "LabelRegion": "Region", "LabelReleaseDate": "Veröffentlichungsdatum", "LabelRemoveCover": "Lösche Titelbild", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Dienstag", "WeekdayWed": "Wed", "WeekdayWednesday": "Mittwoch" -} +} \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 2c68bba0..85ebac07 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Publish Year", "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", + "LabelRecommended": "Recommended", "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Tuesday", "WeekdayWed": "Wed", "WeekdayWednesday": "Wednesday" -} +} \ No newline at end of file diff --git a/client/strings/es.json b/client/strings/es.json index 2c68bba0..85ebac07 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Publish Year", "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", + "LabelRecommended": "Recommended", "LabelRegion": "Region", "LabelReleaseDate": "Release Date", "LabelRemoveCover": "Remove cover", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Tuesday", "WeekdayWed": "Wed", "WeekdayWednesday": "Wednesday" -} +} \ No newline at end of file diff --git a/client/strings/fr.json b/client/strings/fr.json index d237c68d..c4a0e82a 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Année d'Edition", "LabelRecentlyAdded": "Derniers Ajouts", "LabelRecentSeries": "Séries Récentes", + "LabelRecommended": "Recommended", "LabelRegion": "Région", "LabelReleaseDate": "Date de Parution", "LabelRemoveCover": "Supprimer la Couverture", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Mardi", "WeekdayWed": "Mer", "WeekdayWednesday": "Mercredi" -} +} \ No newline at end of file diff --git a/client/strings/hr.json b/client/strings/hr.json index bb144df1..cc001653 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Godina izdavanja", "LabelRecentlyAdded": "Nedavno dodano", "LabelRecentSeries": "Nedavne serije", + "LabelRecommended": "Recommended", "LabelRegion": "Regija", "LabelReleaseDate": "Datum izlaska", "LabelRemoveCover": "Remove cover", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Utorak", "WeekdayWed": "Wed", "WeekdayWednesday": "Srijeda" -} +} \ No newline at end of file diff --git a/client/strings/it.json b/client/strings/it.json index 15f0d9dd..f7d5edc5 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Anno Pubblicazione", "LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentSeries": "Serie Recenti", + "LabelRecommended": "Recommended", "LabelRegion": "Regione", "LabelReleaseDate": "Data Release", "LabelRemoveCover": "Remove cover", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Martedì", "WeekdayWed": "Mer", "WeekdayWednesday": "Mercoledì" -} +} \ No newline at end of file diff --git a/client/strings/pl.json b/client/strings/pl.json index f87a79a3..5edd523f 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -308,6 +308,7 @@ "LabelPublishYear": "Rok publikacji", "LabelRecentlyAdded": "Niedawno dodany", "LabelRecentSeries": "Ostatnie serie", + "LabelRecommended": "Recommended", "LabelRegion": "Region", "LabelReleaseDate": "Data wydania", "LabelRemoveCover": "Remove cover", @@ -640,4 +641,4 @@ "WeekdayTuesday": "Wtorek", "WeekdayWed": "Wed", "WeekdayWednesday": "Środa" -} +} \ No newline at end of file diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 4e594305..8c0e05ea 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -308,6 +308,7 @@ "LabelPublishYear": "发布年份", "LabelRecentlyAdded": "最近添加", "LabelRecentSeries": "最近添加系列", + "LabelRecommended": "Recommended", "LabelRegion": "区域", "LabelReleaseDate": "发布日期", "LabelRemoveCover": "移除封面", @@ -640,4 +641,4 @@ "WeekdayTuesday": "星期二", "WeekdayWed": "Wed", "WeekdayWednesday": "星期三" -} +} \ No newline at end of file diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 41d140f8..93d23466 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -339,6 +339,14 @@ module.exports = { entities: [], category: 'continueSeries' }, + { + id: 'episodes-recently-added', + label: 'Newest Episodes', + labelStringKey: 'LabelNewestEpisodes', + type: 'episode', + entities: [], + category: 'newestEpisodes' + }, { id: 'recently-added', label: 'Recently Added', @@ -347,14 +355,6 @@ module.exports = { entities: [], category: 'newestItems' }, - { - id: 'listen-again', - label: 'Listen Again', - labelStringKey: 'LabelListenAgain', - type: isPodcastLibrary ? 'episode' : mediaType, - entities: [], - category: 'recentlyFinished' - }, { id: 'recent-series', label: 'Recent Series', @@ -363,6 +363,22 @@ module.exports = { entities: [], category: 'newestSeries' }, + { + id: 'recommended', + label: 'Recommended', + labelStringKey: 'LabelRecommended', + type: mediaType, + entities: [], + category: 'recommended' + }, + { + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: isPodcastLibrary ? 'episode' : mediaType, + entities: [], + category: 'recentlyFinished' + }, { id: 'newest-authors', label: 'Newest Authors', @@ -370,22 +386,13 @@ module.exports = { type: 'authors', entities: [], category: 'newestAuthors' - }, - { - id: 'episodes-recently-added', - label: 'Newest Episodes', - labelStringKey: 'LabelNewestEpisodes', - type: 'episode', - entities: [], - category: 'newestEpisodes' } ] - const categories = ['recentlyListened', 'continueSeries', 'newestEpisodes', 'newestItems', 'newestSeries', 'recentlyFinished', 'newestAuthors'] const categoryMap = {} - categories.forEach((cat) => { - categoryMap[cat] = { - category: cat, + shelves.forEach((shelf) => { + categoryMap[shelf.category] = { + category: shelf.category, biggest: 0, smallest: 0, items: [] @@ -395,6 +402,12 @@ module.exports = { const seriesMap = {} const authorMap = {} + // For use with recommended + const topGenresListened = {} + const topAuthorsListened = {} + const topTagsListened = {} + const notStartedBooks = [] + for (const libraryItem of libraryItems) { if (libraryItem.addedAt > categoryMap.newestItems.smallest) { @@ -494,10 +507,28 @@ module.exports = { } else if (libraryItem.isBook) { // Book categories + const mediaProgress = allItemProgress.length ? allItemProgress[0] : null + + // Used for recommended. Tally up most listened to authors/genres/tags + if (mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)) { + libraryItem.media.metadata.authors.forEach((author) => { + topAuthorsListened[author.id] = (topAuthorsListened[author.id] || 0) + 1 + }) + libraryItem.media.metadata.genres.forEach((genre) => { + topGenresListened[genre] = (topGenresListened[genre] || 0) + 1 + }) + libraryItem.media.tags.forEach((tag) => { + topTagsListened[tag] = (topTagsListened[tag] || 0) + 1 + }) + } else { + // Insert in random position to add randomization to equal weighted items + notStartedBooks.splice(Math.floor(Math.random() * (notStartedBooks.length + 1)), 0, libraryItem) + } + // Newest series if (libraryItem.media.metadata.series.length) { for (const librarySeries of libraryItem.media.metadata.series) { - const mediaProgress = allItemProgress.length ? allItemProgress[0] : null + const bookInProgress = mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished) const bookActive = mediaProgress && mediaProgress.inProgress && !mediaProgress.isFinished const libraryItemJson = libraryItem.toJSONMinified() @@ -602,7 +633,6 @@ module.exports = { } // Book listening and finished - var mediaProgress = allItemProgress.length ? allItemProgress[0] : null if (mediaProgress) { // Handle most recently finished if (mediaProgress.isFinished) { @@ -612,7 +642,7 @@ module.exports = { finishedAt: mediaProgress.finishedAt } - var indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt) + const indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt) if (indexToPut >= 0) { categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj) } else { @@ -632,7 +662,7 @@ module.exports = { progressLastUpdate: mediaProgress.lastUpdate } - var indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate) + const indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate) if (indexToPut >= 0) { categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj) } else { // Should only happen when array is < max @@ -652,9 +682,9 @@ module.exports = { // For Continue Series - Find next book in series for series that are in progress for (const seriesId in seriesMap) { - if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) { - seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence) + seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence) + if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) { // take the first book unread with the smallest series sequence // unless the user is already listening to a book from this series const hasActiveBook = seriesMap[seriesId].hasActiveBook @@ -683,6 +713,76 @@ module.exports = { } } + // For recommended + if (!isPodcastLibrary && notStartedBooks.length) { + const genresCount = Object.values(topGenresListened).reduce((a, b) => a + b, 0) + const authorsCount = Object.values(topAuthorsListened).reduce((a, b) => a + b, 0) + const tagsCount = Object.values(topTagsListened).reduce((a, b) => a + b, 0) + + for (const libraryItem of notStartedBooks) { + // dont include books in an unfinished series and books that are not first in an unstarted series + let shouldContinue = !libraryItem.media.metadata.series.length + libraryItem.media.metadata.series.forEach((se) => { + if (seriesMap[se.id]) { + if (seriesMap[se.id].inProgress) { + shouldContinue = false + return + } else if (seriesMap[se.id].books[0].id === libraryItem.id) { + shouldContinue = true + } + } + }) + if (!shouldContinue) { + continue; + } + + let totalWeight = 0 + + if (authorsCount > 0) { + libraryItem.media.metadata.authors.forEach((author) => { + if (topAuthorsListened[author.id]) { + totalWeight += topAuthorsListened[author.id] / authorsCount + } + }) + } + + if (genresCount > 0) { + libraryItem.media.metadata.genres.forEach((genre) => { + if (topGenresListened[genre]) { + totalWeight += topGenresListened[genre] / genresCount + } + }) + } + + if (tagsCount > 0) { + libraryItem.media.tags.forEach((tag) => { + if (topTagsListened[tag]) { + totalWeight += topTagsListened[tag] / tagsCount + } + }) + } + + if (!categoryMap.recommended.smallest || totalWeight > categoryMap.recommended.smallest) { + const libraryItemObj = { + ...libraryItem.toJSONMinified(), + weight: totalWeight + } + + const indexToPut = categoryMap.recommended.items.findIndex(i => totalWeight > i.weight) + if (indexToPut >= 0) { + categoryMap.recommended.items.splice(indexToPut, 0, libraryItemObj) + } else { + categoryMap.recommended.items.push(libraryItemObj) + } + + if (categoryMap.recommended.items.length > maxEntitiesPerShelf) { + categoryMap.recommended.items.pop() + categoryMap.recommended.smallest = categoryMap.recommended.items[categoryMap.recommended.items.length - 1].weight + } + } + } + } + // Sort series books by sequence if (categoryMap.newestSeries.items.length) { for (const seriesItem of categoryMap.newestSeries.items) {