Merge branch 'advplyr:master' into master

This commit is contained in:
Mario 2023-08-22 11:39:13 +02:00 committed by GitHub
commit adafefecd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 5590 additions and 4410 deletions

View File

@ -171,7 +171,7 @@ export default {
}, },
async fetchCategories() { async fetchCategories() {
const categories = await this.$axios const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized2?include=rssfeed,numEpisodesIncomplete`) .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
.then((data) => { .then((data) => {
return data return data
}) })

View File

@ -628,6 +628,11 @@ export default {
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
}, },
async init(bookshelf) { async init(bookshelf) {
if (this.entityName === 'series') {
this.booksPerFetch = 50
} else {
this.booksPerFetch = 100
}
this.checkUpdateSearchParams() this.checkUpdateSearchParams()
this.initSizeData(bookshelf) this.initSizeData(bookshelf)

View File

@ -36,7 +36,7 @@ export default {
return this.narrator?.name || '' return this.narrator?.name || ''
}, },
numBooks() { numBooks() {
return this.narrator?.books?.length || 0 return this.narrator?.numBooks || this.narrator?.books?.length || 0
}, },
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']

View File

@ -103,7 +103,7 @@ export default {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
totalResults() { totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
} }
}, },
methods: { methods: {

View File

@ -13,8 +13,8 @@
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }"> <div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center"> <div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" /> <img v-if="width > 100" src="/Logo.png" class="mb-2" :style="{ height: 40 * sizeMultiplier + 'px' }" />
<p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p> <p class="text-center text-error" :style="{ fontSize: invalidCoverFontSize + 'rem' }">Invalid Cover</p>
</div> </div>
</div> </div>
@ -58,6 +58,9 @@ export default {
sizeMultiplier() { sizeMultiplier() {
return this.width / 120 return this.width / 120
}, },
invalidCoverFontSize() {
return Math.max(this.sizeMultiplier * 0.8, 0.5)
},
placeholderCoverPadding() { placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier return 0.8 * this.sizeMultiplier
}, },

View File

@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<div class="flex px-4"> <div v-if="isBookLibrary" class="flex px-4">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24"> <svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /> <path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg> </svg>
@ -58,26 +58,32 @@ export default {
return {} return {}
}, },
computed: { computed: {
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
},
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalItems() { totalItems() {
return this.libraryStats ? this.libraryStats.totalItems : 0 return this.libraryStats?.totalItems || 0
}, },
totalAuthors() { totalAuthors() {
return this.libraryStats ? this.libraryStats.totalAuthors : 0 return this.libraryStats?.totalAuthors || 0
}, },
numAudioTracks() { numAudioTracks() {
return this.libraryStats ? this.libraryStats.numAudioTracks : 0 return this.libraryStats?.numAudioTracks || 0
}, },
totalDuration() { totalDuration() {
return this.libraryStats ? this.libraryStats.totalDuration : 0 return this.libraryStats?.totalDuration || 0
}, },
totalHours() { totalHours() {
return Math.round(this.totalDuration / (60 * 60)) return Math.round(this.totalDuration / (60 * 60))
}, },
totalSizePretty() { totalSizePretty() {
var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0 var totalSize = this.libraryStats?.totalSize || 0
return this.$bytesPretty(totalSize, 1) return this.$bytesPretty(totalSize, 1)
}, },
totalSizeNum() { totalSizeNum() {

View File

@ -343,6 +343,10 @@ export default {
} }
this.$store.commit('libraries/removeCollection', collection) this.$store.commit('libraries/removeCollection', collection)
}, },
seriesRemoved({ id, libraryId }) {
if (this.currentLibraryId !== libraryId) return
this.$store.commit('libraries/removeSeriesFromFilterData', id)
},
playlistAdded(playlist) { playlistAdded(playlist) {
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
this.$store.commit('libraries/addUpdateUserPlaylist', playlist) this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
@ -442,6 +446,9 @@ export default {
this.socket.on('collection_updated', this.collectionUpdated) this.socket.on('collection_updated', this.collectionUpdated)
this.socket.on('collection_removed', this.collectionRemoved) this.socket.on('collection_removed', this.collectionRemoved)
// Series Listeners
this.socket.on('series_removed', this.seriesRemoved)
// User Playlist Listeners // User Playlist Listeners
this.socket.on('playlist_added', this.playlistAdded) this.socket.on('playlist_added', this.playlistAdded)
this.socket.on('playlist_updated', this.playlistUpdated) this.socket.on('playlist_updated', this.playlistUpdated)

View File

@ -22,7 +22,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class="w-80 my-6 mx-auto"> <div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1> <h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p> <p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
<template v-for="(author, index) in top10Authors"> <template v-for="(author, index) in top10Authors">
@ -114,43 +114,49 @@ export default {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalItems() { totalItems() {
return this.libraryStats ? this.libraryStats.totalItems : 0 return this.libraryStats?.totalItems || 0
}, },
genresWithCount() { genresWithCount() {
return this.libraryStats ? this.libraryStats.genresWithCount : [] return this.libraryStats?.genresWithCount || []
}, },
top5Genres() { top5Genres() {
return this.genresWithCount.slice(0, 5) return this.genresWithCount?.slice(0, 5) || []
}, },
top10LongestItems() { top10LongestItems() {
return this.libraryStats ? this.libraryStats.longestItems || [] : [] return this.libraryStats?.longestItems || []
}, },
longestItemDuration() { longestItemDuration() {
if (!this.top10LongestItems.length) return 0 if (!this.top10LongestItems.length) return 0
return this.top10LongestItems[0].duration return this.top10LongestItems[0].duration
}, },
top10LargestItems() { top10LargestItems() {
return this.libraryStats ? this.libraryStats.largestItems || [] : [] return this.libraryStats?.largestItems || []
}, },
largestItemSize() { largestItemSize() {
if (!this.top10LargestItems.length) return 0 if (!this.top10LargestItems.length) return 0
return this.top10LargestItems[0].size return this.top10LargestItems[0].size
}, },
authorsWithCount() { authorsWithCount() {
return this.libraryStats ? this.libraryStats.authorsWithCount : [] return this.libraryStats?.authorsWithCount || []
}, },
mostUsedAuthorCount() { mostUsedAuthorCount() {
if (!this.authorsWithCount.length) return 0 if (!this.authorsWithCount.length) return 0
return this.authorsWithCount[0].count return this.authorsWithCount[0].count
}, },
top10Authors() { top10Authors() {
return this.authorsWithCount.slice(0, 10) return this.authorsWithCount?.slice(0, 10) || []
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
currentLibraryName() { currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName'] return this.$store.getters['libraries/getCurrentLibraryName']
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
} }
}, },
methods: { methods: {

View File

@ -47,7 +47,7 @@
<div class="py-2"> <div class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1> <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
<table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable"> <table v-if="mediaProgress.length" class="userAudiobooksTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">
<th class="w-16 text-left">{{ $strings.LabelItem }}</th> <th class="w-16 text-left">{{ $strings.LabelItem }}</th>
<th class="text-left"></th> <th class="text-left"></th>
@ -55,19 +55,14 @@
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th> <th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th> <th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr> </tr>
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'"> <tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
<td> <td>
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover v-if="item.coverPath" :width="50" :src="$store.getters['globals/getLibraryItemCoverSrcById'](item.libraryItemId, item.mediaUpdatedAt)" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
<div v-else class="bg-primary flex items-center justify-center text-center text-xs text-gray-400 p-1" :style="{ width: '50px', height: 50 * bookCoverAspectRatio + 'px' }">No Cover</div>
</td> </td>
<td> <td>
<template v-if="item.media && item.media.metadata && item.episode"> <p>{{ item.displayTitle || 'Unknown' }}</p>
<p>{{ item.episode.title || 'Unknown' }}</p> <p v-if="item.displaySubtitle" class="text-white text-opacity-50 text-sm font-sans">{{ item.displaySubtitle }}</p>
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
</template>
<template v-else-if="item.media && item.media.metadata">
<p>{{ item.media.metadata.title || 'Unknown' }}</p>
<p v-if="item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
</template>
</td> </td>
<td class="text-center"> <td class="text-center">
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p> <p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
@ -124,9 +119,6 @@ export default {
mediaProgress() { mediaProgress() {
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate) return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
}, },
mediaProgressWithMedia() {
return this.mediaProgress.filter((mp) => mp.media)
},
totalListeningTime() { totalListeningTime() {
return this.listeningStats.totalTime || 0 return this.listeningStats.totalTime || 0
}, },

View File

@ -234,6 +234,10 @@ export const mutations = {
setNumUserPlaylists(state, numUserPlaylists) { setNumUserPlaylists(state, numUserPlaylists) {
state.numUserPlaylists = numUserPlaylists state.numUserPlaylists = numUserPlaylists
}, },
removeSeriesFromFilterData(state, seriesId) {
if (!seriesId || !state.filterData) return
state.filterData.series = state.filterData.series.filter(se => se.id !== seriesId)
},
updateFilterDataWithItem(state, libraryItem) { updateFilterDataWithItem(state, libraryItem) {
if (!libraryItem || !state.filterData) return if (!libraryItem || !state.filterData) return
if (state.currentLibraryId !== libraryItem.libraryId) return if (state.currentLibraryId !== libraryItem.libraryId) return

View File

@ -32,7 +32,7 @@ class Auth {
await Database.updateServerSettings() await Database.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user // New token secret creation added in v2.1.0 so generate new API tokens for each user
const users = await Database.models.user.getOldUsers() const users = await Database.userModel.getOldUsers()
if (users.length) { if (users.length) {
for (const user of users) { for (const user of users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
@ -100,7 +100,7 @@ class Auth {
return resolve(null) return resolve(null)
} }
const user = await Database.models.user.getUserByIdOrOldId(payload.userId) const user = await Database.userModel.getUserByIdOrOldId(payload.userId)
if (user && user.username === payload.username) { if (user && user.username === payload.username) {
resolve(user) resolve(user)
} else { } else {
@ -116,7 +116,7 @@ class Auth {
* @returns {object} * @returns {object}
*/ */
async getUserLoginResponsePayload(user) { async getUserLoginResponsePayload(user) {
const libraryIds = await Database.models.library.getAllLibraryIds() const libraryIds = await Database.libraryModel.getAllLibraryIds()
return { return {
user: user.toJSONForBrowser(), user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
@ -131,7 +131,7 @@ class Auth {
const username = (req.body.username || '').toLowerCase() const username = (req.body.username || '').toLowerCase()
const password = req.body.password || '' const password = req.body.password || ''
const user = await Database.models.user.getUserByUsername(username) const user = await Database.userModel.getUserByUsername(username)
if (!user?.isActive) { if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
@ -178,7 +178,7 @@ class Auth {
async userChangePassword(req, res) { async userChangePassword(req, res) {
var { password, newPassword } = req.body var { password, newPassword } = req.body
newPassword = newPassword || '' newPassword = newPassword || ''
const matchingUser = await Database.models.user.getUserById(req.user.id) const matchingUser = await Database.userModel.getUserById(req.user.id)
// Only root can have an empty password // Only root can have an empty password
if (matchingUser.type !== 'root' && !newPassword) { if (matchingUser.type !== 'root' && !newPassword) {

View File

@ -34,6 +34,100 @@ class Database {
return this.sequelize?.models || {} return this.sequelize?.models || {}
} }
/** @type {typeof import('./models/User')} */
get userModel() {
return this.models.user
}
/** @type {typeof import('./models/Library')} */
get libraryModel() {
return this.models.library
}
/** @type {typeof import('./models/Author')} */
get authorModel() {
return this.models.author
}
/** @type {typeof import('./models/Series')} */
get seriesModel() {
return this.models.series
}
/** @type {typeof import('./models/Book')} */
get bookModel() {
return this.models.book
}
/** @type {typeof import('./models/BookSeries')} */
get bookSeriesModel() {
return this.models.bookSeries
}
/** @type {typeof import('./models/BookAuthor')} */
get bookAuthorModel() {
return this.models.bookAuthor
}
/** @type {typeof import('./models/Podcast')} */
get podcastModel() {
return this.models.podcast
}
/** @type {typeof import('./models/PodcastEpisode')} */
get podcastEpisodeModel() {
return this.models.podcastEpisode
}
/** @type {typeof import('./models/LibraryItem')} */
get libraryItemModel() {
return this.models.libraryItem
}
/** @type {typeof import('./models/PodcastEpisode')} */
get podcastEpisodeModel() {
return this.models.podcastEpisode
}
/** @type {typeof import('./models/MediaProgress')} */
get mediaProgressModel() {
return this.models.mediaProgress
}
/** @type {typeof import('./models/Collection')} */
get collectionModel() {
return this.models.collection
}
/** @type {typeof import('./models/CollectionBook')} */
get collectionBookModel() {
return this.models.collectionBook
}
/** @type {typeof import('./models/Playlist')} */
get playlistModel() {
return this.models.playlist
}
/** @type {typeof import('./models/PlaylistMediaItem')} */
get playlistMediaItemModel() {
return this.models.playlistMediaItem
}
/** @type {typeof import('./models/Feed')} */
get feedModel() {
return this.models.feed
}
/** @type {typeof import('./models/Feed')} */
get feedEpisodeModel() {
return this.models.feedEpisode
}
/**
* Check if db file exists
* @returns {boolean}
*/
async checkHasDb() { async checkHasDb() {
if (!await fs.pathExists(this.dbPath)) { if (!await fs.pathExists(this.dbPath)) {
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`) Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
@ -42,6 +136,10 @@ class Database {
return true return true
} }
/**
* Connect to db, build models and run migrations
* @param {boolean} [force=false] Used for testing, drops & re-creates all tables
*/
async init(force = false) { async init(force = false) {
this.dbPath = Path.join(global.ConfigPath, 'absdatabase.sqlite') this.dbPath = Path.join(global.ConfigPath, 'absdatabase.sqlite')
@ -58,6 +156,10 @@ class Database {
await this.loadData() await this.loadData()
} }
/**
* Connect to db
* @returns {boolean}
*/
async connect() { async connect() {
Logger.info(`[Database] Initializing db at "${this.dbPath}"`) Logger.info(`[Database] Initializing db at "${this.dbPath}"`)
this.sequelize = new Sequelize({ this.sequelize = new Sequelize({
@ -80,39 +182,45 @@ class Database {
} }
} }
/**
* Disconnect from db
*/
async disconnect() { async disconnect() {
Logger.info(`[Database] Disconnecting sqlite db`) Logger.info(`[Database] Disconnecting sqlite db`)
await this.sequelize.close() await this.sequelize.close()
this.sequelize = null this.sequelize = null
} }
/**
* Reconnect to db and init
*/
async reconnect() { async reconnect() {
Logger.info(`[Database] Reconnecting sqlite db`) Logger.info(`[Database] Reconnecting sqlite db`)
await this.init() await this.init()
} }
buildModels(force = false) { buildModels(force = false) {
require('./models/User')(this.sequelize) require('./models/User').init(this.sequelize)
require('./models/Library')(this.sequelize) require('./models/Library').init(this.sequelize)
require('./models/LibraryFolder')(this.sequelize) require('./models/LibraryFolder').init(this.sequelize)
require('./models/Book')(this.sequelize) require('./models/Book').init(this.sequelize)
require('./models/Podcast')(this.sequelize) require('./models/Podcast').init(this.sequelize)
require('./models/PodcastEpisode')(this.sequelize) require('./models/PodcastEpisode').init(this.sequelize)
require('./models/LibraryItem')(this.sequelize) require('./models/LibraryItem').init(this.sequelize)
require('./models/MediaProgress')(this.sequelize) require('./models/MediaProgress').init(this.sequelize)
require('./models/Series')(this.sequelize) require('./models/Series').init(this.sequelize)
require('./models/BookSeries')(this.sequelize) require('./models/BookSeries').init(this.sequelize)
require('./models/Author').init(this.sequelize) require('./models/Author').init(this.sequelize)
require('./models/BookAuthor')(this.sequelize) require('./models/BookAuthor').init(this.sequelize)
require('./models/Collection')(this.sequelize) require('./models/Collection').init(this.sequelize)
require('./models/CollectionBook')(this.sequelize) require('./models/CollectionBook').init(this.sequelize)
require('./models/Playlist')(this.sequelize) require('./models/Playlist').init(this.sequelize)
require('./models/PlaylistMediaItem')(this.sequelize) require('./models/PlaylistMediaItem').init(this.sequelize)
require('./models/Device')(this.sequelize) require('./models/Device').init(this.sequelize)
require('./models/PlaybackSession')(this.sequelize) require('./models/PlaybackSession').init(this.sequelize)
require('./models/Feed')(this.sequelize) require('./models/Feed').init(this.sequelize)
require('./models/FeedEpisode')(this.sequelize) require('./models/FeedEpisode').init(this.sequelize)
require('./models/Setting')(this.sequelize) require('./models/Setting').init(this.sequelize)
return this.sequelize.sync({ force, alter: false }) return this.sequelize.sync({ force, alter: false })
} }
@ -481,6 +589,88 @@ class Database {
} }
} }
} }
removeSeriesFromFilterData(libraryId, seriesId) {
if (!this.libraryFilterData[libraryId]) return
this.libraryFilterData[libraryId].series = this.libraryFilterData[libraryId].series.filter(se => se.id !== seriesId)
}
addSeriesToFilterData(libraryId, seriesName, seriesId) {
if (!this.libraryFilterData[libraryId]) return
// Check if series is already added
if (this.libraryFilterData[libraryId].series.some(se => se.id === seriesId)) return
this.libraryFilterData[libraryId].series.push({
id: seriesId,
name: seriesName
})
}
removeAuthorFromFilterData(libraryId, authorId) {
if (!this.libraryFilterData[libraryId]) return
this.libraryFilterData[libraryId].authors = this.libraryFilterData[libraryId].authors.filter(au => au.id !== authorId)
}
addAuthorToFilterData(libraryId, authorName, authorId) {
if (!this.libraryFilterData[libraryId]) return
// Check if author is already added
if (this.libraryFilterData[libraryId].authors.some(au => au.id === authorId)) return
this.libraryFilterData[libraryId].authors.push({
id: authorId,
name: authorName
})
}
/**
* Used when updating items to make sure author id exists
* If library filter data is set then use that for check
* otherwise lookup in db
* @param {string} libraryId
* @param {string} authorId
* @returns {Promise<boolean>}
*/
async checkAuthorExists(libraryId, authorId) {
if (!this.libraryFilterData[libraryId]) {
return this.authorModel.checkExistsById(authorId)
}
return this.libraryFilterData[libraryId].authors.some(au => au.id === authorId)
}
/**
* Used when updating items to make sure series id exists
* If library filter data is set then use that for check
* otherwise lookup in db
* @param {string} libraryId
* @param {string} seriesId
* @returns {Promise<boolean>}
*/
async checkSeriesExists(libraryId, seriesId) {
if (!this.libraryFilterData[libraryId]) {
return this.seriesModel.checkExistsById(seriesId)
}
return this.libraryFilterData[libraryId].series.some(se => se.id === seriesId)
}
/**
* Reset numIssues for library
* @param {string} libraryId
*/
async resetLibraryIssuesFilterData(libraryId) {
if (!this.libraryFilterData[libraryId]) return // Do nothing if filter data is not set
this.libraryFilterData[libraryId].numIssues = await this.libraryItemModel.count({
where: {
libraryId,
[Sequelize.Op.or]: [
{
isMissing: true
},
{
isInvalid: true
}
]
}
})
}
} }
module.exports = new Database() module.exports = new Database()

View File

@ -114,10 +114,9 @@ class Server {
await this.backupManager.init() await this.backupManager.init()
await this.logManager.init() await this.logManager.init()
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
await this.rssFeedManager.init() await this.rssFeedManager.init()
const libraries = await Database.models.library.getAllOldLibraries() const libraries = await Database.libraryModel.getAllOldLibraries()
await this.cronManager.init(libraries) await this.cronManager.init(libraries)
if (Database.serverSettings.scannerDisableWatcher) { if (Database.serverSettings.scannerDisableWatcher) {
@ -254,7 +253,7 @@ class Server {
*/ */
async cleanUserData() { async cleanUserData() {
// Get all media progress without an associated media item // Get all media progress without an associated media item
const mediaProgressToRemove = await Database.models.mediaProgress.findAll({ const mediaProgressToRemove = await Database.mediaProgressModel.findAll({
where: { where: {
'$podcastEpisode.id$': null, '$podcastEpisode.id$': null,
'$book.id$': null '$book.id$': null
@ -262,18 +261,18 @@ class Server {
attributes: ['id'], attributes: ['id'],
include: [ include: [
{ {
model: Database.models.book, model: Database.bookModel,
attributes: ['id'] attributes: ['id']
}, },
{ {
model: Database.models.podcastEpisode, model: Database.podcastEpisodeModel,
attributes: ['id'] attributes: ['id']
} }
] ]
}) })
if (mediaProgressToRemove.length) { if (mediaProgressToRemove.length) {
// Remove media progress // Remove media progress
const mediaProgressRemoved = await Database.models.mediaProgress.destroy({ const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
where: { where: {
id: { id: {
[Sequelize.Op.in]: mediaProgressToRemove.map(mp => mp.id) [Sequelize.Op.in]: mediaProgressToRemove.map(mp => mp.id)
@ -286,7 +285,7 @@ class Server {
} }
// Remove series from hide from continue listening that no longer exist // Remove series from hide from continue listening that no longer exist
const users = await Database.models.user.getOldUsers() const users = await Database.userModel.getOldUsers()
for (const _user of users) { for (const _user of users) {
let hasUpdated = false let hasUpdated = false
if (_user.seriesHideFromContinueListening.length) { if (_user.seriesHideFromContinueListening.length) {

View File

@ -21,7 +21,7 @@ class AuthorController {
// Used on author landing page to include library items and items grouped in series // Used on author landing page to include library items and items grouped in series
if (include.includes('items')) { if (include.includes('items')) {
authorJson.libraryItems = await Database.models.libraryItem.getForAuthor(req.author, req.user) authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.user)
if (include.includes('series')) { if (include.includes('series')) {
const seriesMap = {} const seriesMap = {}
@ -96,7 +96,7 @@ class AuthorController {
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
if (existingAuthor) { if (existingAuthor) {
const bookAuthorsToCreate = [] const bookAuthorsToCreate = []
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author) const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor) libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
bookAuthorsToCreate.push({ bookAuthorsToCreate.push({
@ -113,9 +113,11 @@ class AuthorController {
// Remove old author // Remove old author
await Database.removeAuthor(req.author.id) await Database.removeAuthor(req.author.id)
SocketAuthority.emitter('author_removed', req.author.toJSON()) SocketAuthority.emitter('author_removed', req.author.toJSON())
// Update filter data
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
// Send updated num books for merged author // Send updated num books for merged author
const numBooks = await Database.models.libraryItem.getForAuthor(existingAuthor).length const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
res.json({ res.json({
@ -130,7 +132,7 @@ class AuthorController {
if (hasUpdated) { if (hasUpdated) {
req.author.updatedAt = Date.now() req.author.updatedAt = Date.now()
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author) const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
if (authorNameUpdate) { // Update author name on all books if (authorNameUpdate) { // Update author name on all books
itemsWithAuthor.forEach(libraryItem => { itemsWithAuthor.forEach(libraryItem => {
libraryItem.media.metadata.updateAuthor(req.author) libraryItem.media.metadata.updateAuthor(req.author)
@ -202,7 +204,7 @@ class AuthorController {
await Database.updateAuthor(req.author) await Database.updateAuthor(req.author)
const numBooks = await Database.models.libraryItem.getForAuthor(req.author).length const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
} }

View File

@ -22,10 +22,10 @@ class CollectionController {
} }
// Create collection record // Create collection record
await Database.models.collection.createFromOld(newCollection) await Database.collectionModel.createFromOld(newCollection)
// Get library items in collection // Get library items in collection
const libraryItemsInCollection = await Database.models.libraryItem.getForCollection(newCollection) const libraryItemsInCollection = await Database.libraryItemModel.getForCollection(newCollection)
// Create collectionBook records // Create collectionBook records
let order = 1 let order = 1
@ -50,7 +50,7 @@ class CollectionController {
} }
async findAll(req, res) { async findAll(req, res) {
const collectionsExpanded = await Database.models.collection.getOldCollectionsJsonExpanded(req.user) const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user)
res.json({ res.json({
collections: collectionsExpanded collections: collectionsExpanded
}) })
@ -96,8 +96,8 @@ class CollectionController {
if (req.body.books?.length) { if (req.body.books?.length) {
const collectionBooks = await req.collection.getCollectionBooks({ const collectionBooks = await req.collection.getCollectionBooks({
include: { include: {
model: Database.models.book, model: Database.bookModel,
include: Database.models.libraryItem include: Database.libraryItemModel
}, },
order: [['order', 'ASC']] order: [['order', 'ASC']]
}) })
@ -143,7 +143,7 @@ class CollectionController {
* @param {*} res * @param {*} res
*/ */
async addBook(req, res) { async addBook(req, res) {
const libraryItem = await Database.models.libraryItem.getOldById(req.body.id) const libraryItem = await Database.libraryItemModel.getOldById(req.body.id)
if (!libraryItem) { if (!libraryItem) {
return res.status(404).send('Book not found') return res.status(404).send('Book not found')
} }
@ -158,7 +158,7 @@ class CollectionController {
} }
// Create collectionBook record // Create collectionBook record
await Database.models.collectionBook.create({ await Database.collectionBookModel.create({
collectionId: req.collection.id, collectionId: req.collection.id,
bookId: libraryItem.media.id, bookId: libraryItem.media.id,
order: collectionBooks.length + 1 order: collectionBooks.length + 1
@ -176,7 +176,7 @@ class CollectionController {
* @param {*} res * @param {*} res
*/ */
async removeBook(req, res) { async removeBook(req, res) {
const libraryItem = await Database.models.libraryItem.getOldById(req.params.bookId) const libraryItem = await Database.libraryItemModel.getOldById(req.params.bookId)
if (!libraryItem) { if (!libraryItem) {
return res.sendStatus(404) return res.sendStatus(404)
} }
@ -227,14 +227,14 @@ class CollectionController {
} }
// Get library items associated with ids // Get library items associated with ids
const libraryItems = await Database.models.libraryItem.findAll({ const libraryItems = await Database.libraryItemModel.findAll({
where: { where: {
id: { id: {
[Sequelize.Op.in]: bookIdsToAdd [Sequelize.Op.in]: bookIdsToAdd
} }
}, },
include: { include: {
model: Database.models.book model: Database.bookModel
} }
}) })
@ -285,14 +285,14 @@ class CollectionController {
} }
// Get library items associated with ids // Get library items associated with ids
const libraryItems = await Database.models.libraryItem.findAll({ const libraryItems = await Database.libraryItemModel.findAll({
where: { where: {
id: { id: {
[Sequelize.Op.in]: bookIdsToRemove [Sequelize.Op.in]: bookIdsToRemove
} }
}, },
include: { include: {
model: Database.models.book model: Database.bookModel
} }
}) })
@ -327,7 +327,7 @@ class CollectionController {
async middleware(req, res, next) { async middleware(req, res, next) {
if (req.params.id) { if (req.params.id) {
const collection = await Database.models.collection.findByPk(req.params.id) const collection = await Database.collectionModel.findByPk(req.params.id)
if (!collection) { if (!collection) {
return res.status(404).send('Collection not found') return res.status(404).send('Collection not found')
} }

View File

@ -17,7 +17,7 @@ class FileSystemController {
}) })
// Do not include existing mapped library paths in response // Do not include existing mapped library paths in response
const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths() const libraryFoldersPaths = await Database.libraryModelFolder.getAllLibraryFolderPaths()
libraryFoldersPaths.forEach((path) => { libraryFoldersPaths.forEach((path) => {
let dir = path || '' let dir = path || ''
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')

View File

@ -8,6 +8,7 @@ const Library = require('../objects/Library')
const libraryHelpers = require('../utils/libraryHelpers') const libraryHelpers = require('../utils/libraryHelpers')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
const libraryItemFilters = require('../utils/queries/libraryItemFilters') const libraryItemFilters = require('../utils/queries/libraryItemFilters')
const seriesFilters = require('../utils/queries/seriesFilters')
const { sort, createNewSortInstance } = require('../libs/fastSort') const { sort, createNewSortInstance } = require('../libs/fastSort')
const naturalSort = createNewSortInstance({ const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
@ -15,6 +16,8 @@ const naturalSort = createNewSortInstance({
const Database = require('../Database') const Database = require('../Database')
const libraryFilters = require('../utils/queries/libraryFilters') const libraryFilters = require('../utils/queries/libraryFilters')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
const authorFilters = require('../utils/queries/authorFilters')
class LibraryController { class LibraryController {
constructor() { } constructor() { }
@ -48,7 +51,7 @@ class LibraryController {
const library = new Library() const library = new Library()
let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder() let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder()
if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0 if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1 newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
library.setData(newLibraryPayload) library.setData(newLibraryPayload)
@ -67,7 +70,7 @@ class LibraryController {
} }
async findAll(req, res) { async findAll(req, res) {
const libraries = await Database.models.library.getAllOldLibraries() const libraries = await Database.libraryModel.getAllOldLibraries()
const librariesAccessible = req.user.librariesAccessible || [] const librariesAccessible = req.user.librariesAccessible || []
if (librariesAccessible.length) { if (librariesAccessible.length) {
@ -89,7 +92,7 @@ class LibraryController {
return res.json({ return res.json({
filterdata, filterdata,
issues: filterdata.numIssues, issues: filterdata.numIssues,
numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
library: req.library library: req.library
}) })
} }
@ -141,17 +144,17 @@ class LibraryController {
for (const folder of library.folders) { for (const folder of library.folders) {
if (!req.body.folders.some(f => f.id === folder.id)) { if (!req.body.folders.some(f => f.id === folder.id)) {
// Remove library items in folder // Remove library items in folder
const libraryItemsInFolder = await Database.models.libraryItem.findAll({ const libraryItemsInFolder = await Database.libraryItemModel.findAll({
where: { where: {
libraryFolderId: folder.id libraryFolderId: folder.id
}, },
attributes: ['id', 'mediaId', 'mediaType'], attributes: ['id', 'mediaId', 'mediaType'],
include: [ include: [
{ {
model: Database.models.podcast, model: Database.podcastModel,
attributes: ['id'], attributes: ['id'],
include: { include: {
model: Database.models.podcastEpisode, model: Database.podcastEpisodeModel,
attributes: ['id'] attributes: ['id']
} }
} }
@ -188,6 +191,8 @@ class LibraryController {
return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id) return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id)
} }
SocketAuthority.emitter('library_updated', library.toJSON(), userFilter) SocketAuthority.emitter('library_updated', library.toJSON(), userFilter)
await Database.resetLibraryIssuesFilterData(library.id)
} }
return res.json(library.toJSON()) return res.json(library.toJSON())
} }
@ -205,23 +210,23 @@ class LibraryController {
this.watcher.removeLibrary(library) this.watcher.removeLibrary(library)
// Remove collections for library // Remove collections for library
const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id) const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(library.id)
if (numCollectionsRemoved) { if (numCollectionsRemoved) {
Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`) Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
} }
// Remove items in this library // Remove items in this library
const libraryItemsInLibrary = await Database.models.libraryItem.findAll({ const libraryItemsInLibrary = await Database.libraryItemModel.findAll({
where: { where: {
libraryId: library.id libraryId: library.id
}, },
attributes: ['id', 'mediaId', 'mediaType'], attributes: ['id', 'mediaId', 'mediaType'],
include: [ include: [
{ {
model: Database.models.podcast, model: Database.podcastModel,
attributes: ['id'], attributes: ['id'],
include: { include: {
model: Database.models.podcastEpisode, model: Database.podcastEpisodeModel,
attributes: ['id'] attributes: ['id']
} }
} }
@ -243,9 +248,15 @@ class LibraryController {
await Database.removeLibrary(library.id) await Database.removeLibrary(library.id)
// Re-order libraries // Re-order libraries
await Database.models.library.resetDisplayOrder() await Database.libraryModel.resetDisplayOrder()
SocketAuthority.emitter('library_removed', libraryJson) SocketAuthority.emitter('library_removed', libraryJson)
// Remove library filter data
if (Database.libraryFilterData[library.id]) {
delete Database.libraryFilterData[library.id]
}
return res.json(libraryJson) return res.json(libraryJson)
} }
@ -267,7 +278,7 @@ class LibraryController {
} }
payload.offset = payload.page * payload.limit payload.offset = payload.page * payload.limit
const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library, req.user, payload) const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload)
payload.results = libraryItems payload.results = libraryItems
payload.total = count payload.total = count
@ -471,12 +482,13 @@ class LibraryController {
/** /**
* DELETE: /libraries/:id/issues * DELETE: /libraries/:id/issues
* Remove all library items missing or invalid * Remove all library items missing or invalid
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async removeLibraryItemsWithIssues(req, res) { async removeLibraryItemsWithIssues(req, res) {
const libraryItemsWithIssues = await Database.models.libraryItem.findAll({ const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
where: { where: {
libraryId: req.library.id,
[Sequelize.Op.or]: [ [Sequelize.Op.or]: [
{ {
isMissing: true isMissing: true
@ -489,10 +501,10 @@ class LibraryController {
attributes: ['id', 'mediaId', 'mediaType'], attributes: ['id', 'mediaId', 'mediaType'],
include: [ include: [
{ {
model: Database.models.podcast, model: Database.podcastModel,
attributes: ['id'], attributes: ['id'],
include: { include: {
model: Database.models.podcastEpisode, model: Database.podcastEpisodeModel,
attributes: ['id'] attributes: ['id']
} }
} }
@ -507,7 +519,7 @@ class LibraryController {
Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`) Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`)
for (const libraryItem of libraryItemsWithIssues) { for (const libraryItem of libraryItemsWithIssues) {
let mediaItemIds = [] let mediaItemIds = []
if (library.isPodcast) { if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id) mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id)
} else { } else {
mediaItemIds.push(libraryItem.mediaId) mediaItemIds.push(libraryItem.mediaId)
@ -516,6 +528,11 @@ class LibraryController {
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds) await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
} }
// Set numIssues to 0 for library filter data
if (Database.libraryFilterData[req.library.id]) {
Database.libraryFilterData[req.library.id].numIssues = 0
}
res.sendStatus(200) res.sendStatus(200)
} }
@ -523,12 +540,10 @@ class LibraryController {
* GET: /api/libraries/:id/series * GET: /api/libraries/:id/series
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
* *
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async getAllSeriesForLibrary(req, res) { async getAllSeriesForLibrary(req, res) {
const libraryItems = req.libraryItems
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
const payload = { const payload = {
@ -543,45 +558,10 @@ class LibraryController {
include: include.join(',') include: include.join(',')
} }
let series = libraryHelpers.getSeriesFromBooks(libraryItems, Database.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries) const offset = payload.page * payload.limit
const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset)
const direction = payload.sortDesc ? 'desc' : 'asc'
series = naturalSort(series).by([
{
[direction]: (se) => {
if (payload.sortBy === 'numBooks') {
return se.books.length
} else if (payload.sortBy === 'totalDuration') {
return se.totalDuration
} else if (payload.sortBy === 'addedAt') {
return se.addedAt
} else if (payload.sortBy === 'lastBookUpdated') {
return Math.max(...(se.books).map(x => x.updatedAt), 0)
} else if (payload.sortBy === 'lastBookAdded') {
return Math.max(...(se.books).map(x => x.addedAt), 0)
} else { // sort by name
return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
}
}
}
])
payload.total = series.length
if (payload.limit) {
const startIndex = payload.page * payload.limit
series = series.slice(startIndex, startIndex + payload.limit)
}
// add rssFeed when "include=rssfeed" is in query string
if (include.includes('rssfeed')) {
series = await Promise.all(series.map(async (se) => {
const feedData = await this.rssFeedManager.findFeedForEntityId(se.id)
se.rssFeed = feedData?.toJSONMinified() || null
return se
}))
}
payload.total = count
payload.results = series payload.results = series
res.json(payload) res.json(payload)
} }
@ -644,7 +624,7 @@ class LibraryController {
} }
// TODO: Create paginated queries // TODO: Create paginated queries
let collections = await Database.models.collection.getOldCollectionsJsonExpanded(req.user, req.library.id, include) let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user, req.library.id, include)
payload.total = collections.length payload.total = collections.length
@ -664,7 +644,7 @@ class LibraryController {
* @param {*} res * @param {*} res
*/ */
async getUserPlaylistsForLibrary(req, res) { async getUserPlaylistsForLibrary(req, res) {
let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id) let playlistsForUser = await Database.playlistModel.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded())) playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded()))
const payload = { const payload = {
@ -685,8 +665,8 @@ class LibraryController {
/** /**
* GET: /api/libraries/:id/filterdata * GET: /api/libraries/:id/filterdata
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async getLibraryFilterData(req, res) { async getLibraryFilterData(req, res) {
const filterData = await libraryFilters.getFilterData(req.library) const filterData = await libraryFilters.getFilterData(req.library)
@ -694,44 +674,30 @@ class LibraryController {
} }
/** /**
* GET: /api/libraries/:id/personalized2 * GET: /api/libraries/:id/personalized
* TODO: new endpoint * Home page shelves
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async getUserPersonalizedShelves(req, res) { async getUserPersonalizedShelves(req, res) {
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
const shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf)
res.json(shelves) res.json(shelves)
} }
/**
* GET: /api/libraries/:id/personalized
* TODO: remove after personalized2 is ready
* @param {*} req
* @param {*} res
*/
async getLibraryUserPersonalizedOptimal(req, res) {
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
const categories = await libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include)
res.json(categories)
}
/** /**
* POST: /api/libraries/order * POST: /api/libraries/order
* Change the display order of libraries * Change the display order of libraries
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async reorder(req, res) { async reorder(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user) Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
return res.sendStatus(403) return res.sendStatus(403)
} }
const libraries = await Database.models.library.getAllOldLibraries() const libraries = await Database.libraryModel.getAllOldLibraries()
const orderdata = req.body const orderdata = req.body
let hasUpdates = false let hasUpdates = false
@ -759,99 +725,62 @@ class LibraryController {
}) })
} }
// GET: Global library search /**
search(req, res) { * GET: /api/libraries/:id/search
* Search library items with query
* ?q=search
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async search(req, res) {
if (!req.query.q) { if (!req.query.q) {
return res.status(400).send('No query string') return res.status(400).send('No query string')
} }
const libraryItems = req.libraryItems const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
const maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 const query = req.query.q.trim().toLowerCase()
const itemMatches = [] const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
const authorMatches = {} res.json(matches)
const narratorMatches = {}
const seriesMatches = {}
const tagMatches = {}
libraryItems.forEach((li) => {
const queryResult = li.searchQuery(req.query.q)
if (queryResult.matchKey) {
itemMatches.push({
libraryItem: li.toJSONExpanded(),
matchKey: queryResult.matchKey,
matchText: queryResult.matchText
})
}
if (queryResult.series?.length) {
queryResult.series.forEach((se) => {
if (!seriesMatches[se.id]) {
const _series = Database.series.find(_se => _se.id === se.id)
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
} else {
seriesMatches[se.id].books.push(li.toJSON())
}
})
}
if (queryResult.authors?.length) {
queryResult.authors.forEach((au) => {
if (!authorMatches[au.id]) {
const _author = Database.authors.find(_au => _au.id === au.id)
if (_author) {
authorMatches[au.id] = _author.toJSON()
authorMatches[au.id].numBooks = 1
}
} else {
authorMatches[au.id].numBooks++
}
})
}
if (queryResult.tags?.length) {
queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) {
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
} else {
tagMatches[tag].books.push(li.toJSON())
}
})
}
if (queryResult.narrators?.length) {
queryResult.narrators.forEach((narrator) => {
if (!narratorMatches[narrator]) {
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
} else {
narratorMatches[narrator].books.push(li.toJSON())
}
})
}
})
const itemKey = req.library.mediaType
const results = {
[itemKey]: itemMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults),
narrators: Object.values(narratorMatches).slice(0, maxResults)
}
res.json(results)
} }
/**
* GET: /api/libraries/:id/stats
* Get stats for library
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async stats(req, res) { async stats(req, res) {
var libraryItems = req.libraryItems const stats = {
var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems) largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems) }
var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
var sizeStats = libraryHelpers.getItemSizeStats(libraryItems) if (req.library.isBook) {
var stats = { const authors = await authorFilters.getAuthorsWithCount(req.library.id)
totalItems: libraryItems.length, const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
totalAuthors: Object.keys(authorsWithCount).length, const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
totalGenres: Object.keys(genresWithCount).length, const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)
totalDuration: durationStats.totalDuration,
longestItems: durationStats.longestItems, stats.totalAuthors = authors.length
numAudioTracks: durationStats.numAudioTracks, stats.authorsWithCount = authors
totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems), stats.totalGenres = genres.length
largestItems: sizeStats.largestItems, stats.genresWithCount = genres
authorsWithCount, stats.totalItems = bookStats.totalItems
genresWithCount stats.longestItems = longestBooks
stats.totalSize = bookStats.totalSize
stats.totalDuration = bookStats.totalDuration
stats.numAudioTracks = bookStats.numAudioFiles
} else {
const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
const longestPodcasts = await libraryItemsPodcastFilters.getLongestPodcasts(req.library.id, 10)
stats.totalGenres = genres.length
stats.genresWithCount = genres
stats.totalItems = podcastStats.totalItems
stats.longestItems = longestPodcasts
stats.totalSize = podcastStats.totalSize
stats.totalDuration = podcastStats.totalDuration
stats.numAudioTracks = podcastStats.numAudioFiles
} }
res.json(stats) res.json(stats)
} }
@ -859,18 +788,18 @@ class LibraryController {
/** /**
* GET: /api/libraries/:id/authors * GET: /api/libraries/:id/authors
* Get authors for library * Get authors for library
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
*/ */
async getAuthors(req, res) { async getAuthors(req, res) {
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user) const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
const authors = await Database.models.author.findAll({ const authors = await Database.authorModel.findAll({
where: { where: {
libraryId: req.library.id libraryId: req.library.id
}, },
replacements, replacements,
include: { include: {
model: Database.models.book, model: Database.bookModel,
attributes: ['id', 'tags', 'explicit'], attributes: ['id', 'tags', 'explicit'],
where: bookWhere, where: bookWhere,
required: true, required: true,
@ -903,12 +832,12 @@ class LibraryController {
*/ */
async getNarrators(req, res) { async getNarrators(req, res) {
// Get all books with narrators // Get all books with narrators
const booksWithNarrators = await Database.models.book.findAll({ const booksWithNarrators = await Database.bookModel.findAll({
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('narrators')), { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('narrators')), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
}), }),
include: { include: {
model: Database.models.libraryItem, model: Database.libraryItemModel,
attributes: ['id', 'libraryId'], attributes: ['id', 'libraryId'],
where: { where: {
libraryId: req.library.id libraryId: req.library.id
@ -975,7 +904,7 @@ class LibraryController {
await libraryItem.media.update({ await libraryItem.media.update({
narrators: libraryItem.media.narrators narrators: libraryItem.media.narrators
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
itemsUpdated.push(oldLibraryItem) itemsUpdated.push(oldLibraryItem)
} }
@ -1015,7 +944,7 @@ class LibraryController {
await libraryItem.media.update({ await libraryItem.media.update({
narrators: libraryItem.media.narrators narrators: libraryItem.media.narrators
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
itemsUpdated.push(oldLibraryItem) itemsUpdated.push(oldLibraryItem)
} }
@ -1048,10 +977,16 @@ class LibraryController {
} }
res.sendStatus(200) res.sendStatus(200)
await this.scanner.scan(req.library, options) await this.scanner.scan(req.library, options)
await Database.resetLibraryIssuesFilterData(req.library.id)
Logger.info('[LibraryController] Scan complete') Logger.info('[LibraryController] Scan complete')
} }
// GET: api/libraries/:id/recent-episode /**
* GET: /api/libraries/:id/recent-episodes
* Used for latest page
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async getRecentEpisodes(req, res) { async getRecentEpisodes(req, res) {
if (!req.library.isPodcast) { if (!req.library.isPodcast) {
return res.sendStatus(404) return res.sendStatus(404)
@ -1059,40 +994,37 @@ class LibraryController {
const payload = { const payload = {
episodes: [], episodes: [],
total: 0,
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
} }
var allUnfinishedEpisodes = [] const offset = payload.page * payload.limit
for (const libraryItem of req.libraryItems) { payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, req.library, payload.limit, offset)
const unfinishedEpisodes = libraryItem.media.episodes.filter(ep => {
const userProgress = req.user.getMediaProgress(libraryItem.id, ep.id)
return !userProgress || !userProgress.isFinished
}).map(_ep => {
const ep = _ep.toJSONExpanded()
ep.podcast = libraryItem.media.toJSONMinified()
ep.libraryItemId = libraryItem.id
ep.libraryId = libraryItem.libraryId
return ep
})
allUnfinishedEpisodes.push(...unfinishedEpisodes)
}
payload.total = allUnfinishedEpisodes.length
allUnfinishedEpisodes = sort(allUnfinishedEpisodes).desc(ep => ep.publishedAt)
if (payload.limit) {
var startIndex = payload.page * payload.limit
allUnfinishedEpisodes = allUnfinishedEpisodes.slice(startIndex, startIndex + payload.limit)
}
payload.episodes = allUnfinishedEpisodes
res.json(payload) res.json(payload)
} }
getOPMLFile(req, res) { /**
const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems) * GET: /api/libraries/:id/opml
* Get OPML file for a podcast library
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async getOPMLFile(req, res) {
const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.user)
const podcasts = await Database.podcastModel.findAll({
attributes: ['id', 'feedURL', 'title', 'description', 'itunesPageURL', 'language'],
where: userPermissionPodcastWhere.podcastWhere,
replacements: userPermissionPodcastWhere.replacements,
include: {
model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId: req.library.id
}
}
})
const opmlText = this.podcastManager.generateOPMLFileText(podcasts)
res.type('application/xml') res.type('application/xml')
res.send(opmlText) res.send(opmlText)
} }
@ -1109,7 +1041,7 @@ class LibraryController {
return res.sendStatus(403) return res.sendStatus(403)
} }
const library = await Database.models.library.getOldById(req.params.id) const library = await Database.libraryModel.getOldById(req.params.id)
if (!library) { if (!library) {
return res.status(404).send('Library not found') return res.status(404).send('Library not found')
} }
@ -1122,9 +1054,9 @@ class LibraryController {
/** /**
* Middleware that is not using libraryItems from memory * Middleware that is not using libraryItems from memory
* @param {*} req * @param {import('express').Request} req
* @param {*} res * @param {import('express').Response} res
* @param {*} next * @param {import('express').NextFunction} next
*/ */
async middlewareNew(req, res, next) { async middlewareNew(req, res, next) {
if (!req.user.checkCanAccessLibrary(req.params.id)) { if (!req.user.checkCanAccessLibrary(req.params.id)) {
@ -1132,7 +1064,7 @@ class LibraryController {
return res.sendStatus(403) return res.sendStatus(403)
} }
const library = await Database.models.library.getOldById(req.params.id) const library = await Database.libraryModel.getOldById(req.params.id)
if (!library) { if (!library) {
return res.status(404).send('Library not found') return res.status(404).send('Library not found')
} }

View File

@ -78,6 +78,7 @@ class LibraryItemController {
Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error) Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error)
}) })
} }
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.sendStatus(200) res.sendStatus(200)
} }
@ -124,7 +125,7 @@ class LibraryItemController {
// Book specific - Get all series being removed from this item // Book specific - Get all series being removed from this item
let seriesRemoved = [] let seriesRemoved = []
if (libraryItem.isBook && mediaPayload.metadata?.series) { if (libraryItem.isBook && mediaPayload.metadata?.series) {
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id) const seriesIdsInUpdate = mediaPayload.metadata.series?.map(se => se.id) || []
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id)) seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
} }
@ -135,7 +136,7 @@ class LibraryItemController {
if (seriesRemoved.length) { if (seriesRemoved.length) {
// Check remove empty series // Check remove empty series
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`) Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
await this.checkRemoveEmptySeries(seriesRemoved) await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
} }
if (isPodcastAutoDownloadUpdated) { if (isPodcastAutoDownloadUpdated) {
@ -313,7 +314,7 @@ class LibraryItemController {
return res.status(400).send('Invalid request body') return res.status(400).send('Invalid request body')
} }
const itemsToDelete = await Database.models.libraryItem.getAllOldLibraryItems({ const itemsToDelete = await Database.libraryItemModel.getAllOldLibraryItems({
id: libraryItemIds id: libraryItemIds
}) })
@ -332,6 +333,8 @@ class LibraryItemController {
}) })
} }
} }
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.sendStatus(200) res.sendStatus(200)
} }
@ -346,13 +349,26 @@ class LibraryItemController {
for (const updatePayload of updatePayloads) { for (const updatePayload of updatePayloads) {
const mediaPayload = updatePayload.mediaPayload const mediaPayload = updatePayload.mediaPayload
const libraryItem = await Database.models.libraryItem.getOldById(updatePayload.id) const libraryItem = await Database.libraryItemModel.getOldById(updatePayload.id)
if (!libraryItem) return null if (!libraryItem) return null
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId) await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
let seriesRemoved = []
if (libraryItem.isBook && mediaPayload.metadata?.series) {
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
}
if (libraryItem.media.update(mediaPayload)) { if (libraryItem.media.update(mediaPayload)) {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
if (seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
}
await Database.updateLibraryItem(libraryItem) await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
itemsUpdated++ itemsUpdated++
@ -371,7 +387,7 @@ class LibraryItemController {
if (!libraryItemIds.length) { if (!libraryItemIds.length) {
return res.status(403).send('Invalid payload') return res.status(403).send('Invalid payload')
} }
const libraryItems = await Database.models.libraryItem.getAllOldLibraryItems({ const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({
id: libraryItemIds id: libraryItemIds
}) })
res.json({ res.json({
@ -443,9 +459,11 @@ class LibraryItemController {
await this.scanner.scanLibraryItemByRequest(libraryItem) await this.scanner.scanLibraryItemByRequest(libraryItem)
} }
} }
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
} }
// POST: api/items/:id/scan (admin) // POST: api/items/:id/scan
async scan(req, res) { async scan(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user) Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user)
@ -458,6 +476,7 @@ class LibraryItemController {
} }
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem) const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.json({ res.json({
result: Object.keys(ScanResult).find(key => ScanResult[key] == result) result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
}) })
@ -681,7 +700,7 @@ class LibraryItemController {
} }
async middleware(req, res, next) { async middleware(req, res, next) {
req.libraryItem = await Database.models.libraryItem.getOldById(req.params.id) req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!req.libraryItem?.media) return res.sendStatus(404) if (!req.libraryItem?.media) return res.sendStatus(404)
// Check user can access this library item // Check user can access this library item

View File

@ -59,7 +59,7 @@ class MeController {
// PATCH: api/me/progress/:id // PATCH: api/me/progress/:id
async createUpdateMediaProgress(req, res) { async createUpdateMediaProgress(req, res) {
const libraryItem = await Database.models.libraryItem.getOldById(req.params.id) const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) { if (!libraryItem) {
return res.status(404).send('Item not found') return res.status(404).send('Item not found')
} }
@ -75,7 +75,7 @@ class MeController {
// PATCH: api/me/progress/:id/:episodeId // PATCH: api/me/progress/:id/:episodeId
async createUpdateEpisodeMediaProgress(req, res) { async createUpdateEpisodeMediaProgress(req, res) {
const episodeId = req.params.episodeId const episodeId = req.params.episodeId
const libraryItem = await Database.models.libraryItem.getOldById(req.params.id) const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) { if (!libraryItem) {
return res.status(404).send('Item not found') return res.status(404).send('Item not found')
} }
@ -101,7 +101,7 @@ class MeController {
let shouldUpdate = false let shouldUpdate = false
for (const itemProgress of itemProgressPayloads) { for (const itemProgress of itemProgressPayloads) {
const libraryItem = await Database.models.libraryItem.getOldById(itemProgress.libraryItemId) const libraryItem = await Database.libraryItemModel.getOldById(itemProgress.libraryItemId)
if (libraryItem) { if (libraryItem) {
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) { if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId) const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
@ -122,7 +122,7 @@ class MeController {
// POST: api/me/item/:id/bookmark // POST: api/me/item/:id/bookmark
async createBookmark(req, res) { async createBookmark(req, res) {
if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const { time, title } = req.body const { time, title } = req.body
const bookmark = req.user.createBookmark(req.params.id, time, title) const bookmark = req.user.createBookmark(req.params.id, time, title)
@ -133,7 +133,7 @@ class MeController {
// PATCH: api/me/item/:id/bookmark // PATCH: api/me/item/:id/bookmark
async updateBookmark(req, res) { async updateBookmark(req, res) {
if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const { time, title } = req.body const { time, title } = req.body
if (!req.user.findBookmark(req.params.id, time)) { if (!req.user.findBookmark(req.params.id, time)) {
@ -151,7 +151,7 @@ class MeController {
// DELETE: api/me/item/:id/bookmark/:time // DELETE: api/me/item/:id/bookmark/:time
async removeBookmark(req, res) { async removeBookmark(req, res) {
if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const time = Number(req.params.time) const time = Number(req.params.time)
if (isNaN(time)) return res.sendStatus(500) if (isNaN(time)) return res.sendStatus(500)

View File

@ -38,7 +38,7 @@ class MiscController {
const libraryId = req.body.library const libraryId = req.body.library
const folderId = req.body.folder const folderId = req.body.folder
const library = await Database.models.library.getOldById(libraryId) const library = await Database.libraryModel.getOldById(libraryId)
if (!library) { if (!library) {
return res.status(404).send(`Library not found with id ${libraryId}`) return res.status(404).send(`Library not found with id ${libraryId}`)
} }
@ -177,7 +177,7 @@ class MiscController {
} }
const tags = [] const tags = []
const books = await Database.models.book.findAll({ const books = await Database.bookModel.findAll({
attributes: ['tags'], attributes: ['tags'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
@ -189,7 +189,7 @@ class MiscController {
} }
} }
const podcasts = await Database.models.podcast.findAll({ const podcasts = await Database.podcastModel.findAll({
attributes: ['tags'], attributes: ['tags'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
@ -248,7 +248,7 @@ class MiscController {
await libraryItem.media.update({ await libraryItem.media.update({
tags: libraryItem.media.tags tags: libraryItem.media.tags
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
@ -289,7 +289,7 @@ class MiscController {
await libraryItem.media.update({ await libraryItem.media.update({
tags: libraryItem.media.tags tags: libraryItem.media.tags
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
@ -311,7 +311,7 @@ class MiscController {
return res.sendStatus(404) return res.sendStatus(404)
} }
const genres = [] const genres = []
const books = await Database.models.book.findAll({ const books = await Database.bookModel.findAll({
attributes: ['genres'], attributes: ['genres'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
@ -323,7 +323,7 @@ class MiscController {
} }
} }
const podcasts = await Database.models.podcast.findAll({ const podcasts = await Database.podcastModel.findAll({
attributes: ['genres'], attributes: ['genres'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
@ -382,7 +382,7 @@ class MiscController {
await libraryItem.media.update({ await libraryItem.media.update({
genres: libraryItem.media.genres genres: libraryItem.media.genres
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
@ -423,7 +423,7 @@ class MiscController {
await libraryItem.media.update({ await libraryItem.media.update({
genres: libraryItem.media.genres genres: libraryItem.media.genres
}) })
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem) const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }

View File

@ -22,11 +22,11 @@ class PlaylistController {
} }
// Create Playlist record // Create Playlist record
const newPlaylist = await Database.models.playlist.createFromOld(oldPlaylist) const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
// Lookup all library items in playlist // Lookup all library items in playlist
const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId).filter(i => i) const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId).filter(i => i)
const libraryItemsInPlaylist = await Database.models.libraryItem.findAll({ const libraryItemsInPlaylist = await Database.libraryItemModel.findAll({
where: { where: {
id: libraryItemIds id: libraryItemIds
} }
@ -62,7 +62,7 @@ class PlaylistController {
* @param {*} res * @param {*} res
*/ */
async findAllForUser(req, res) { async findAllForUser(req, res) {
const playlistsForUser = await Database.models.playlist.findAll({ const playlistsForUser = await Database.playlistModel.findAll({
where: { where: {
userId: req.user.id userId: req.user.id
} }
@ -106,7 +106,7 @@ class PlaylistController {
// If array of items is passed in then update order of playlist media items // If array of items is passed in then update order of playlist media items
const libraryItemIds = req.body.items?.map(i => i.libraryItemId).filter(i => i) || [] const libraryItemIds = req.body.items?.map(i => i.libraryItemId).filter(i => i) || []
if (libraryItemIds.length) { if (libraryItemIds.length) {
const libraryItems = await Database.models.libraryItem.findAll({ const libraryItems = await Database.libraryItemModel.findAll({
where: { where: {
id: libraryItemIds id: libraryItemIds
} }
@ -173,14 +173,14 @@ class PlaylistController {
* @param {*} res * @param {*} res
*/ */
async addItem(req, res) { async addItem(req, res) {
const oldPlaylist = await Database.models.playlist.getById(req.playlist.id) const oldPlaylist = await Database.playlistModel.getById(req.playlist.id)
const itemToAdd = req.body const itemToAdd = req.body
if (!itemToAdd.libraryItemId) { if (!itemToAdd.libraryItemId) {
return res.status(400).send('Request body has no libraryItemId') return res.status(400).send('Request body has no libraryItemId')
} }
const libraryItem = await Database.models.libraryItem.getOldById(itemToAdd.libraryItemId) const libraryItem = await Database.libraryItemModel.getOldById(itemToAdd.libraryItemId)
if (!libraryItem) { if (!libraryItem) {
return res.status(400).send('Library item not found') return res.status(400).send('Library item not found')
} }
@ -217,7 +217,7 @@ class PlaylistController {
* @param {*} res * @param {*} res
*/ */
async removeItem(req, res) { async removeItem(req, res) {
const oldLibraryItem = await Database.models.libraryItem.getOldById(req.params.libraryItemId) const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId)
if (!oldLibraryItem) { if (!oldLibraryItem) {
return res.status(404).send('Library item not found') return res.status(404).send('Library item not found')
} }
@ -281,7 +281,7 @@ class PlaylistController {
} }
// Find all library items // Find all library items
const libraryItems = await Database.models.libraryItem.findAll({ const libraryItems = await Database.libraryItemModel.findAll({
where: { where: {
id: libraryItemIds id: libraryItemIds
} }
@ -345,7 +345,7 @@ class PlaylistController {
} }
// Find all library items // Find all library items
const libraryItems = await Database.models.libraryItem.findAll({ const libraryItems = await Database.libraryItemModel.findAll({
where: { where: {
id: libraryItemIds id: libraryItemIds
} }
@ -391,7 +391,7 @@ class PlaylistController {
* @param {*} res * @param {*} res
*/ */
async createFromCollection(req, res) { async createFromCollection(req, res) {
const collection = await Database.models.collection.findByPk(req.params.collectionId) const collection = await Database.collectionModel.findByPk(req.params.collectionId)
if (!collection) { if (!collection) {
return res.status(404).send('Collection not found') return res.status(404).send('Collection not found')
} }
@ -416,7 +416,7 @@ class PlaylistController {
}) })
// Create Playlist record // Create Playlist record
const newPlaylist = await Database.models.playlist.createFromOld(oldPlaylist) const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
// Create PlaylistMediaItem records // Create PlaylistMediaItem records
const mediaItemsToAdd = [] const mediaItemsToAdd = []
@ -438,7 +438,7 @@ class PlaylistController {
async middleware(req, res, next) { async middleware(req, res, next) {
if (req.params.id) { if (req.params.id) {
const playlist = await Database.models.playlist.findByPk(req.params.id) const playlist = await Database.playlistModel.findByPk(req.params.id)
if (!playlist) { if (!playlist) {
return res.status(404).send('Playlist not found') return res.status(404).send('Playlist not found')
} }

View File

@ -19,7 +19,7 @@ class PodcastController {
} }
const payload = req.body const payload = req.body
const library = await Database.models.library.getOldById(payload.libraryId) const library = await Database.libraryModel.getOldById(payload.libraryId)
if (!library) { if (!library) {
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
return res.status(404).send('Library not found') return res.status(404).send('Library not found')
@ -34,7 +34,7 @@ class PodcastController {
const podcastPath = filePathToPOSIX(payload.path) const podcastPath = filePathToPOSIX(payload.path)
// Check if a library item with this podcast folder exists already // Check if a library item with this podcast folder exists already
const existingLibraryItem = (await Database.models.libraryItem.count({ const existingLibraryItem = (await Database.libraryItemModel.count({
where: { where: {
path: podcastPath path: podcastPath
} }
@ -272,13 +272,13 @@ class PodcastController {
} }
// Update/remove playlists that had this podcast episode // Update/remove playlists that had this podcast episode
const playlistMediaItems = await Database.models.playlistMediaItem.findAll({ const playlistMediaItems = await Database.playlistMediaItemModel.findAll({
where: { where: {
mediaItemId: episodeId mediaItemId: episodeId
}, },
include: { include: {
model: Database.models.playlist, model: Database.playlistModel,
include: Database.models.playlistMediaItem include: Database.playlistMediaItemModel
} }
}) })
for (const pmi of playlistMediaItems) { for (const pmi of playlistMediaItems) {
@ -297,7 +297,7 @@ class PodcastController {
} }
// Remove media progress for this episode // Remove media progress for this episode
const mediaProgressRemoved = await Database.models.mediaProgress.destroy({ const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
where: { where: {
mediaItemId: episode.id mediaItemId: episode.id
} }
@ -312,7 +312,7 @@ class PodcastController {
} }
async middleware(req, res, next) { async middleware(req, res, next) {
const item = await Database.models.libraryItem.getOldById(req.params.id) const item = await Database.libraryItemModel.getOldById(req.params.id)
if (!item?.media) return res.sendStatus(404) if (!item?.media) return res.sendStatus(404)
if (!item.isPodcast) { if (!item.isPodcast) {

View File

@ -9,7 +9,7 @@ class RSSFeedController {
async openRSSFeedForItem(req, res) { async openRSSFeedForItem(req, res) {
const options = req.body || {} const options = req.body || {}
const item = await Database.models.libraryItem.getOldById(req.params.itemId) const item = await Database.libraryItemModel.getOldById(req.params.itemId)
if (!item) return res.sendStatus(404) if (!item) return res.sendStatus(404)
// Check user can access this library item // Check user can access this library item
@ -46,7 +46,7 @@ class RSSFeedController {
async openRSSFeedForCollection(req, res) { async openRSSFeedForCollection(req, res) {
const options = req.body || {} const options = req.body || {}
const collection = await Database.models.collection.findByPk(req.params.collectionId) const collection = await Database.collectionModel.findByPk(req.params.collectionId)
if (!collection) return res.sendStatus(404) if (!collection) return res.sendStatus(404)
// Check request body options exist // Check request body options exist

View File

@ -49,7 +49,7 @@ class SessionController {
return res.sendStatus(404) return res.sendStatus(404)
} }
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects() const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects()
const openSessions = this.playbackSessionManager.sessions.map(se => { const openSessions = this.playbackSessionManager.sessions.map(se => {
return { return {
...se.toJSON(), ...se.toJSON(),

View File

@ -106,7 +106,7 @@ class ToolsController {
} }
if (req.params.id) { if (req.params.id) {
const item = await Database.models.libraryItem.getOldById(req.params.id) const item = await Database.libraryItemModel.getOldById(req.params.id)
if (!item?.media) return res.sendStatus(404) if (!item?.media) return res.sendStatus(404)
// Check user can access this library item // Check user can access this library item

View File

@ -17,7 +17,7 @@ class UserController {
const includes = (req.query.include || '').split(',').map(i => i.trim()) const includes = (req.query.include || '').split(',').map(i => i.trim())
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks // Minimal toJSONForBrowser does not include mediaProgress and bookmarks
const allUsers = await Database.models.user.getOldUsers() const allUsers = await Database.userModel.getOldUsers()
const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true)) const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
if (includes.includes('latestSession')) { if (includes.includes('latestSession')) {
@ -32,20 +32,67 @@ class UserController {
}) })
} }
/**
* GET: /api/users/:id
* Get a single user toJSONForBrowser
* Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async findOne(req, res) { async findOne(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user) Logger.error('User other than admin attempting to get user', req.user)
return res.sendStatus(403) return res.sendStatus(403)
} }
res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot)) // Get user media progress with associated mediaItem
const mediaProgresses = await Database.mediaProgressModel.findAll({
where: {
userId: req.reqUser.id
},
include: [
{
model: Database.bookModel,
attributes: ['id', 'title', 'coverPath', 'updatedAt']
},
{
model: Database.podcastEpisodeModel,
attributes: ['id', 'title'],
include: {
model: Database.podcastModel,
attributes: ['id', 'title', 'coverPath', 'updatedAt']
}
}
]
})
const oldMediaProgresses = mediaProgresses.map(mp => {
const oldMediaProgress = mp.getOldMediaProgress()
oldMediaProgress.displayTitle = mp.mediaItem?.title
if (mp.mediaItem?.podcast) {
oldMediaProgress.displaySubtitle = mp.mediaItem.podcast?.title
oldMediaProgress.coverPath = mp.mediaItem.podcast?.coverPath
oldMediaProgress.mediaUpdatedAt = mp.mediaItem.podcast?.updatedAt
} else if (mp.mediaItem) {
oldMediaProgress.coverPath = mp.mediaItem.coverPath
oldMediaProgress.mediaUpdatedAt = mp.mediaItem.updatedAt
}
return oldMediaProgress
})
const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot)
userJson.mediaProgress = oldMediaProgresses
res.json(userJson)
} }
async create(req, res) { async create(req, res) {
const account = req.body const account = req.body
const username = account.username const username = account.username
const usernameExists = await Database.models.user.getUserByUsername(username) const usernameExists = await Database.userModel.getUserByUsername(username)
if (usernameExists) { if (usernameExists) {
return res.status(500).send('Username already taken') return res.status(500).send('Username already taken')
} }
@ -80,7 +127,7 @@ class UserController {
var shouldUpdateToken = false var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) { if (account.username !== undefined && account.username !== user.username) {
const usernameExists = await Database.models.user.getUserByUsername(account.username) const usernameExists = await Database.userModel.getUserByUsername(account.username)
if (usernameExists) { if (usernameExists) {
return res.status(500).send('Username already taken') return res.status(500).send('Username already taken')
} }
@ -122,7 +169,7 @@ class UserController {
// Todo: check if user is logged in and cancel streams // Todo: check if user is logged in and cancel streams
// Remove user playlists // Remove user playlists
const userPlaylists = await Database.models.playlist.findAll({ const userPlaylists = await Database.playlistModel.findAll({
where: { where: {
userId: user.id userId: user.id
} }
@ -186,7 +233,7 @@ class UserController {
} }
if (req.params.id) { if (req.params.id) {
req.reqUser = await Database.models.user.getUserById(req.params.id) req.reqUser = await Database.userModel.getUserById(req.params.id)
if (!req.reqUser) { if (!req.reqUser) {
return res.sendStatus(404) return res.sendStatus(404)
} }

View File

@ -5,23 +5,23 @@ const { Sequelize } = require('sequelize')
const Database = require('../Database') const Database = require('../Database')
const getLibraryItemMinified = (libraryItemId) => { const getLibraryItemMinified = (libraryItemId) => {
return Database.models.libraryItem.findByPk(libraryItemId, { return Database.libraryItemModel.findByPk(libraryItemId, {
include: [ include: [
{ {
model: Database.models.book, model: Database.bookModel,
attributes: [ attributes: [
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags' 'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags'
], ],
include: [ include: [
{ {
model: Database.models.author, model: Database.authorModel,
attributes: ['id', 'name'], attributes: ['id', 'name'],
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
attributes: ['id', 'name'], attributes: ['id', 'name'],
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
@ -30,7 +30,7 @@ const getLibraryItemMinified = (libraryItemId) => {
] ]
}, },
{ {
model: Database.models.podcast, model: Database.podcastModel,
attributes: [ attributes: [
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags', 'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags',
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes'] [Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
@ -41,19 +41,19 @@ const getLibraryItemMinified = (libraryItemId) => {
} }
const getLibraryItemExpanded = (libraryItemId) => { const getLibraryItemExpanded = (libraryItemId) => {
return Database.models.libraryItem.findByPk(libraryItemId, { return Database.libraryItemModel.findByPk(libraryItemId, {
include: [ include: [
{ {
model: Database.models.book, model: Database.bookModel,
include: [ include: [
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -61,10 +61,10 @@ const getLibraryItemExpanded = (libraryItemId) => {
] ]
}, },
{ {
model: Database.models.podcast, model: Database.podcastModel,
include: [ include: [
{ {
model: Database.models.podcastEpisode model: Database.podcastEpisodeModel
} }
] ]
}, },

View File

@ -77,7 +77,7 @@ class CronManager {
async initPodcastCrons() { async initPodcastCrons() {
const cronExpressionMap = {} const cronExpressionMap = {}
const podcastsWithAutoDownload = await Database.models.podcast.findAll({ const podcastsWithAutoDownload = await Database.podcastModel.findAll({
where: { where: {
autoDownloadEpisodes: true, autoDownloadEpisodes: true,
autoDownloadSchedule: { autoDownloadSchedule: {
@ -85,7 +85,7 @@ class CronManager {
} }
}, },
include: { include: {
model: Database.models.libraryItem model: Database.libraryItemModel
} }
}) })
@ -139,7 +139,7 @@ class CronManager {
// Get podcast library items to check // Get podcast library items to check
const libraryItems = [] const libraryItems = []
for (const libraryItemId of libraryItemIds) { for (const libraryItemId of libraryItemIds) {
const libraryItem = await Database.models.libraryItem.getOldById(libraryItemId) const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId)
if (!libraryItem) { if (!libraryItem) {
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`) Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out

View File

@ -18,7 +18,7 @@ class NotificationManager {
if (!Database.notificationSettings.isUseable) return if (!Database.notificationSettings.isUseable) return
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`) Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
const library = await Database.models.library.getOldById(libraryItem.libraryId) const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
const eventData = { const eventData = {
libraryItemId: libraryItem.id, libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId, libraryId: libraryItem.libraryId,

View File

@ -265,7 +265,7 @@ class PlaybackSessionManager {
} }
async syncSession(user, session, syncData) { async syncSession(user, session, syncData) {
const libraryItem = await Database.models.libraryItem.getOldById(session.libraryItemId) const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId)
if (!libraryItem) { if (!libraryItem) {
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
return null return null

View File

@ -150,7 +150,7 @@ class PodcastManager {
return false return false
} }
const libraryItem = await Database.models.libraryItem.getOldById(this.currentDownload.libraryItem.id) const libraryItem = await Database.libraryItemModel.getOldById(this.currentDownload.libraryItem.id)
if (!libraryItem) { if (!libraryItem) {
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`) Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
return false return false
@ -372,8 +372,13 @@ class PodcastManager {
} }
} }
generateOPMLFileText(libraryItems) { /**
return opmlGenerator.generate(libraryItems) * OPML file string for podcasts in a library
* @param {import('../models/Podcast')[]} podcasts
* @returns {string} XML string
*/
generateOPMLFileText(podcasts) {
return opmlGenerator.generate(podcasts)
} }
getDownloadQueueDetails(libraryId = null) { getDownloadQueueDetails(libraryId = null) {

View File

@ -13,13 +13,13 @@ class RssFeedManager {
async validateFeedEntity(feedObj) { async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') { if (feedObj.entityType === 'collection') {
const collection = await Database.models.collection.getOldById(feedObj.entityId) const collection = await Database.collectionModel.getOldById(feedObj.entityId)
if (!collection) { if (!collection) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`) Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
return false return false
} }
} else if (feedObj.entityType === 'libraryItem') { } else if (feedObj.entityType === 'libraryItem') {
const libraryItemExists = await Database.models.libraryItem.checkExistsById(feedObj.entityId) const libraryItemExists = await Database.libraryItemModel.checkExistsById(feedObj.entityId)
if (!libraryItemExists) { if (!libraryItemExists) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`) Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
return false return false
@ -41,7 +41,7 @@ class RssFeedManager {
* Validate all feeds and remove invalid * Validate all feeds and remove invalid
*/ */
async init() { async init() {
const feeds = await Database.models.feed.getOldFeeds() const feeds = await Database.feedModel.getOldFeeds()
for (const feed of feeds) { for (const feed of feeds) {
// Remove invalid feeds // Remove invalid feeds
if (!await this.validateFeedEntity(feed)) { if (!await this.validateFeedEntity(feed)) {
@ -56,7 +56,7 @@ class RssFeedManager {
* @returns {Promise<objects.Feed>} oldFeed * @returns {Promise<objects.Feed>} oldFeed
*/ */
findFeedForEntityId(entityId) { findFeedForEntityId(entityId) {
return Database.models.feed.findOneOld({ entityId }) return Database.feedModel.findOneOld({ entityId })
} }
/** /**
@ -65,7 +65,7 @@ class RssFeedManager {
* @returns {Promise<objects.Feed>} oldFeed * @returns {Promise<objects.Feed>} oldFeed
*/ */
findFeedBySlug(slug) { findFeedBySlug(slug) {
return Database.models.feed.findOneOld({ slug }) return Database.feedModel.findOneOld({ slug })
} }
/** /**
@ -74,7 +74,7 @@ class RssFeedManager {
* @returns {Promise<objects.Feed>} oldFeed * @returns {Promise<objects.Feed>} oldFeed
*/ */
findFeed(id) { findFeed(id) {
return Database.models.feed.findByPkOld(id) return Database.feedModel.findByPkOld(id)
} }
async getFeed(req, res) { async getFeed(req, res) {
@ -103,7 +103,7 @@ class RssFeedManager {
await Database.updateFeed(feed) await Database.updateFeed(feed)
} }
} else if (feed.entityType === 'collection') { } else if (feed.entityType === 'collection') {
const collection = await Database.models.collection.findByPk(feed.entityId) const collection = await Database.collectionModel.findByPk(feed.entityId)
if (collection) { if (collection) {
const collectionExpanded = await collection.getOldJsonExpanded() const collectionExpanded = await collection.getOldJsonExpanded()

View File

@ -83,6 +83,15 @@ class Author extends Model {
}) })
} }
/**
* Check if author exists
* @param {string} authorId
* @returns {Promise<boolean>}
*/
static async checkExistsById(authorId) {
return (await this.count({ where: { id: authorId } })) > 0
}
/** /**
* Initialize model * Initialize model
* @param {import('../Database').sequelize} sequelize * @param {import('../Database').sequelize} sequelize

View File

@ -1,8 +1,56 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
module.exports = (sequelize) => { class Book extends Model {
class Book extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.title
/** @type {string} */
this.titleIgnorePrefix
/** @type {string} */
this.publishedYear
/** @type {string} */
this.publishedDate
/** @type {string} */
this.publisher
/** @type {string} */
this.description
/** @type {string} */
this.isbn
/** @type {string} */
this.asin
/** @type {string} */
this.language
/** @type {boolean} */
this.explicit
/** @type {boolean} */
this.abridged
/** @type {string} */
this.coverPath
/** @type {number} */
this.duration
/** @type {Object} */
this.narrators
/** @type {Object} */
this.audioFiles
/** @type {Object} */
this.ebookFile
/** @type {Object} */
this.chapters
/** @type {Object} */
this.tags
/** @type {Object} */
this.genres
/** @type {Date} */
this.updatedAt
/** @type {Date} */
this.createdAt
}
static getOldBook(libraryItemExpanded) { static getOldBook(libraryItemExpanded) {
const bookExpanded = libraryItemExpanded.media const bookExpanded = libraryItemExpanded.media
let authors = [] let authors = []
@ -120,9 +168,13 @@ module.exports = (sequelize) => {
genres: oldBook.metadata.genres genres: oldBook.metadata.genres
} }
} }
}
Book.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -173,6 +225,7 @@ module.exports = (sequelize) => {
} }
] ]
}) })
}
return Book
} }
module.exports = Book

View File

@ -1,7 +1,19 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class BookAuthor extends Model {
class BookAuthor extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {UUIDV4} */
this.bookId
/** @type {UUIDV4} */
this.authorId
/** @type {Date} */
this.createdAt
}
static removeByIds(authorId = null, bookId = null) { static removeByIds(authorId = null, bookId = null) {
const where = {} const where = {}
if (authorId) where.authorId = authorId if (authorId) where.authorId = authorId
@ -10,9 +22,13 @@ module.exports = (sequelize) => {
where where
}) })
} }
}
BookAuthor.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -36,6 +52,6 @@ module.exports = (sequelize) => {
author.hasMany(BookAuthor) author.hasMany(BookAuthor)
BookAuthor.belongsTo(author) BookAuthor.belongsTo(author)
}
return BookAuthor
} }
module.exports = BookAuthor

View File

@ -1,7 +1,21 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class BookSeries extends Model {
class BookSeries extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.sequence
/** @type {UUIDV4} */
this.bookId
/** @type {UUIDV4} */
this.seriesId
/** @type {Date} */
this.createdAt
}
static removeByIds(seriesId = null, bookId = null) { static removeByIds(seriesId = null, bookId = null) {
const where = {} const where = {}
if (seriesId) where.seriesId = seriesId if (seriesId) where.seriesId = seriesId
@ -10,9 +24,13 @@ module.exports = (sequelize) => {
where where
}) })
} }
}
BookSeries.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -37,6 +55,7 @@ module.exports = (sequelize) => {
series.hasMany(BookSeries) series.hasMany(BookSeries)
BookSeries.belongsTo(series) BookSeries.belongsTo(series)
}
return BookSeries
} }
module.exports = BookSeries

View File

@ -1,10 +1,25 @@
const { DataTypes, Model, Sequelize } = require('sequelize') const { DataTypes, Model, Sequelize } = require('sequelize')
const oldCollection = require('../objects/Collection') const oldCollection = require('../objects/Collection')
const { areEquivalent } = require('../utils/index')
module.exports = (sequelize) => {
class Collection extends Model { class Collection extends Model {
constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.name
/** @type {string} */
this.description
/** @type {UUIDV4} */
this.libraryId
/** @type {Date} */
this.updatedAt
/** @type {Date} */
this.createdAt
}
/** /**
* Get all old collections * Get all old collections
* @returns {Promise<oldCollection[]>} * @returns {Promise<oldCollection[]>}
@ -12,10 +27,10 @@ module.exports = (sequelize) => {
static async getOldCollections() { static async getOldCollections() {
const collections = await this.findAll({ const collections = await this.findAll({
include: { include: {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
}) })
return collections.map(c => this.getOldCollection(c)) return collections.map(c => this.getOldCollection(c))
} }
@ -39,7 +54,7 @@ module.exports = (sequelize) => {
const collectionIncludes = [] const collectionIncludes = []
if (include.includes('rssfeed')) { if (include.includes('rssfeed')) {
collectionIncludes.push({ collectionIncludes.push({
model: sequelize.models.feed model: this.sequelize.models.feed
}) })
} }
@ -47,19 +62,19 @@ module.exports = (sequelize) => {
where: collectionWhere, where: collectionWhere,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: [ include: [
{ {
model: sequelize.models.libraryItem model: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -69,7 +84,7 @@ module.exports = (sequelize) => {
}, },
...collectionIncludes ...collectionIncludes
], ],
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
}) })
// TODO: Handle user permission restrictions on initial query // TODO: Handle user permission restrictions on initial query
return collections.map(c => { return collections.map(c => {
@ -93,7 +108,7 @@ module.exports = (sequelize) => {
const libraryItem = b.libraryItem const libraryItem = b.libraryItem
delete b.libraryItem delete b.libraryItem
libraryItem.media = b libraryItem.media = b
return sequelize.models.libraryItem.getOldLibraryItem(libraryItem) return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
}) })
// Users with restricted permissions will not see this collection // Users with restricted permissions will not see this collection
@ -105,7 +120,7 @@ module.exports = (sequelize) => {
// Map feed if found // Map feed if found
if (c.feeds?.length) { if (c.feeds?.length) {
collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(c.feeds[0]) collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0])
} }
return collectionExpanded return collectionExpanded
@ -122,16 +137,16 @@ module.exports = (sequelize) => {
this.books = await this.getBooks({ this.books = await this.getBooks({
include: [ include: [
{ {
model: sequelize.models.libraryItem model: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -141,7 +156,7 @@ module.exports = (sequelize) => {
order: [Sequelize.literal('`collectionBook.order` ASC')] order: [Sequelize.literal('`collectionBook.order` ASC')]
}) || [] }) || []
const oldCollection = sequelize.models.collection.getOldCollection(this) const oldCollection = this.sequelize.models.collection.getOldCollection(this)
// Filter books using user permissions // Filter books using user permissions
// TODO: Handle user permission restrictions on initial query // TODO: Handle user permission restrictions on initial query
@ -162,7 +177,7 @@ module.exports = (sequelize) => {
const libraryItem = b.libraryItem const libraryItem = b.libraryItem
delete b.libraryItem delete b.libraryItem
libraryItem.media = b libraryItem.media = b
return sequelize.models.libraryItem.getOldLibraryItem(libraryItem) return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
}) })
// Users with restricted permissions will not see this collection // Users with restricted permissions will not see this collection
@ -175,7 +190,7 @@ module.exports = (sequelize) => {
if (include?.includes('rssfeed')) { if (include?.includes('rssfeed')) {
const feeds = await this.getFeeds() const feeds = await this.getFeeds()
if (feeds?.length) { if (feeds?.length) {
collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0]) collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
} }
} }
@ -231,10 +246,10 @@ module.exports = (sequelize) => {
if (!collectionId) return null if (!collectionId) return null
const collection = await this.findByPk(collectionId, { const collection = await this.findByPk(collectionId, {
include: { include: {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
}) })
if (!collection) return null if (!collection) return null
return this.getOldCollection(collection) return this.getOldCollection(collection)
@ -248,16 +263,16 @@ module.exports = (sequelize) => {
this.books = await this.getBooks({ this.books = await this.getBooks({
include: [ include: [
{ {
model: sequelize.models.libraryItem model: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -267,7 +282,7 @@ module.exports = (sequelize) => {
order: [Sequelize.literal('`collectionBook.order` ASC')] order: [Sequelize.literal('`collectionBook.order` ASC')]
}) || [] }) || []
return sequelize.models.collection.getOldCollection(this) return this.sequelize.models.collection.getOldCollection(this)
} }
/** /**
@ -287,20 +302,24 @@ module.exports = (sequelize) => {
static async getAllForBook(bookId) { static async getAllForBook(bookId) {
const collections = await this.findAll({ const collections = await this.findAll({
include: { include: {
model: sequelize.models.book, model: this.sequelize.models.book,
where: { where: {
id: bookId id: bookId
}, },
required: true, required: true,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
}) })
return collections.map(c => this.getOldCollection(c)) return collections.map(c => this.getOldCollection(c))
} }
}
Collection.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -317,6 +336,7 @@ module.exports = (sequelize) => {
library.hasMany(Collection) library.hasMany(Collection)
Collection.belongsTo(library) Collection.belongsTo(library)
}
return Collection
} }
module.exports = Collection

View File

@ -1,7 +1,21 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class CollectionBook extends Model {
class CollectionBook extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {number} */
this.order
/** @type {UUIDV4} */
this.bookId
/** @type {UUIDV4} */
this.collectionId
/** @type {Date} */
this.createdAt
}
static removeByIds(collectionId, bookId) { static removeByIds(collectionId, bookId) {
return this.destroy({ return this.destroy({
where: { where: {
@ -10,9 +24,9 @@ module.exports = (sequelize) => {
} }
}) })
} }
}
CollectionBook.init({ static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -41,6 +55,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
CollectionBook.belongsTo(collection) CollectionBook.belongsTo(collection)
}
return CollectionBook
} }
module.exports = CollectionBook

View File

@ -1,8 +1,34 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const oldDevice = require('../objects/DeviceInfo') const oldDevice = require('../objects/DeviceInfo')
module.exports = (sequelize) => { class Device extends Model {
class Device extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.deviceId
/** @type {string} */
this.clientName
/** @type {string} */
this.clientVersion
/** @type {string} */
this.ipAddress
/** @type {string} */
this.deviceName
/** @type {string} */
this.deviceVersion
/** @type {object} */
this.extraData
/** @type {UUIDV4} */
this.userId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
getOldDevice() { getOldDevice() {
let browserVersion = null let browserVersion = null
let sdkVersion = null let sdkVersion = null
@ -85,9 +111,13 @@ module.exports = (sequelize) => {
extraData extraData
} }
} }
}
Device.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -111,6 +141,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
Device.belongsTo(user) Device.belongsTo(user)
}
return Device
} }
module.exports = Device

View File

@ -1,16 +1,61 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const oldFeed = require('../objects/Feed') const oldFeed = require('../objects/Feed')
const areEquivalent = require('../utils/areEquivalent') const areEquivalent = require('../utils/areEquivalent')
/*
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ class Feed extends Model {
* Feeds can be created from LibraryItem, Collection, Playlist or Series constructor(values, options) {
*/ super(values, options)
module.exports = (sequelize) => {
class Feed extends Model { /** @type {UUIDV4} */
this.id
/** @type {string} */
this.slug
/** @type {string} */
this.entityType
/** @type {UUIDV4} */
this.entityId
/** @type {Date} */
this.entityUpdatedAt
/** @type {string} */
this.serverAddress
/** @type {string} */
this.feedURL
/** @type {string} */
this.imageURL
/** @type {string} */
this.siteURL
/** @type {string} */
this.title
/** @type {string} */
this.description
/** @type {string} */
this.author
/** @type {string} */
this.podcastType
/** @type {string} */
this.language
/** @type {string} */
this.ownerName
/** @type {string} */
this.ownerEmail
/** @type {boolean} */
this.explicit
/** @type {boolean} */
this.preventIndexing
/** @type {string} */
this.coverPath
/** @type {UUIDV4} */
this.userId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
static async getOldFeeds() { static async getOldFeeds() {
const feeds = await this.findAll({ const feeds = await this.findAll({
include: { include: {
model: sequelize.models.feedEpisode model: this.sequelize.models.feedEpisode
} }
}) })
return feeds.map(f => this.getOldFeed(f)) return feeds.map(f => this.getOldFeed(f))
@ -85,7 +130,7 @@ module.exports = (sequelize) => {
const feedExpanded = await this.findOne({ const feedExpanded = await this.findOne({
where, where,
include: { include: {
model: sequelize.models.feedEpisode model: this.sequelize.models.feedEpisode
} }
}) })
if (!feedExpanded) return null if (!feedExpanded) return null
@ -101,7 +146,7 @@ module.exports = (sequelize) => {
if (!id) return null if (!id) return null
const feedExpanded = await this.findByPk(id, { const feedExpanded = await this.findByPk(id, {
include: { include: {
model: sequelize.models.feedEpisode model: this.sequelize.models.feedEpisode
} }
}) })
if (!feedExpanded) return null if (!feedExpanded) return null
@ -114,9 +159,9 @@ module.exports = (sequelize) => {
if (oldFeed.episodes?.length) { if (oldFeed.episodes?.length) {
for (const oldFeedEpisode of oldFeed.episodes) { for (const oldFeedEpisode of oldFeed.episodes) {
const feedEpisode = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
feedEpisode.feedId = newFeed.id feedEpisode.feedId = newFeed.id
await sequelize.models.feedEpisode.create(feedEpisode) await this.sequelize.models.feedEpisode.create(feedEpisode)
} }
} }
} }
@ -126,7 +171,7 @@ module.exports = (sequelize) => {
const feedObj = this.getFromOld(oldFeed) const feedObj = this.getFromOld(oldFeed)
const existingFeed = await this.findByPk(feedObj.id, { const existingFeed = await this.findByPk(feedObj.id, {
include: sequelize.models.feedEpisode include: this.sequelize.models.feedEpisode
}) })
if (!existingFeed) return false if (!existingFeed) return false
@ -138,7 +183,7 @@ module.exports = (sequelize) => {
feedEpisode.destroy() feedEpisode.destroy()
} else { } else {
let episodeHasUpdates = false let episodeHasUpdates = false
const oldFeedEpisodeCleaned = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode)
for (const key in oldFeedEpisodeCleaned) { for (const key in oldFeedEpisodeCleaned) {
if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) { if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) {
episodeHasUpdates = true episodeHasUpdates = true
@ -197,12 +242,20 @@ module.exports = (sequelize) => {
getEntity(options) { getEntity(options) {
if (!this.entityType) return Promise.resolve(null) if (!this.entityType) return Promise.resolve(null)
const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}` const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}`
return this[mixinMethodName](options) return this[mixinMethodName](options)
} }
}
Feed.init({ /**
* Initialize model
*
* Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
*
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -302,6 +355,7 @@ module.exports = (sequelize) => {
delete instance.dataValues.playlist delete instance.dataValues.playlist
} }
}) })
}
return Feed
} }
module.exports = Feed

View File

@ -1,7 +1,45 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class FeedEpisode extends Model {
class FeedEpisode extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.title
/** @type {string} */
this.description
/** @type {string} */
this.siteURL
/** @type {string} */
this.enclosureURL
/** @type {string} */
this.enclosureType
/** @type {BigInt} */
this.enclosureSize
/** @type {string} */
this.pubDate
/** @type {string} */
this.season
/** @type {string} */
this.episode
/** @type {string} */
this.episodeType
/** @type {number} */
this.duration
/** @type {string} */
this.filePath
/** @type {boolean} */
this.explicit
/** @type {UUIDV4} */
this.feedId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
getOldEpisode() { getOldEpisode() {
const enclosure = { const enclosure = {
url: this.enclosureURL, url: this.enclosureURL,
@ -44,9 +82,13 @@ module.exports = (sequelize) => {
explicit: !!oldFeedEpisode.explicit explicit: !!oldFeedEpisode.explicit
} }
} }
}
FeedEpisode.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -77,6 +119,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
FeedEpisode.belongsTo(feed) FeedEpisode.belongsTo(feed)
}
return FeedEpisode
} }
module.exports = FeedEpisode

View File

@ -2,15 +2,44 @@ const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
const oldLibrary = require('../objects/Library') const oldLibrary = require('../objects/Library')
module.exports = (sequelize) => {
class Library extends Model { class Library extends Model {
constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.name
/** @type {number} */
this.displayOrder
/** @type {string} */
this.icon
/** @type {string} */
this.mediaType
/** @type {string} */
this.provider
/** @type {Date} */
this.lastScan
/** @type {string} */
this.lastScanVersion
/** @type {Object} */
this.settings
/** @type {Object} */
this.extraData
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
/** /**
* Get all old libraries * Get all old libraries
* @returns {Promise<oldLibrary[]>} * @returns {Promise<oldLibrary[]>}
*/ */
static async getAllOldLibraries() { static async getAllOldLibraries() {
const libraries = await this.findAll({ const libraries = await this.findAll({
include: sequelize.models.libraryFolder, include: this.sequelize.models.libraryFolder,
order: [['displayOrder', 'ASC']] order: [['displayOrder', 'ASC']]
}) })
return libraries.map(lib => this.getOldLibrary(lib)) return libraries.map(lib => this.getOldLibrary(lib))
@ -60,7 +89,7 @@ module.exports = (sequelize) => {
}) })
return this.create(library, { return this.create(library, {
include: sequelize.models.libraryFolder include: this.sequelize.models.libraryFolder
}).catch((error) => { }).catch((error) => {
Logger.error(`[Library] Failed to create library ${library.id}`, error) Logger.error(`[Library] Failed to create library ${library.id}`, error)
return null return null
@ -74,7 +103,7 @@ module.exports = (sequelize) => {
*/ */
static async updateFromOld(oldLibrary) { static async updateFromOld(oldLibrary) {
const existingLibrary = await this.findByPk(oldLibrary.id, { const existingLibrary = await this.findByPk(oldLibrary.id, {
include: sequelize.models.libraryFolder include: this.sequelize.models.libraryFolder
}) })
if (!existingLibrary) { if (!existingLibrary) {
Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`) Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`)
@ -93,7 +122,7 @@ module.exports = (sequelize) => {
for (const libraryFolder of libraryFolders) { for (const libraryFolder of libraryFolders) {
const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id) const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id)
if (!existingLibraryFolder) { if (!existingLibraryFolder) {
await sequelize.models.libraryFolder.create(libraryFolder) await this.sequelize.models.libraryFolder.create(libraryFolder)
} else if (existingLibraryFolder.path !== libraryFolder.path) { } else if (existingLibraryFolder.path !== libraryFolder.path) {
await existingLibraryFolder.update({ path: libraryFolder.path }) await existingLibraryFolder.update({ path: libraryFolder.path })
} }
@ -159,7 +188,7 @@ module.exports = (sequelize) => {
static async getOldById(libraryId) { static async getOldById(libraryId) {
if (!libraryId) return null if (!libraryId) return null
const library = await this.findByPk(libraryId, { const library = await this.findByPk(libraryId, {
include: sequelize.models.libraryFolder include: this.sequelize.models.libraryFolder
}) })
if (!library) return null if (!library) return null
return this.getOldLibrary(library) return this.getOldLibrary(library)
@ -192,9 +221,13 @@ module.exports = (sequelize) => {
} }
} }
} }
}
Library.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -213,6 +246,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'library' modelName: 'library'
}) })
}
return Library
} }
module.exports = Library

View File

@ -1,7 +1,21 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class LibraryFolder extends Model {
class LibraryFolder extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.path
/** @type {UUIDV4} */
this.libraryId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
/** /**
* Gets all library folder path strings * Gets all library folder path strings
* @returns {Promise<string[]>} array of library folder paths * @returns {Promise<string[]>} array of library folder paths
@ -12,9 +26,13 @@ module.exports = (sequelize) => {
}) })
return libraryFolders.map(l => l.path) return libraryFolders.map(l => l.path)
} }
}
LibraryFolder.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -31,6 +49,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
LibraryFolder.belongsTo(library) LibraryFolder.belongsTo(library)
}
return LibraryFolder
} }
module.exports = LibraryFolder

View File

@ -4,8 +4,55 @@ const oldLibraryItem = require('../objects/LibraryItem')
const libraryFilters = require('../utils/queries/libraryFilters') const libraryFilters = require('../utils/queries/libraryFilters')
const { areEquivalent } = require('../utils/index') const { areEquivalent } = require('../utils/index')
module.exports = (sequelize) => {
class LibraryItem extends Model { class LibraryItem extends Model {
constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.ino
/** @type {string} */
this.path
/** @type {string} */
this.relPath
/** @type {UUIDV4} */
this.mediaId
/** @type {string} */
this.mediaType
/** @type {boolean} */
this.isFile
/** @type {boolean} */
this.isMissing
/** @type {boolean} */
this.isInvalid
/** @type {Date} */
this.mtime
/** @type {Date} */
this.ctime
/** @type {Date} */
this.birthtime
/** @type {BigInt} */
this.size
/** @type {Date} */
this.lastScan
/** @type {string} */
this.lastScanVersion
/** @type {Object} */
this.libraryFiles
/** @type {Object} */
this.extraData
/** @type {UUIDV4} */
this.libraryId
/** @type {UUIDV4} */
this.libraryFolderId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
/** /**
* Loads all podcast episodes, all library items in chunks of 500, then maps them to old library items * Loads all podcast episodes, all library items in chunks of 500, then maps them to old library items
* @todo this is a temporary solution until we can use the sqlite without loading all the library items on init * @todo this is a temporary solution until we can use the sqlite without loading all the library items on init
@ -15,7 +62,7 @@ module.exports = (sequelize) => {
static async loadAllLibraryItems() { static async loadAllLibraryItems() {
let start = Date.now() let start = Date.now()
Logger.info(`[LibraryItem] Loading podcast episodes...`) Logger.info(`[LibraryItem] Loading podcast episodes...`)
const podcastEpisodes = await sequelize.models.podcastEpisode.findAll() const podcastEpisodes = await this.sequelize.models.podcastEpisode.findAll()
Logger.info(`[LibraryItem] Finished loading ${podcastEpisodes.length} podcast episodes in ${((Date.now() - start) / 1000).toFixed(2)}s`) Logger.info(`[LibraryItem] Finished loading ${podcastEpisodes.length} podcast episodes in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now() start = Date.now()
@ -69,16 +116,16 @@ module.exports = (sequelize) => {
}, },
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: [ include: [
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: ['createdAt'] attributes: ['createdAt']
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence', 'createdAt'] attributes: ['sequence', 'createdAt']
} }
@ -86,14 +133,14 @@ module.exports = (sequelize) => {
] ]
}, },
{ {
model: sequelize.models.podcast model: this.sequelize.models.podcast
} }
], ],
order: [ order: [
['createdAt', 'ASC'], ['createdAt', 'ASC'],
// Ensure author & series stay in the same order // Ensure author & series stay in the same order
[sequelize.models.book, sequelize.models.author, sequelize.models.bookAuthor, 'createdAt', 'ASC'], [this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
[sequelize.models.book, sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] [this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
], ],
offset, offset,
limit limit
@ -110,16 +157,16 @@ module.exports = (sequelize) => {
where, where,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: [ include: [
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -127,10 +174,10 @@ module.exports = (sequelize) => {
] ]
}, },
{ {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: [ include: [
{ {
model: sequelize.models.podcastEpisode model: this.sequelize.models.podcastEpisode
} }
] ]
} }
@ -148,9 +195,9 @@ module.exports = (sequelize) => {
static getOldLibraryItem(libraryItemExpanded) { static getOldLibraryItem(libraryItemExpanded) {
let media = null let media = null
if (libraryItemExpanded.mediaType === 'book') { if (libraryItemExpanded.mediaType === 'book') {
media = sequelize.models.book.getOldBook(libraryItemExpanded) media = this.sequelize.models.book.getOldBook(libraryItemExpanded)
} else if (libraryItemExpanded.mediaType === 'podcast') { } else if (libraryItemExpanded.mediaType === 'podcast') {
media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded) media = this.sequelize.models.podcast.getOldPodcast(libraryItemExpanded)
} }
return new oldLibraryItem({ return new oldLibraryItem({
@ -181,30 +228,30 @@ module.exports = (sequelize) => {
const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem)) const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem))
if (oldLibraryItem.mediaType === 'book') { if (oldLibraryItem.mediaType === 'book') {
const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media) const bookObj = this.sequelize.models.book.getFromOld(oldLibraryItem.media)
bookObj.libraryItemId = newLibraryItem.id bookObj.libraryItemId = newLibraryItem.id
const newBook = await sequelize.models.book.create(bookObj) const newBook = await this.sequelize.models.book.create(bookObj)
const oldBookAuthors = oldLibraryItem.media.metadata.authors || [] const oldBookAuthors = oldLibraryItem.media.metadata.authors || []
const oldBookSeriesAll = oldLibraryItem.media.metadata.series || [] const oldBookSeriesAll = oldLibraryItem.media.metadata.series || []
for (const oldBookAuthor of oldBookAuthors) { for (const oldBookAuthor of oldBookAuthors) {
await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id }) await this.sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id })
} }
for (const oldSeries of oldBookSeriesAll) { for (const oldSeries of oldBookSeriesAll) {
await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence }) await this.sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence })
} }
} else if (oldLibraryItem.mediaType === 'podcast') { } else if (oldLibraryItem.mediaType === 'podcast') {
const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media) const podcastObj = this.sequelize.models.podcast.getFromOld(oldLibraryItem.media)
podcastObj.libraryItemId = newLibraryItem.id podcastObj.libraryItemId = newLibraryItem.id
const newPodcast = await sequelize.models.podcast.create(podcastObj) const newPodcast = await this.sequelize.models.podcast.create(podcastObj)
const oldEpisodes = oldLibraryItem.media.episodes || [] const oldEpisodes = oldLibraryItem.media.episodes || []
for (const oldEpisode of oldEpisodes) { for (const oldEpisode of oldEpisodes) {
const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode) const episodeObj = this.sequelize.models.podcastEpisode.getFromOld(oldEpisode)
episodeObj.libraryItemId = newLibraryItem.id episodeObj.libraryItemId = newLibraryItem.id
episodeObj.podcastId = newPodcast.id episodeObj.podcastId = newPodcast.id
await sequelize.models.podcastEpisode.create(episodeObj) await this.sequelize.models.podcastEpisode.create(episodeObj)
} }
} }
@ -215,16 +262,16 @@ module.exports = (sequelize) => {
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: [ include: [
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['id', 'sequence'] attributes: ['id', 'sequence']
} }
@ -232,10 +279,10 @@ module.exports = (sequelize) => {
] ]
}, },
{ {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: [ include: [
{ {
model: sequelize.models.podcastEpisode model: this.sequelize.models.podcastEpisode
} }
] ]
} }
@ -249,7 +296,7 @@ module.exports = (sequelize) => {
if (libraryItemExpanded.media) { if (libraryItemExpanded.media) {
let updatedMedia = null let updatedMedia = null
if (libraryItemExpanded.mediaType === 'podcast') { if (libraryItemExpanded.mediaType === 'podcast') {
updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media) updatedMedia = this.sequelize.models.podcast.getFromOld(oldLibraryItem.media)
const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || [] const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || []
const updatedPodcastEpisodes = oldLibraryItem.media.episodes || [] const updatedPodcastEpisodes = oldLibraryItem.media.episodes || []
@ -266,10 +313,10 @@ module.exports = (sequelize) => {
const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
if (!existingEpisodeMatch) { if (!existingEpisodeMatch) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) await this.sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
hasUpdates = true hasUpdates = true
} else { } else {
const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode) const updatedEpisodeCleaned = this.sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode)
let episodeHasUpdates = false let episodeHasUpdates = false
for (const key in updatedEpisodeCleaned) { for (const key in updatedEpisodeCleaned) {
let existingValue = existingEpisodeMatch[key] let existingValue = existingEpisodeMatch[key]
@ -287,7 +334,7 @@ module.exports = (sequelize) => {
} }
} }
} else if (libraryItemExpanded.mediaType === 'book') { } else if (libraryItemExpanded.mediaType === 'book') {
updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media) updatedMedia = this.sequelize.models.book.getFromOld(oldLibraryItem.media)
const existingAuthors = libraryItemExpanded.media.authors || [] const existingAuthors = libraryItemExpanded.media.authors || []
const existingSeriesAll = libraryItemExpanded.media.series || [] const existingSeriesAll = libraryItemExpanded.media.series || []
@ -298,7 +345,7 @@ module.exports = (sequelize) => {
// Author was removed from Book // Author was removed from Book
if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
hasUpdates = true hasUpdates = true
} }
} }
@ -306,7 +353,7 @@ module.exports = (sequelize) => {
// Author was added // Author was added
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) await this.sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
hasUpdates = true hasUpdates = true
} }
} }
@ -314,7 +361,7 @@ module.exports = (sequelize) => {
// Series was removed // Series was removed
if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) await this.sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
hasUpdates = true hasUpdates = true
} }
} }
@ -323,7 +370,7 @@ module.exports = (sequelize) => {
const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
if (!existingSeriesMatch) { if (!existingSeriesMatch) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) await this.sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
hasUpdates = true hasUpdates = true
} else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) { } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
@ -415,16 +462,16 @@ module.exports = (sequelize) => {
const libraryItem = await this.findByPk(libraryItemId, { const libraryItem = await this.findByPk(libraryItemId, {
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: [ include: [
{ {
model: sequelize.models.author, model: this.sequelize.models.author,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -432,13 +479,17 @@ module.exports = (sequelize) => {
] ]
}, },
{ {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: [ include: [
{ {
model: sequelize.models.podcastEpisode model: this.sequelize.models.podcastEpisode
} }
] ]
} }
],
order: [
[this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
[this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
] ]
}) })
if (!libraryItem) return null if (!libraryItem) return null
@ -467,7 +518,7 @@ module.exports = (sequelize) => {
oldLibraryItem.media.metadata.series = li.series oldLibraryItem.media.metadata.series = li.series
} }
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = this.sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.media.numEpisodes) { if (li.media.numEpisodes) {
oldLibraryItem.media.numEpisodes = li.media.numEpisodes oldLibraryItem.media.numEpisodes = li.media.numEpisodes
@ -688,14 +739,62 @@ module.exports = (sequelize) => {
return (await this.count({ where: { id: libraryItemId } })) > 0 return (await this.count({ where: { id: libraryItemId } })) > 0
} }
getMedia(options) { /**
if (!this.mediaType) return Promise.resolve(null) *
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}` * @param {WhereOptions} where
return this[mixinMethodName](options) * @returns {Object} oldLibraryItem
*/
static async findOneOld(where) {
const libraryItem = await this.findOne({
where,
include: [
{
model: this.sequelize.models.book,
include: [
{
model: this.sequelize.models.author,
through: {
attributes: []
} }
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
}
]
},
{
model: this.sequelize.models.podcast,
include: [
{
model: this.sequelize.models.podcastEpisode
}
]
}
],
order: [
[this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
[this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
]
})
if (!libraryItem) return null
return this.getOldLibraryItem(libraryItem)
} }
LibraryItem.init({ getMedia(options) {
if (!this.mediaType) return Promise.resolve(null)
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`
return this[mixinMethodName](options)
}
/**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -791,6 +890,7 @@ module.exports = (sequelize) => {
media.destroy() media.destroy()
} }
}) })
}
return LibraryItem
} }
module.exports = LibraryItem

View File

@ -1,11 +1,39 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
/* class MediaProgress extends Model {
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ constructor(values, options) {
* Book has many MediaProgress. PodcastEpisode has many MediaProgress. super(values, options)
*/
module.exports = (sequelize) => { /** @type {UUIDV4} */
class MediaProgress extends Model { this.id
/** @type {UUIDV4} */
this.mediaItemId
/** @type {string} */
this.mediaItemType
/** @type {number} */
this.duration
/** @type {number} */
this.currentTime
/** @type {boolean} */
this.isFinished
/** @type {boolean} */
this.hideFromContinueListening
/** @type {string} */
this.ebookLocation
/** @type {number} */
this.ebookProgress
/** @type {Date} */
this.finishedAt
/** @type {Object} */
this.extraData
/** @type {UUIDV4} */
this.userId
/** @type {Date} */
this.updatedAt
/** @type {Date} */
this.createdAt
}
getOldMediaProgress() { getOldMediaProgress() {
const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' const isPodcastEpisode = this.mediaItemType === 'podcastEpisode'
@ -66,13 +94,20 @@ module.exports = (sequelize) => {
getMediaItem(options) { getMediaItem(options) {
if (!this.mediaItemType) return Promise.resolve(null) if (!this.mediaItemType) return Promise.resolve(null)
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}`
return this[mixinMethodName](options) return this[mixinMethodName](options)
} }
}
/**
MediaProgress.init({ * Initialize model
*
* Polymorphic association: Book has many MediaProgress. PodcastEpisode has many MediaProgress.
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
*
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -143,6 +178,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
MediaProgress.belongsTo(user) MediaProgress.belongsTo(user)
}
return MediaProgress
} }
module.exports = MediaProgress

View File

@ -2,14 +2,63 @@ const { DataTypes, Model } = require('sequelize')
const oldPlaybackSession = require('../objects/PlaybackSession') const oldPlaybackSession = require('../objects/PlaybackSession')
module.exports = (sequelize) => {
class PlaybackSession extends Model { class PlaybackSession extends Model {
constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {UUIDV4} */
this.mediaItemId
/** @type {string} */
this.mediaItemType
/** @type {string} */
this.displayTitle
/** @type {string} */
this.displayAuthor
/** @type {number} */
this.duration
/** @type {number} */
this.playMethod
/** @type {string} */
this.mediaPlayer
/** @type {number} */
this.startTime
/** @type {number} */
this.currentTime
/** @type {string} */
this.serverVersion
/** @type {string} */
this.coverPath
/** @type {number} */
this.timeListening
/** @type {Object} */
this.mediaMetadata
/** @type {string} */
this.date
/** @type {string} */
this.dayOfWeek
/** @type {Object} */
this.extraData
/** @type {UUIDV4} */
this.userId
/** @type {UUIDV4} */
this.deviceId
/** @type {UUIDV4} */
this.libraryId
/** @type {Date} */
this.updatedAt
/** @type {Date} */
this.createdAt
}
static async getOldPlaybackSessions(where = null) { static async getOldPlaybackSessions(where = null) {
const playbackSessions = await this.findAll({ const playbackSessions = await this.findAll({
where, where,
include: [ include: [
{ {
model: sequelize.models.device model: this.sequelize.models.device
} }
] ]
}) })
@ -20,7 +69,7 @@ module.exports = (sequelize) => {
const playbackSession = await this.findByPk(sessionId, { const playbackSession = await this.findByPk(sessionId, {
include: [ include: [
{ {
model: sequelize.models.device model: this.sequelize.models.device
} }
] ]
}) })
@ -112,12 +161,16 @@ module.exports = (sequelize) => {
getMediaItem(options) { getMediaItem(options) {
if (!this.mediaItemType) return Promise.resolve(null) if (!this.mediaItemType) return Promise.resolve(null)
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}`
return this[mixinMethodName](options) return this[mixinMethodName](options)
} }
}
PlaybackSession.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -193,6 +246,7 @@ module.exports = (sequelize) => {
delete instance.dataValues.podcastEpisode delete instance.dataValues.podcastEpisode
} }
}) })
}
return PlaybackSession
} }
module.exports = PlaybackSession

View File

@ -3,22 +3,40 @@ const Logger = require('../Logger')
const oldPlaylist = require('../objects/Playlist') const oldPlaylist = require('../objects/Playlist')
module.exports = (sequelize) => { class Playlist extends Model {
class Playlist extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.name
/** @type {string} */
this.description
/** @type {UUIDV4} */
this.libraryId
/** @type {UUIDV4} */
this.userId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
static async getOldPlaylists() { static async getOldPlaylists() {
const playlists = await this.findAll({ const playlists = await this.findAll({
include: { include: {
model: sequelize.models.playlistMediaItem, model: this.sequelize.models.playlistMediaItem,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.podcastEpisode, model: this.sequelize.models.podcastEpisode,
include: { include: {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
} }
} }
] ]
@ -62,24 +80,24 @@ module.exports = (sequelize) => {
this.playlistMediaItems = await this.getPlaylistMediaItems({ this.playlistMediaItems = await this.getPlaylistMediaItems({
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.podcastEpisode, model: this.sequelize.models.podcastEpisode,
include: { include: {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
} }
} }
], ],
order: [['order', 'ASC']] order: [['order', 'ASC']]
}) || [] }) || []
const oldPlaylist = sequelize.models.playlist.getOldPlaylist(this) const oldPlaylist = this.sequelize.models.playlist.getOldPlaylist(this)
const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId) const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId)
let libraryItems = await sequelize.models.libraryItem.getAllOldLibraryItems({ let libraryItems = await this.sequelize.models.libraryItem.getAllOldLibraryItems({
id: libraryItemIds id: libraryItemIds
}) })
@ -88,7 +106,7 @@ module.exports = (sequelize) => {
if (include?.includes('rssfeed')) { if (include?.includes('rssfeed')) {
const feeds = await this.getFeeds() const feeds = await this.getFeeds()
if (feeds?.length) { if (feeds?.length) {
playlistExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0]) playlistExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
} }
} }
@ -127,17 +145,17 @@ module.exports = (sequelize) => {
if (!playlistId) return null if (!playlistId) return null
const playlist = await this.findByPk(playlistId, { const playlist = await this.findByPk(playlistId, {
include: { include: {
model: sequelize.models.playlistMediaItem, model: this.sequelize.models.playlistMediaItem,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.podcastEpisode, model: this.sequelize.models.podcastEpisode,
include: { include: {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
} }
} }
] ]
@ -166,17 +184,17 @@ module.exports = (sequelize) => {
const playlists = await this.findAll({ const playlists = await this.findAll({
where: whereQuery, where: whereQuery,
include: { include: {
model: sequelize.models.playlistMediaItem, model: this.sequelize.models.playlistMediaItem,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.podcastEpisode, model: this.sequelize.models.podcastEpisode,
include: { include: {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
} }
} }
] ]
@ -212,7 +230,7 @@ module.exports = (sequelize) => {
static async getPlaylistsForMediaItemIds(mediaItemIds) { static async getPlaylistsForMediaItemIds(mediaItemIds) {
if (!mediaItemIds?.length) return [] if (!mediaItemIds?.length) return []
const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({ const playlistMediaItemsExpanded = await this.sequelize.models.playlistMediaItem.findAll({
where: { where: {
mediaItemId: { mediaItemId: {
[Op.in]: mediaItemIds [Op.in]: mediaItemIds
@ -220,19 +238,19 @@ module.exports = (sequelize) => {
}, },
include: [ include: [
{ {
model: sequelize.models.playlist, model: this.sequelize.models.playlist,
include: { include: {
model: sequelize.models.playlistMediaItem, model: this.sequelize.models.playlistMediaItem,
include: [ include: [
{ {
model: sequelize.models.book, model: this.sequelize.models.book,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
}, },
{ {
model: sequelize.models.podcastEpisode, model: this.sequelize.models.podcastEpisode,
include: { include: {
model: sequelize.models.podcast, model: this.sequelize.models.podcast,
include: sequelize.models.libraryItem include: this.sequelize.models.libraryItem
} }
} }
] ]
@ -265,9 +283,13 @@ module.exports = (sequelize) => {
} }
return playlists return playlists
} }
}
Playlist.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -315,6 +337,7 @@ module.exports = (sequelize) => {
} }
}) })
}
return Playlist
} }
module.exports = Playlist

View File

@ -1,7 +1,23 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class PlaylistMediaItem extends Model {
class PlaylistMediaItem extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {UUIDV4} */
this.mediaItemId
/** @type {string} */
this.mediaItemType
/** @type {number} */
this.order
/** @type {UUIDV4} */
this.playlistId
/** @type {Date} */
this.createdAt
}
static removeByIds(playlistId, mediaItemId) { static removeByIds(playlistId, mediaItemId) {
return this.destroy({ return this.destroy({
where: { where: {
@ -13,12 +29,16 @@ module.exports = (sequelize) => {
getMediaItem(options) { getMediaItem(options) {
if (!this.mediaItemType) return Promise.resolve(null) if (!this.mediaItemType) return Promise.resolve(null)
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}`
return this[mixinMethodName](options) return this[mixinMethodName](options)
} }
}
PlaylistMediaItem.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -79,6 +99,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
PlaylistMediaItem.belongsTo(playlist) PlaylistMediaItem.belongsTo(playlist)
}
return PlaylistMediaItem
} }
module.exports = PlaylistMediaItem

View File

@ -1,10 +1,60 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => { class Podcast extends Model {
class Podcast extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.title
/** @type {string} */
this.titleIgnorePrefix
/** @type {string} */
this.author
/** @type {string} */
this.releaseDate
/** @type {string} */
this.feedURL
/** @type {string} */
this.imageURL
/** @type {string} */
this.description
/** @type {string} */
this.itunesPageURL
/** @type {string} */
this.itunesId
/** @type {string} */
this.itunesArtistId
/** @type {string} */
this.language
/** @type {string} */
this.podcastType
/** @type {boolean} */
this.explicit
/** @type {boolean} */
this.autoDownloadEpisodes
/** @type {string} */
this.autoDownloadSchedule
/** @type {Date} */
this.lastEpisodeCheck
/** @type {number} */
this.maxEpisodesToKeep
/** @type {string} */
this.coverPath
/** @type {Object} */
this.tags
/** @type {Object} */
this.genres
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
static getOldPodcast(libraryItemExpanded) { static getOldPodcast(libraryItemExpanded) {
const podcastExpanded = libraryItemExpanded.media const podcastExpanded = libraryItemExpanded.media
const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index) const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index)
return { return {
id: podcastExpanded.id, id: podcastExpanded.id,
libraryItemId: libraryItemExpanded.id, libraryItemId: libraryItemExpanded.id,
@ -61,9 +111,13 @@ module.exports = (sequelize) => {
genres: oldPodcastMetadata.genres genres: oldPodcastMetadata.genres
} }
} }
}
Podcast.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -95,6 +149,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'podcast' modelName: 'podcast'
}) })
}
return Podcast
} }
module.exports = Podcast

View File

@ -1,7 +1,54 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const oldPodcastEpisode = require('../objects/entities/PodcastEpisode')
module.exports = (sequelize) => { class PodcastEpisode extends Model {
class PodcastEpisode extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {number} */
this.index
/** @type {string} */
this.season
/** @type {string} */
this.episode
/** @type {string} */
this.episodeType
/** @type {string} */
this.title
/** @type {string} */
this.subtitle
/** @type {string} */
this.description
/** @type {string} */
this.pubDate
/** @type {string} */
this.enclosureURL
/** @type {BigInt} */
this.enclosureSize
/** @type {string} */
this.enclosureType
/** @type {Date} */
this.publishedAt
/** @type {Object} */
this.audioFile
/** @type {Object} */
this.chapters
/** @type {Object} */
this.extraData
/** @type {UUIDV4} */
this.podcastId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
/**
* @param {string} libraryItemId
* @returns {oldPodcastEpisode}
*/
getOldPodcastEpisode(libraryItemId = null) { getOldPodcastEpisode(libraryItemId = null) {
let enclosure = null let enclosure = null
if (this.enclosureURL) { if (this.enclosureURL) {
@ -11,7 +58,7 @@ module.exports = (sequelize) => {
length: this.enclosureSize !== null ? String(this.enclosureSize) : null length: this.enclosureSize !== null ? String(this.enclosureSize) : null
} }
} }
return { return new oldPodcastEpisode({
libraryItemId: libraryItemId || null, libraryItemId: libraryItemId || null,
podcastId: this.podcastId, podcastId: this.podcastId,
id: this.id, id: this.id,
@ -30,7 +77,7 @@ module.exports = (sequelize) => {
publishedAt: this.publishedAt?.valueOf() || null, publishedAt: this.publishedAt?.valueOf() || null,
addedAt: this.createdAt.valueOf(), addedAt: this.createdAt.valueOf(),
updatedAt: this.updatedAt.valueOf() updatedAt: this.updatedAt.valueOf()
} })
} }
static createFromOld(oldEpisode) { static createFromOld(oldEpisode) {
@ -63,9 +110,13 @@ module.exports = (sequelize) => {
extraData extraData
} }
} }
}
PodcastEpisode.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -97,6 +148,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
PodcastEpisode.belongsTo(podcast) PodcastEpisode.belongsTo(podcast)
}
return PodcastEpisode
} }
module.exports = PodcastEpisode

View File

@ -2,8 +2,26 @@ const { DataTypes, Model } = require('sequelize')
const oldSeries = require('../objects/entities/Series') const oldSeries = require('../objects/entities/Series')
module.exports = (sequelize) => { class Series extends Model {
class Series extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.name
/** @type {string} */
this.nameIgnorePrefix
/** @type {string} */
this.description
/** @type {UUIDV4} */
this.libraryId
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
static async getAllOldSeries() { static async getAllOldSeries() {
const series = await this.findAll() const series = await this.findAll()
return series.map(se => se.getOldSeries()) return series.map(se => se.getOldSeries())
@ -56,9 +74,22 @@ module.exports = (sequelize) => {
} }
}) })
} }
/**
* Check if series exists
* @param {string} seriesId
* @returns {Promise<boolean>}
*/
static async checkExistsById(seriesId) {
return (await this.count({ where: { id: seriesId } })) > 0
} }
Series.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -69,7 +100,24 @@ module.exports = (sequelize) => {
description: DataTypes.TEXT description: DataTypes.TEXT
}, { }, {
sequelize, sequelize,
modelName: 'series' modelName: 'series',
indexes: [
{
fields: [{
name: 'name',
collate: 'NOCASE'
}]
},
{
fields: [{
name: 'nameIgnorePrefix',
collate: 'NOCASE'
}]
},
{
fields: ['libraryId']
}
]
}) })
const { library } = sequelize.models const { library } = sequelize.models
@ -77,6 +125,7 @@ module.exports = (sequelize) => {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
Series.belongsTo(library) Series.belongsTo(library)
}
return Series
} }
module.exports = Series

View File

@ -4,8 +4,20 @@ const oldEmailSettings = require('../objects/settings/EmailSettings')
const oldServerSettings = require('../objects/settings/ServerSettings') const oldServerSettings = require('../objects/settings/ServerSettings')
const oldNotificationSettings = require('../objects/settings/NotificationSettings') const oldNotificationSettings = require('../objects/settings/NotificationSettings')
module.exports = (sequelize) => { class Setting extends Model {
class Setting extends Model { constructor(values, options) {
super(values, options)
/** @type {string} */
this.key
/** @type {Object} */
this.value
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
static async getOldSettings() { static async getOldSettings() {
const settings = (await this.findAll()).map(se => se.value) const settings = (await this.findAll()).map(se => se.value)
@ -28,9 +40,13 @@ module.exports = (sequelize) => {
value: setting value: setting
}) })
} }
}
Setting.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
key: { key: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true primaryKey: true
@ -40,6 +56,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'setting' modelName: 'setting'
}) })
}
return Setting
} }
module.exports = Setting

View File

@ -3,15 +3,45 @@ const { DataTypes, Model, Op } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
const oldUser = require('../objects/user/User') const oldUser = require('../objects/user/User')
module.exports = (sequelize) => { class User extends Model {
class User extends Model { constructor(values, options) {
super(values, options)
/** @type {UUIDV4} */
this.id
/** @type {string} */
this.username
/** @type {string} */
this.email
/** @type {string} */
this.pash
/** @type {string} */
this.type
/** @type {boolean} */
this.isActive
/** @type {boolean} */
this.isLocked
/** @type {Date} */
this.lastSeen
/** @type {Object} */
this.permissions
/** @type {Object} */
this.bookmarks
/** @type {Object} */
this.extraData
/** @type {Date} */
this.createdAt
/** @type {Date} */
this.updatedAt
}
/** /**
* Get all oldUsers * Get all oldUsers
* @returns {Promise<oldUser>} * @returns {Promise<oldUser>}
*/ */
static async getOldUsers() { static async getOldUsers() {
const users = await this.findAll({ const users = await this.findAll({
include: sequelize.models.mediaProgress include: this.sequelize.models.mediaProgress
}) })
return users.map(u => this.getOldUser(u)) return users.map(u => this.getOldUser(u))
} }
@ -139,7 +169,7 @@ module.exports = (sequelize) => {
} }
] ]
}, },
include: sequelize.models.mediaProgress include: this.sequelize.models.mediaProgress
}) })
if (!user) return null if (!user) return null
return this.getOldUser(user) return this.getOldUser(user)
@ -158,7 +188,7 @@ module.exports = (sequelize) => {
[Op.like]: username [Op.like]: username
} }
}, },
include: sequelize.models.mediaProgress include: this.sequelize.models.mediaProgress
}) })
if (!user) return null if (!user) return null
return this.getOldUser(user) return this.getOldUser(user)
@ -172,7 +202,7 @@ module.exports = (sequelize) => {
static async getUserById(userId) { static async getUserById(userId) {
if (!userId) return null if (!userId) return null
const user = await this.findByPk(userId, { const user = await this.findByPk(userId, {
include: sequelize.models.mediaProgress include: this.sequelize.models.mediaProgress
}) })
if (!user) return null if (!user) return null
return this.getOldUser(user) return this.getOldUser(user)
@ -206,9 +236,13 @@ module.exports = (sequelize) => {
}) })
return count > 0 return count > 0
} }
}
User.init({ /**
* Initialize model
* @param {import('../Database').sequelize} sequelize
*/
static init(sequelize) {
super.init({
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
@ -235,6 +269,7 @@ module.exports = (sequelize) => {
sequelize, sequelize,
modelName: 'user' modelName: 'user'
}) })
}
return User
} }
module.exports = User

View File

@ -1,5 +1,5 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require("uuid").v4
const { getTitleIgnorePrefix } = require('../../utils/index') const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class Series { class Series {
constructor(series) { constructor(series) {
@ -33,6 +33,7 @@ class Series {
return { return {
id: this.id, id: this.id,
name: this.name, name: this.name,
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
description: this.description, description: this.description,
addedAt: this.addedAt, addedAt: this.addedAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,

View File

@ -78,23 +78,22 @@ class ApiRouter {
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
this.router.delete('/libraries/:id/issues', LibraryController.middlewareNew.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middlewareNew.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
this.router.get('/libraries/:id/episode-downloads', LibraryController.middlewareNew.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) this.router.get('/libraries/:id/episode-downloads', LibraryController.middlewareNew.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middlewareNew.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middlewareNew.bind(this), LibraryController.getSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series/:seriesId', LibraryController.middlewareNew.bind(this), LibraryController.getSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/collections', LibraryController.middlewareNew.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middlewareNew.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
this.router.get('/libraries/:id/playlists', LibraryController.middlewareNew.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) this.router.get('/libraries/:id/playlists', LibraryController.middlewareNew.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
this.router.get('/libraries/:id/personalized2', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this))
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this))
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this)) this.router.get('/libraries/:id/stats', LibraryController.middlewareNew.bind(this), LibraryController.stats.bind(this))
this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this)) this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this))
this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this)) this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this))
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.updateNarrator.bind(this)) this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.updateNarrator.bind(this))
this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.removeNarrator.bind(this)) this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.removeNarrator.bind(this))
this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this)) this.router.get('/libraries/:id/matchall', LibraryController.middlewareNew.bind(this), LibraryController.matchAll.bind(this))
this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this)) this.router.post('/libraries/:id/scan', LibraryController.middlewareNew.bind(this), LibraryController.scan.bind(this))
this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) this.router.get('/libraries/:id/recent-episodes', LibraryController.middlewareNew.bind(this), LibraryController.getRecentEpisodes.bind(this))
this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this)) this.router.get('/libraries/:id/opml', LibraryController.middlewareNew.bind(this), LibraryController.getOPMLFile.bind(this))
this.router.post('/libraries/order', LibraryController.reorder.bind(this)) this.router.post('/libraries/order', LibraryController.reorder.bind(this))
// //
@ -352,34 +351,6 @@ class ApiRouter {
// //
// Helper Methods // Helper Methods
// //
userJsonWithItemProgressDetails(user, hideRootToken = false) {
const json = user.toJSONForBrowser(hideRootToken)
json.mediaProgress = json.mediaProgress.map(lip => {
const libraryItem = Database.libraryItems.find(li => li.id === lip.libraryItemId)
if (!libraryItem) {
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
lip.media = null
} else {
if (lip.episodeId) {
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(lip.episodeId) : null
if (!episode) {
Logger.warn(`[ApiRouter] Episode ${lip.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`)
lip.media = null
} else {
lip.media = libraryItem.media.toJSONExpanded()
lip.episode = episode.toJSON()
}
} else {
lip.media = libraryItem.media.toJSONExpanded()
}
}
return lip
}).filter(lip => !!lip)
return json
}
/** /**
* Remove library item and associated entities * Remove library item and associated entities
* @param {string} mediaType * @param {string} mediaType
@ -388,7 +359,7 @@ class ApiRouter {
*/ */
async handleDeleteLibraryItem(mediaType, libraryItemId, mediaItemIds) { async handleDeleteLibraryItem(mediaType, libraryItemId, mediaItemIds) {
// Remove media progress for this library item from all users // Remove media progress for this library item from all users
const users = await Database.models.user.getOldUsers() const users = await Database.userModel.getOldUsers()
for (const user of users) { for (const user of users) {
for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItemId)) { for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItemId)) {
await Database.removeMediaProgress(mediaProgress.id) await Database.removeMediaProgress(mediaProgress.id)
@ -399,14 +370,15 @@ class ApiRouter {
// Remove series if empty // Remove series if empty
if (mediaType === 'book') { if (mediaType === 'book') {
const bookSeries = await Database.models.bookSeries.findAll({ // TODO: update filter data
const bookSeries = await Database.bookSeriesModel.findAll({
where: { where: {
bookId: mediaItemIds[0] bookId: mediaItemIds[0]
}, },
include: { include: {
model: Database.models.series, model: Database.seriesModel,
include: { include: {
model: Database.models.book model: Database.bookModel
} }
} }
}) })
@ -418,7 +390,7 @@ class ApiRouter {
} }
// remove item from playlists // remove item from playlists
const playlistsWithItem = await Database.models.playlist.getPlaylistsForMediaItemIds(mediaItemIds) const playlistsWithItem = await Database.playlistModel.getPlaylistsForMediaItemIds(mediaItemIds)
for (const playlist of playlistsWithItem) { for (const playlist of playlistsWithItem) {
let numMediaItems = playlist.playlistMediaItems.length let numMediaItems = playlist.playlistMediaItems.length
@ -468,25 +440,51 @@ class ApiRouter {
}) })
} }
async checkRemoveEmptySeries(seriesToCheck, excludeLibraryItemId = null) { /**
if (!seriesToCheck?.length) return * Used when a series is removed from a book
* Series is removed if it only has 1 book
*
* @param {string} bookId
* @param {string[]} seriesIds
*/
async checkRemoveEmptySeries(bookId, seriesIds) {
if (!seriesIds?.length) return
for (const series of seriesToCheck) { const bookSeries = await Database.bookSeriesModel.findAll({
const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id)) where: {
if (!otherLibraryItemsInSeries.length) { bookId,
// Close open RSS feed for series seriesId: seriesIds
await this.rssFeedManager.closeFeedForEntityId(series.id) },
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) include: [
await Database.removeSeries(series.id) {
// TODO: Socket events for series? model: Database.seriesModel,
include: {
model: Database.bookModel
}
}
]
})
for (const bs of bookSeries) {
if (bs.series.books.length === 1) {
await this.removeEmptySeries(bs.series)
} }
} }
} }
/**
* Remove an empty series & close an open RSS feed
* @param {import('../models/Series')} series
*/
async removeEmptySeries(series) { async removeEmptySeries(series) {
await this.rssFeedManager.closeFeedForEntityId(series.id) await this.rssFeedManager.closeFeedForEntityId(series.id)
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
await Database.removeSeries(series.id) await Database.removeSeries(series.id)
// Remove series from library filter data
Database.removeSeriesFromFilterData(series.libraryId, series.id)
SocketAuthority.emitter('series_removed', {
id: series.id,
libraryId: series.libraryId
})
} }
async getUserListeningSessionsHelper(userId) { async getUserListeningSessionsHelper(userId) {
@ -497,7 +495,7 @@ class ApiRouter {
async getAllSessionsWithUserData() { async getAllSessionsWithUserData() {
const sessions = await Database.getPlaybackSessions() const sessions = await Database.getPlaybackSessions()
sessions.sort((a, b) => b.updatedAt - a.updatedAt) sessions.sort((a, b) => b.updatedAt - a.updatedAt)
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects() const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects()
return sessions.map(se => { return sessions.map(se => {
return { return {
...se, ...se,
@ -557,7 +555,7 @@ class ApiRouter {
const mediaMetadata = mediaPayload.metadata const mediaMetadata = mediaPayload.metadata
// Create new authors if in payload // Create new authors if in payload
if (mediaMetadata.authors && mediaMetadata.authors.length) { if (mediaMetadata.authors?.length) {
const newAuthors = [] const newAuthors = []
for (let i = 0; i < mediaMetadata.authors.length; i++) { for (let i = 0; i < mediaMetadata.authors.length; i++) {
const authorName = (mediaMetadata.authors[i].name || '').trim() const authorName = (mediaMetadata.authors[i].name || '').trim()
@ -566,6 +564,12 @@ class ApiRouter {
continue continue
} }
// Ensure the ID for the author exists
if (mediaMetadata.authors[i].id && !(await Database.checkAuthorExists(libraryId, mediaMetadata.authors[i].id))) {
Logger.warn(`[ApiRouter] Author id "${mediaMetadata.authors[i].id}" does not exist`)
mediaMetadata.authors[i].id = null
}
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) { if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
let author = Database.authors.find(au => au.libraryId === libraryId && au.checkNameEquals(authorName)) let author = Database.authors.find(au => au.libraryId === libraryId && au.checkNameEquals(authorName))
if (!author) { if (!author) {
@ -573,6 +577,8 @@ class ApiRouter {
author.setData(mediaMetadata.authors[i], libraryId) author.setData(mediaMetadata.authors[i], libraryId)
Logger.debug(`[ApiRouter] Created new author "${author.name}"`) Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
newAuthors.push(author) newAuthors.push(author)
// Update filter data
Database.addAuthorToFilterData(libraryId, author.name, author.id)
} }
// Update ID in original payload // Update ID in original payload
@ -595,6 +601,12 @@ class ApiRouter {
continue continue
} }
// Ensure the ID for the series exists
if (mediaMetadata.series[i].id && !(await Database.checkSeriesExists(libraryId, mediaMetadata.series[i].id))) {
Logger.warn(`[ApiRouter] Series id "${mediaMetadata.series[i].id}" does not exist`)
mediaMetadata.series[i].id = null
}
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) { if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
let seriesItem = Database.series.find(se => se.libraryId === libraryId && se.checkNameEquals(seriesName)) let seriesItem = Database.series.find(se => se.libraryId === libraryId && se.checkNameEquals(seriesName))
if (!seriesItem) { if (!seriesItem) {
@ -602,6 +614,8 @@ class ApiRouter {
seriesItem.setData(mediaMetadata.series[i], libraryId) seriesItem.setData(mediaMetadata.series[i], libraryId)
Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`) Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
newSeries.push(seriesItem) newSeries.push(seriesItem)
// Update filter data
Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id)
} }
// Update ID in original payload // Update ID in original payload

View File

@ -1,3 +1,4 @@
const Sequelize = require('sequelize')
const fs = require('../libs/fsExtra') const fs = require('../libs/fsExtra')
const Path = require('path') const Path = require('path')
const Logger = require('../Logger') const Logger = require('../Logger')
@ -66,7 +67,7 @@ class Scanner {
} }
async scanLibraryItemByRequest(libraryItem) { async scanLibraryItemByRequest(libraryItem) {
const library = await Database.models.library.getOldById(libraryItem.libraryId) const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
if (!library) { if (!library) {
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
return ScanResult.NOTHING return ScanResult.NOTHING
@ -485,6 +486,8 @@ class Scanner {
_author = new Author() _author = new Author()
_author.setData(tempMinAuthor, libraryItem.libraryId) _author.setData(tempMinAuthor, libraryItem.libraryId)
newAuthors.push(_author) newAuthors.push(_author)
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, _author.name, _author.id)
} }
return { return {
@ -501,11 +504,17 @@ class Scanner {
const newSeries = [] const newSeries = []
libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => { libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => {
let _series = Database.series.find(se => se.libraryId === libraryItem.libraryId && se.checkNameEquals(tempMinSeries.name)) let _series = Database.series.find(se => se.libraryId === libraryItem.libraryId && se.checkNameEquals(tempMinSeries.name))
if (!_series) _series = newSeries.find(se => se.libraryId === libraryItem.libraryId && se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series if (!_series) {
// Check new unsaved series
_series = newSeries.find(se => se.libraryId === libraryItem.libraryId && se.checkNameEquals(tempMinSeries.name))
}
if (!_series) { // Must create new series if (!_series) { // Must create new series
_series = new Series() _series = new Series()
_series.setData(tempMinSeries, libraryItem.libraryId) _series.setData(tempMinSeries, libraryItem.libraryId)
newSeries.push(_series) newSeries.push(_series)
// Update filter data
Database.addSeriesToFilterData(libraryItem.libraryId, _series.name, _series.id)
} }
return { return {
id: _series.id, id: _series.id,
@ -552,25 +561,30 @@ class Scanner {
for (const folderId in folderGroups) { for (const folderId in folderGroups) {
const libraryId = folderGroups[folderId].libraryId const libraryId = folderGroups[folderId].libraryId
const library = await Database.models.library.getOldById(libraryId) const library = await Database.libraryModel.getOldById(libraryId)
if (!library) { if (!library) {
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
continue; continue
} }
const folder = library.getFolderById(folderId) const folder = library.getFolderById(folderId)
if (!folder) { if (!folder) {
Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`) Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
continue; continue
} }
const relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) const relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
const fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths, false) const fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths, false)
if (!Object.keys(fileUpdateGroup).length) { if (!Object.keys(fileUpdateGroup).length) {
Logger.info(`[Scanner] No important changes to scan for in folder "${folderId}"`) Logger.info(`[Scanner] No important changes to scan for in folder "${folderId}"`)
continue; continue
} }
const folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) const folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
Logger.debug(`[Scanner] Folder scan results`, folderScanResults) Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
// If something was updated then reset numIssues filter data for library
if (Object.values(folderScanResults).some(scanResult => scanResult !== ScanResult.NOTHING && scanResult !== ScanResult.UPTODATE)) {
await Database.resetLibraryIssuesFilterData(libraryId)
}
} }
this.scanningFilesChanged = false this.scanningFilesChanged = false
@ -589,28 +603,49 @@ class Scanner {
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item // Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
const updateGroup = { ...fileUpdateGroup } const updateGroup = { ...fileUpdateGroup }
for (const itemDir in updateGroup) { for (const itemDir in updateGroup) {
if (itemDir == fileUpdateGroup[itemDir]) continue; // Media in root path if (itemDir == fileUpdateGroup[itemDir]) continue // Media in root path
const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/'))
if (!itemDirNestedFiles.length) continue; if (!itemDirNestedFiles.length) continue
const firstNest = itemDirNestedFiles[0].split('/').shift() const firstNest = itemDirNestedFiles[0].split('/').shift()
const altDir = `${itemDir}/${firstNest}` const altDir = `${itemDir}/${firstNest}`
const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir)
const childLibraryItem = Database.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath)) const childLibraryItem = await Database.libraryItemModel.findOne({
attributes: ['id', 'path'],
where: {
path: {
[Sequelize.Op.not]: fullPath
},
path: {
[Sequelize.Op.startsWith]: fullPath
}
}
})
if (!childLibraryItem) { if (!childLibraryItem) {
continue continue
} }
const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir) const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir)
const altChildLibraryItem = Database.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath)) const altChildLibraryItem = await Database.libraryItemModel.findOne({
attributes: ['id', 'path'],
where: {
path: {
[Sequelize.Op.not]: altFullPath
},
path: {
[Sequelize.Op.startsWith]: altFullPath
}
}
})
if (altChildLibraryItem) { if (altChildLibraryItem) {
continue continue
} }
delete fileUpdateGroup[itemDir] delete fileUpdateGroup[itemDir]
fileUpdateGroup[altDir] = itemDirNestedFiles.map((f) => f.split('/').slice(1).join('/')) fileUpdateGroup[altDir] = itemDirNestedFiles.map((f) => f.split('/').slice(1).join('/'))
Logger.warn(`[Scanner] Some files were modified in a parent directory of a library item "${childLibraryItem.title}" - ignoring`) Logger.warn(`[Scanner] Some files were modified in a parent directory of a library item "${childLibraryItem.path}" - ignoring`)
} }
// Second pass: Check for new/updated/removed items // Second pass: Check for new/updated/removed items
@ -619,10 +654,21 @@ class Scanner {
const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir)
const dirIno = await getIno(fullPath) const dirIno = await getIno(fullPath)
const itemDirParts = itemDir.split('/').slice(0, -1)
const potentialChildDirs = []
for (let i = 0; i < itemDirParts.length; i++) {
potentialChildDirs.push(Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir.split('/').slice(0, -1 - i).join('/')))
}
// Check if book dir group is already an item // Check if book dir group is already an item
let existingLibraryItem = Database.libraryItems.find(li => fullPath.startsWith(li.path)) let existingLibraryItem = await Database.libraryItemModel.findOneOld({
path: potentialChildDirs
})
if (!existingLibraryItem) { if (!existingLibraryItem) {
existingLibraryItem = Database.libraryItems.find(li => li.ino === dirIno) existingLibraryItem = await Database.libraryItemModel.findOneOld({
ino: dirIno
})
if (existingLibraryItem) { if (existingLibraryItem) {
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`) Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`)
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData // Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
@ -655,9 +701,16 @@ class Scanner {
} }
// Check if a library item is a subdirectory of this dir // Check if a library item is a subdirectory of this dir
var childItem = Database.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/')) const childItem = await Database.libraryItemModel.findOne({
attributes: ['id', 'path'],
where: {
path: {
[Sequelize.Op.startsWith]: fullPath + '/'
}
}
})
if (childItem) { if (childItem) {
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`) Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.path}" - ignoring`)
itemGroupingResults[itemDir] = ScanResult.NOTHING itemGroupingResults[itemDir] = ScanResult.NOTHING
continue continue
} }
@ -884,6 +937,8 @@ class Scanner {
author.setData({ name: authorName }, libraryItem.libraryId) author.setData({ name: authorName }, libraryItem.libraryId)
await Database.createAuthor(author) await Database.createAuthor(author)
SocketAuthority.emitter('author_added', author.toJSON()) SocketAuthority.emitter('author_added', author.toJSON())
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
} }
authorPayload.push(author.toJSONMinimal()) authorPayload.push(author.toJSONMinimal())
} }
@ -900,6 +955,8 @@ class Scanner {
seriesItem = new Series() seriesItem = new Series()
seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId) seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId)
await Database.createSeries(seriesItem) await Database.createSeries(seriesItem)
// Update filter data
Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id)
SocketAuthority.emitter('series_added', seriesItem.toJSON()) SocketAuthority.emitter('series_added', seriesItem.toJSON())
} }
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))

View File

@ -1,23 +1,29 @@
const xml = require('../../libs/xml') const xml = require('../../libs/xml')
module.exports.generate = (libraryItems, indent = true) => { /**
* Generate OPML file string for podcasts in a library
* @param {import('../../models/Podcast')[]} podcasts
* @param {boolean} [indent=true]
* @returns {string}
*/
module.exports.generate = (podcasts, indent = true) => {
const bodyItems = [] const bodyItems = []
libraryItems.forEach((item) => { podcasts.forEach((podcast) => {
if (!item.media.metadata.feedUrl) return if (!podcast.feedURL) return
const feedAttributes = { const feedAttributes = {
type: 'rss', type: 'rss',
text: item.media.metadata.title, text: podcast.title,
title: item.media.metadata.title, title: podcast.title,
xmlUrl: item.media.metadata.feedUrl xmlUrl: podcast.feedURL
} }
if (item.media.metadata.description) { if (podcast.description) {
feedAttributes.description = item.media.metadata.description feedAttributes.description = podcast.description
} }
if (item.media.metadata.itunesPageUrl) { if (podcast.itunesPageUrl) {
feedAttributes.htmlUrl = item.media.metadata.itunesPageUrl feedAttributes.htmlUrl = podcast.itunesPageUrl
} }
if (item.media.metadata.language) { if (podcast.language) {
feedAttributes.language = item.media.metadata.language feedAttributes.language = podcast.language
} }
bodyItems.push({ bodyItems.push({
outline: { outline: {

View File

@ -1,5 +1,4 @@
const { sort, createNewSortInstance } = require('../libs/fastSort') const { createNewSortInstance } = require('../libs/fastSort')
const Logger = require('../Logger')
const Database = require('../Database') const Database = require('../Database')
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index') const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
const naturalSort = createNewSortInstance({ const naturalSort = createNewSortInstance({
@ -72,7 +71,7 @@ module.exports = {
} else if (filterBy === 'issues') { } else if (filterBy === 'issues') {
filtered = filtered.filter(li => li.hasIssues) filtered = filtered.filter(li => li.hasIssues)
} else if (filterBy === 'feed-open') { } else if (filterBy === 'feed-open') {
const libraryItemIdsWithFeed = await Database.models.feed.findAllLibraryItemIds() const libraryItemIdsWithFeed = await Database.feedModel.findAllLibraryItemIds()
filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id)) filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id))
} else if (filterBy === 'abridged') { } else if (filterBy === 'abridged') {
filtered = filtered.filter(li => !!li.media.metadata?.abridged) filtered = filtered.filter(li => !!li.media.metadata?.abridged)
@ -126,60 +125,6 @@ module.exports = {
return true return true
}, },
getDistinctFilterDataNew(libraryItems) {
const data = {
authors: [],
genres: [],
tags: [],
series: [],
narrators: [],
languages: [],
publishers: []
}
libraryItems.forEach((li) => {
const mediaMetadata = li.media.metadata
if (mediaMetadata.authors?.length) {
mediaMetadata.authors.forEach((author) => {
if (author && !data.authors.some(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
})
}
if (mediaMetadata.series?.length) {
mediaMetadata.series.forEach((series) => {
if (series && !data.series.some(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
})
}
if (mediaMetadata.genres?.length) {
mediaMetadata.genres.forEach((genre) => {
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
})
}
if (li.media.tags.length) {
li.media.tags.forEach((tag) => {
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
})
}
if (mediaMetadata.narrators?.length) {
mediaMetadata.narrators.forEach((narrator) => {
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
})
}
if (mediaMetadata.publisher && !data.publishers.includes(mediaMetadata.publisher)) {
data.publishers.push(mediaMetadata.publisher)
}
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) {
data.languages.push(mediaMetadata.language)
}
})
data.authors = naturalSort(data.authors).asc(au => au.name)
data.genres = naturalSort(data.genres).asc()
data.tags = naturalSort(data.tags).asc()
data.series = naturalSort(data.series).asc(se => se.name)
data.narrators = naturalSort(data.narrators).asc()
data.publishers = naturalSort(data.publishers).asc()
data.languages = naturalSort(data.languages).asc()
return data
},
getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified, hideSingleBookSeries) { getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified, hideSingleBookSeries) {
const _series = {} const _series = {}
const seriesToFilterOut = {} const seriesToFilterOut = {}
@ -246,89 +191,6 @@ module.exports = {
}) })
}, },
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
var incompleteSeires = seriesWithUserAb.filter((series) => series.books.some((book) => !book.userAudiobook || (!book.userAudiobook.isRead && book.userAudiobook.progress == 0)))
var booksNextInSeries = []
incompleteSeires.forEach((series) => {
var dateLastRead = series.books.filter((data) => data.userAudiobook && data.userAudiobook.isRead).sort((a, b) => { return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt })[0].userAudiobook.finishedAt
var nextUnreadBook = series.books.filter((data) => !data.userAudiobook || (!data.userAudiobook.isRead && data.userAudiobook.progress == 0))[0]
nextUnreadBook.DateLastReadSeries = dateLastRead
booksNextInSeries.push(nextUnreadBook)
})
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
},
getGenresWithCount(libraryItems) {
var genresMap = {}
libraryItems.forEach((li) => {
var genres = li.media.metadata.genres || []
genres.forEach((genre) => {
if (genresMap[genre]) genresMap[genre].count++
else
genresMap[genre] = {
genre,
count: 1
}
})
})
return Object.values(genresMap).sort((a, b) => b.count - a.count)
},
getAuthorsWithCount(libraryItems) {
var authorsMap = {}
libraryItems.forEach((li) => {
var authors = li.media.metadata.authors || []
authors.forEach((author) => {
if (authorsMap[author.id]) authorsMap[author.id].count++
else
authorsMap[author.id] = {
id: author.id,
name: author.name,
count: 1
}
})
})
return Object.values(authorsMap).sort((a, b) => b.count - a.count)
},
getItemDurationStats(libraryItems) {
var sorted = sort(libraryItems).desc(li => li.media.duration)
var top10 = sorted.slice(0, 10).map(li => ({ id: li.id, title: li.media.metadata.title, duration: li.media.duration })).filter(i => i.duration > 0)
var totalDuration = 0
var numAudioTracks = 0
libraryItems.forEach((li) => {
totalDuration += li.media.duration
numAudioTracks += li.media.numTracks
})
return {
totalDuration,
numAudioTracks,
longestItems: top10
}
},
getItemSizeStats(libraryItems) {
var sorted = sort(libraryItems).desc(li => li.media.size)
var top10 = sorted.slice(0, 10).map(li => ({ id: li.id, title: li.media.metadata.title, size: li.media.size })).filter(i => i.size > 0)
var totalSize = 0
libraryItems.forEach((li) => {
totalSize += li.media.size
})
return {
totalSize,
largestItems: top10
}
},
getLibraryItemsTotalSize(libraryItems) {
var totalSize = 0
libraryItems.forEach((li) => {
totalSize += li.media.size
})
return totalSize
},
collapseBookSeries(libraryItems, series, filterSeries, hideSingleBookSeries) { collapseBookSeries(libraryItems, series, filterSeries, hideSingleBookSeries) {
// Get series from the library items. If this list is being collapsed after filtering for a series, // Get series from the library items. If this list is being collapsed after filtering for a series,
// don't collapse that series, only books that are in other series. // don't collapse that series, only books that are in other series.
@ -356,550 +218,5 @@ module.exports = {
}) })
return filteredLibraryItems return filteredLibraryItems
},
async buildPersonalizedShelves(ctx, user, libraryItems, library, maxEntitiesPerShelf, include) {
const mediaType = library.mediaType
const isPodcastLibrary = mediaType === 'podcast'
const includeRssFeed = include.includes('rssfeed')
const includeNumEpisodesIncomplete = include.includes('numepisodesincomplete') // Podcasts only
const hideSingleBookSeries = library.settings.hideSingleBookSeries
const shelves = [
{
id: 'continue-listening',
label: 'Continue Listening',
labelStringKey: 'LabelContinueListening',
type: isPodcastLibrary ? 'episode' : mediaType,
entities: []
},
{
id: 'continue-reading',
label: 'Continue Reading',
labelStringKey: 'LabelContinueReading',
type: 'book',
entities: []
},
{
id: 'continue-series',
label: 'Continue Series',
labelStringKey: 'LabelContinueSeries',
type: mediaType,
entities: []
},
{
id: 'episodes-recently-added',
label: 'Newest Episodes',
labelStringKey: 'LabelNewestEpisodes',
type: 'episode',
entities: []
},
{
id: 'recently-added',
label: 'Recently Added',
labelStringKey: 'LabelRecentlyAdded',
type: mediaType,
entities: []
},
{
id: 'recent-series',
label: 'Recent Series',
labelStringKey: 'LabelRecentSeries',
type: 'series',
entities: []
},
{
id: 'recommended',
label: 'Recommended',
labelStringKey: 'LabelRecommended',
type: mediaType,
entities: []
},
{
id: 'listen-again',
label: 'Listen Again',
labelStringKey: 'LabelListenAgain',
type: isPodcastLibrary ? 'episode' : mediaType,
entities: []
},
{
id: 'read-again',
label: 'Read Again',
labelStringKey: 'LabelReadAgain',
type: 'book',
entities: []
},
{
id: 'newest-authors',
label: 'Newest Authors',
labelStringKey: 'LabelNewestAuthors',
type: 'authors',
entities: []
}
]
const categoryMap = {}
shelves.forEach((shelf) => {
categoryMap[shelf.id] = {
id: shelf.id,
biggest: 0,
smallest: 0,
items: []
}
})
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['recently-added'].smallest) {
const libraryItemObj = libraryItem.toJSONMinified()
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
if (includeNumEpisodesIncomplete && libraryItem.isPodcast) {
libraryItemObj.numEpisodesIncomplete = user.getNumEpisodesIncompleteForPodcast(libraryItem)
}
const indexToPut = categoryMap['recently-added'].items.findIndex(i => libraryItem.addedAt > i.addedAt)
if (indexToPut >= 0) {
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItemObj)
} else {
categoryMap['recently-added'].items.push(libraryItemObj)
}
if (categoryMap['recently-added'].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap['recently-added'].items.pop()
categoryMap['recently-added'].smallest = categoryMap['recently-added'].items[categoryMap['recently-added'].items.length - 1].addedAt
}
categoryMap['recently-added'].biggest = categoryMap['recently-added'].items[0].addedAt
}
const allItemProgress = user.getAllMediaProgressForLibraryItem(libraryItem.id)
if (libraryItem.isPodcast) {
// Podcast categories
const podcastEpisodes = libraryItem.media.episodes || []
for (const episode of podcastEpisodes) {
const mediaProgress = allItemProgress.find(mp => mp.episodeId === episode.id)
// Newest episodes
if (!mediaProgress?.isFinished && episode.addedAt > categoryMap['episodes-recently-added'].smallest) {
const libraryItemWithEpisode = {
...libraryItem.toJSONMinified(),
recentEpisode: episode.toJSON()
}
const indexToPut = categoryMap['episodes-recently-added'].items.findIndex(i => episode.addedAt > i.recentEpisode.addedAt)
if (indexToPut >= 0) {
categoryMap['episodes-recently-added'].items.splice(indexToPut, 0, libraryItemWithEpisode)
} else {
categoryMap['episodes-recently-added'].items.push(libraryItemWithEpisode)
}
if (categoryMap['episodes-recently-added'].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap['episodes-recently-added'].items.pop()
categoryMap['episodes-recently-added'].smallest = categoryMap['episodes-recently-added'].items[categoryMap['episodes-recently-added'].items.length - 1].recentEpisode.addedAt
}
categoryMap['episodes-recently-added'].biggest = categoryMap['episodes-recently-added'].items[0].recentEpisode.addedAt
}
// Episode recently listened and finished
if (mediaProgress) {
if (mediaProgress.isFinished) {
if (mediaProgress.finishedAt > categoryMap['listen-again'].smallest) { // Item belongs on shelf
const libraryItemWithEpisode = {
...libraryItem.toJSONMinified(),
recentEpisode: episode.toJSON(),
finishedAt: mediaProgress.finishedAt
}
const indexToPut = categoryMap['listen-again'].items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
if (indexToPut >= 0) {
categoryMap['listen-again'].items.splice(indexToPut, 0, libraryItemWithEpisode)
} else {
categoryMap['listen-again'].items.push(libraryItemWithEpisode)
}
if (categoryMap['listen-again'].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap['listen-again'].items.pop()
categoryMap['listen-again'].smallest = categoryMap['listen-again'].items[categoryMap['listen-again'].items.length - 1].finishedAt
}
categoryMap['listen-again'].biggest = categoryMap['listen-again'].items[0].finishedAt
}
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
if (mediaProgress.lastUpdate > categoryMap['continue-listening'].smallest) { // Item belongs on shelf
const libraryItemWithEpisode = {
...libraryItem.toJSONMinified(),
recentEpisode: episode.toJSON(),
progressLastUpdate: mediaProgress.lastUpdate
}
const indexToPut = categoryMap['continue-listening'].items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
if (indexToPut >= 0) {
categoryMap['continue-listening'].items.splice(indexToPut, 0, libraryItemWithEpisode)
} else {
categoryMap['continue-listening'].items.push(libraryItemWithEpisode)
}
if (categoryMap['continue-listening'].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap['continue-listening'].items.pop()
categoryMap['continue-listening'].smallest = categoryMap['continue-listening'].items[categoryMap['continue-listening'].items.length - 1].progressLastUpdate
}
categoryMap['continue-listening'].biggest = categoryMap['continue-listening'].items[0].progressLastUpdate
}
}
}
}
} 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 bookInProgress = mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)
const bookActive = mediaProgress && mediaProgress.inProgress && !mediaProgress.isFinished
const libraryItemJson = libraryItem.toJSONMinified()
libraryItemJson.seriesSequence = librarySeries.sequence
const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id)
if (!seriesMap[librarySeries.id]) {
const seriesObj = Database.series.find(se => se.id === librarySeries.id)
if (seriesObj) {
const series = {
...seriesObj.toJSON(),
books: [libraryItemJson],
inProgress: bookInProgress,
hasActiveBook: bookActive,
hideFromContinueListening,
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
firstBookUnread: bookInProgress ? null : libraryItemJson
}
seriesMap[librarySeries.id] = series
const indexToPut = categoryMap['recent-series'].items.findIndex(i => series.addedAt > i.addedAt)
if (indexToPut >= 0) {
categoryMap['recent-series'].items.splice(indexToPut, 0, series)
} else {
categoryMap['recent-series'].items.push(series)
}
}
} else {
// series already in map - add book
seriesMap[librarySeries.id].books.push(libraryItemJson)
if (bookInProgress) { // Update if this series is in progress
seriesMap[librarySeries.id].inProgress = true
if (seriesMap[librarySeries.id].bookInProgressLastUpdate < mediaProgress.lastUpdate) {
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
}
} else if (!seriesMap[librarySeries.id].firstBookUnread) {
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
} else if (libraryItemJson.seriesSequence) {
// If current firstBookUnread has a series sequence greater than this series sequence, then update firstBookUnread
const firstBookUnreadSequence = seriesMap[librarySeries.id].firstBookUnread.seriesSequence
if (!firstBookUnreadSequence || String(firstBookUnreadSequence).localeCompare(String(librarySeries.sequence), undefined, { sensitivity: 'base', numeric: true }) > 0) {
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
}
}
// Update if series has an active (progress < 100%) book
if (bookActive) {
seriesMap[librarySeries.id].hasActiveBook = true
}
}
}
}
// Newest authors
if (libraryItem.media.metadata.authors.length) {
for (const libraryAuthor of libraryItem.media.metadata.authors) {
if (!authorMap[libraryAuthor.id]) {
const authorObj = Database.authors.find(au => au.id === libraryAuthor.id)
if (authorObj) {
const author = {
...authorObj.toJSON(),
numBooks: 1
}
if (author.addedAt > categoryMap['newest-authors'].smallest) {
const indexToPut = categoryMap['newest-authors'].items.findIndex(i => author.addedAt > i.addedAt)
if (indexToPut >= 0) {
categoryMap['newest-authors'].items.splice(indexToPut, 0, author)
} else {
categoryMap['newest-authors'].items.push(author)
}
// Max authors is 10
if (categoryMap['newest-authors'].items.length > 10) {
categoryMap['newest-authors'].items.pop()
categoryMap['newest-authors'].smallest = categoryMap['newest-authors'].items[categoryMap['newest-authors'].items.length - 1].addedAt
}
categoryMap['newest-authors'].biggest = categoryMap['newest-authors'].items[0].addedAt
}
authorMap[libraryAuthor.id] = author
}
} else {
authorMap[libraryAuthor.id].numBooks++
}
}
}
// Book listening and finished
if (mediaProgress) {
const categoryId = libraryItem.media.isEBookOnly ? 'read-again' : 'listen-again'
// Handle most recently finished
if (mediaProgress.isFinished) {
if (mediaProgress.finishedAt > categoryMap[categoryId].smallest) { // Item belongs on shelf
const libraryItemObj = {
...libraryItem.toJSONMinified(),
finishedAt: mediaProgress.finishedAt
}
const indexToPut = categoryMap[categoryId].items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
if (indexToPut >= 0) {
categoryMap[categoryId].items.splice(indexToPut, 0, libraryItemObj)
} else {
categoryMap[categoryId].items.push(libraryItemObj)
}
if (categoryMap[categoryId].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap[categoryId].items.pop()
categoryMap[categoryId].smallest = categoryMap[categoryId].items[categoryMap[categoryId].items.length - 1].finishedAt
}
categoryMap[categoryId].biggest = categoryMap[categoryId].items[0].finishedAt
}
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
const categoryId = libraryItem.media.isEBookOnly ? 'continue-reading' : 'continue-listening'
if (mediaProgress.lastUpdate > categoryMap[categoryId].smallest) { // Item belongs on shelf
const libraryItemObj = {
...libraryItem.toJSONMinified(),
progressLastUpdate: mediaProgress.lastUpdate
}
const indexToPut = categoryMap[categoryId].items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
if (indexToPut >= 0) {
categoryMap[categoryId].items.splice(indexToPut, 0, libraryItemObj)
} else { // Should only happen when array is < max
categoryMap[categoryId].items.push(libraryItemObj)
}
if (categoryMap[categoryId].items.length > maxEntitiesPerShelf) {
// Remove last item
categoryMap[categoryId].items.pop()
categoryMap[categoryId].smallest = categoryMap[categoryId].items[categoryMap[categoryId].items.length - 1].progressLastUpdate
}
categoryMap[categoryId].biggest = categoryMap[categoryId].items[0].progressLastUpdate
}
}
}
}
}
// For Continue Series - Find next book in series for series that are in progress
for (const seriesId in seriesMap) {
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
const nextBookInSeries = seriesMap[seriesId].firstBookUnread
if (!hasActiveBook && nextBookInSeries) {
const bookForContinueSeries = {
...nextBookInSeries,
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
}
bookForContinueSeries.media.metadata.series = {
id: seriesId,
name: seriesMap[seriesId].name,
sequence: nextBookInSeries.seriesSequence
}
const indexToPut = categoryMap['continue-series'].items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
if (!categoryMap['continue-series'].items.find(book => book.id === bookForContinueSeries.id)) {
if (indexToPut >= 0) {
categoryMap['continue-series'].items.splice(indexToPut, 0, bookForContinueSeries)
} else if (categoryMap['continue-series'].items.length < 10) { // Max 10 books
categoryMap['continue-series'].items.push(bookForContinueSeries)
}
}
}
}
}
// 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['recent-series'].items.length) {
if (hideSingleBookSeries) {
categoryMap['recent-series'].items = categoryMap['recent-series'].items.filter(seriesItem => seriesItem.books.length > 1)
}
// Limit series shown to 5
categoryMap['recent-series'].items = categoryMap['recent-series'].items.slice(0, 5)
for (const seriesItem of categoryMap['recent-series'].items) {
seriesItem.books = naturalSort(seriesItem.books).asc(li => li.seriesSequence)
}
}
const categoriesWithItems = Object.values(categoryMap).filter(cat => cat.items.length)
const finalShelves = []
for (const categoryWithItems of categoriesWithItems) {
const shelf = shelves.find(s => s.id === categoryWithItems.id)
shelf.entities = categoryWithItems.items
// Add rssFeed to entities if query string "include=rssfeed" was on request
if (includeRssFeed) {
if (shelf.type === 'book' || shelf.type === 'podcast') {
shelf.entities = await Promise.all(shelf.entities.map(async (item) => {
const feed = await ctx.rssFeedManager.findFeedForEntityId(item.id)
item.rssFeed = feed?.toJSONMinified() || null
return item
}))
} else if (shelf.type === 'series') {
shelf.entities = await Promise.all(shelf.entities.map(async (series) => {
const feed = await ctx.rssFeedManager.findFeedForEntityId(series.id)
series.rssFeed = feed?.toJSONMinified() || null
return series
}))
}
}
finalShelves.push(shelf)
}
return finalShelves
},
groupMusicLibraryItemsIntoAlbums(libraryItems) {
const albums = {}
libraryItems.forEach((li) => {
const albumTitle = li.media.metadata.album
const albumArtist = li.media.metadata.albumArtist
if (albumTitle && !albums[albumTitle]) {
albums[albumTitle] = {
title: albumTitle,
artist: albumArtist,
libraryItemId: li.media.coverPath ? li.id : null,
numTracks: 1
}
} else if (albumTitle && albums[albumTitle].artist === albumArtist) {
if (!albums[albumTitle].libraryItemId && li.media.coverPath) albums[albumTitle].libraryItemId = li.id
albums[albumTitle].numTracks++
} else {
if (albumTitle) {
Logger.warn(`Music track "${li.media.metadata.title}" with album "${albumTitle}" has a different album artist then another track in the same album. This track album artist is "${albumArtist}" but the album artist is already set to "${albums[albumTitle].artist}"`)
}
if (!albums['_none_']) albums['_none_'] = { title: 'No Album', artist: 'Various Artists', libraryItemId: null, numTracks: 0 }
albums['_none_'].numTracks++
}
})
return Object.values(albums)
} }
} }

View File

@ -0,0 +1,70 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
module.exports = {
/**
* Get authors with count of num books
* @param {string} libraryId
* @returns {{id:string, name:string, count:number}}
*/
async getAuthorsWithCount(libraryId) {
const authors = await Database.authorModel.findAll({
where: [
{
libraryId
},
Sequelize.where(Sequelize.literal('count'), {
[Sequelize.Op.gt]: 0
})
],
attributes: [
'id',
'name',
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'count']
],
order: [
['count', 'DESC']
]
})
return authors.map(au => {
return {
id: au.id,
name: au.name,
count: au.dataValues.count
}
})
},
/**
* Search authors
* @param {string} libraryId
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {object[]} oldAuthor with numBooks
*/
async search(libraryId, query, limit, offset) {
const authors = await Database.authorModel.findAll({
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId
},
attributes: {
include: [
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
]
},
limit,
offset
})
const authorMatches = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.dataValues.numBooks
authorMatches.push(oldAuthor)
}
return authorMatches
}
}

View File

@ -15,8 +15,8 @@ module.exports = {
/** /**
* Get library items using filter and sort * Get library items using filter and sort
* @param {oldLibrary} library * @param {import('../../objects/Library')} library
* @param {oldUser} user * @param {import('../../objects/user/User')} user
* @param {object} options * @param {object} options
* @returns {object} { libraryItems:LibraryItem[], count:number } * @returns {object} { libraryItems:LibraryItem[], count:number }
*/ */
@ -41,20 +41,20 @@ module.exports = {
/** /**
* Get library items for continue listening & continue reading shelves * Get library items for continue listening & continue reading shelves
* @param {oldLibrary} library * @param {import('../../objects/Library')} library
* @param {oldUser} user * @param {import('../../objects/user/User')} user
* @param {string[]} include * @param {string[]} include
* @param {number} limit * @param {number} limit
* @returns {object} { items:LibraryItem[], count:number } * @returns {Promise<{ items:import('../../models/LibraryItem')[], count:number }>}
*/ */
async getMediaItemsInProgress(library, user, include, limit) { async getMediaItemsInProgress(library, user, include, limit) {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0)
return { return {
items: libraryItems.map(li => { items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
return oldLibraryItem return oldLibraryItem
}), }),
@ -65,7 +65,7 @@ module.exports = {
return { return {
count, count,
items: libraryItems.map(li => { items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
}) })
@ -86,9 +86,9 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.size && !oldLibraryItem.media.size) { if (li.size && !oldLibraryItem.media.size) {
oldLibraryItem.media.size = li.size oldLibraryItem.media.size = li.size
@ -101,9 +101,9 @@ module.exports = {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0) const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.size && !oldLibraryItem.media.size) { if (li.size && !oldLibraryItem.media.size) {
oldLibraryItem.media.size = li.size oldLibraryItem.media.size = li.size
@ -127,9 +127,9 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, user, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, user, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.series) { if (li.series) {
oldLibraryItem.media.metadata.series = li.series oldLibraryItem.media.metadata.series = li.series
@ -153,9 +153,9 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0)
return { return {
items: libraryItems.map(li => { items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
return oldLibraryItem return oldLibraryItem
}), }),
@ -166,7 +166,7 @@ module.exports = {
return { return {
count, count,
items: libraryItems.map(li => { items: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
}) })
@ -176,19 +176,19 @@ module.exports = {
/** /**
* Get series for recent series shelf * Get series for recent series shelf
* @param {oldLibrary} library * @param {import('../../objects/Library')} library
* @param {oldUser} user * @param {import('../../objects/user/User')} user
* @param {string[]} include * @param {string[]} include
* @param {number} limit * @param {number} limit
* @returns {object} { series:oldSeries[], count:number} * @returns {{ series:import('../../objects/entities/Series')[], count:number}}
*/ */
async getSeriesMostRecentlyAdded(library, user, include, limit) { async getSeriesMostRecentlyAdded(library, user, include, limit) {
if (library.mediaType !== 'book') return { series: [], count: 0 } if (!library.isBook) return { series: [], count: 0 }
const seriesIncludes = [] const seriesIncludes = []
if (include.includes('rssfeed')) { if (include.includes('rssfeed')) {
seriesIncludes.push({ seriesIncludes.push({
model: Database.models.feed model: Database.feedModel
}) })
} }
@ -221,7 +221,7 @@ module.exports = {
})) }))
} }
const { rows: series, count } = await Database.models.series.findAndCountAll({ const { rows: series, count } = await Database.seriesModel.findAndCountAll({
where: seriesWhere, where: seriesWhere,
limit, limit,
offset: 0, offset: 0,
@ -230,12 +230,12 @@ module.exports = {
replacements: userPermissionBookWhere.replacements, replacements: userPermissionBookWhere.replacements,
include: [ include: [
{ {
model: Database.models.bookSeries, model: Database.bookSeriesModel,
include: { include: {
model: Database.models.book, model: Database.bookModel,
where: userPermissionBookWhere.bookWhere, where: userPermissionBookWhere.bookWhere,
include: { include: {
model: Database.models.libraryItem model: Database.libraryItemModel
} }
}, },
separate: true separate: true
@ -252,7 +252,7 @@ module.exports = {
const oldSeries = s.getOldSeries().toJSON() const oldSeries = s.getOldSeries().toJSON()
if (s.feeds?.length) { if (s.feeds?.length) {
oldSeries.rssFeed = Database.models.feed.getOldFeed(s.feeds[0]).toJSONMinified() oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
} }
// TODO: Sort books by sequence in query // TODO: Sort books by sequence in query
@ -268,7 +268,7 @@ module.exports = {
const libraryItem = bs.book.libraryItem.toJSON() const libraryItem = bs.book.libraryItem.toJSON()
delete bs.book.libraryItem delete bs.book.libraryItem
libraryItem.media = bs.book libraryItem.media = bs.book
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified()
return oldLibraryItem return oldLibraryItem
}) })
allOldSeries.push(oldSeries) allOldSeries.push(oldSeries)
@ -291,7 +291,7 @@ module.exports = {
async getNewestAuthors(library, user, limit) { async getNewestAuthors(library, user, limit) {
if (library.mediaType !== 'book') return { authors: [], count: 0 } if (library.mediaType !== 'book') return { authors: [], count: 0 }
const { rows: authors, count } = await Database.models.author.findAndCountAll({ const { rows: authors, count } = await Database.authorModel.findAndCountAll({
where: { where: {
libraryId: library.id, libraryId: library.id,
createdAt: { createdAt: {
@ -299,7 +299,7 @@ module.exports = {
} }
}, },
include: { include: {
model: Database.models.bookAuthor, model: Database.bookAuthorModel,
required: true // Must belong to a book required: true // Must belong to a book
}, },
limit, limit,
@ -332,9 +332,9 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit) const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
return oldLibraryItem return oldLibraryItem
}), }),
@ -356,7 +356,7 @@ module.exports = {
return { return {
count, count,
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map(li => {
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
}) })
@ -390,7 +390,7 @@ module.exports = {
/** /**
* Get filter data used in filter menus * Get filter data used in filter menus
* @param {oldLibrary} oldLibrary * @param {import('../../objects/Library')} oldLibrary
* @returns {Promise<object>} * @returns {Promise<object>}
*/ */
async getFilterData(oldLibrary) { async getFilterData(oldLibrary) {
@ -417,9 +417,9 @@ module.exports = {
} }
if (oldLibrary.isPodcast) { if (oldLibrary.isPodcast) {
const podcasts = await Database.models.podcast.findAll({ const podcasts = await Database.podcastModel.findAll({
include: { include: {
model: Database.models.libraryItem, model: Database.libraryItemModel,
attributes: [], attributes: [],
where: { where: {
libraryId: oldLibrary.id libraryId: oldLibrary.id
@ -436,9 +436,9 @@ module.exports = {
} }
} }
} else { } else {
const books = await Database.models.book.findAll({ const books = await Database.bookModel.findAll({
include: { include: {
model: Database.models.libraryItem, model: Database.libraryItemModel,
attributes: ['isMissing', 'isInvalid'], attributes: ['isMissing', 'isInvalid'],
where: { where: {
libraryId: oldLibrary.id libraryId: oldLibrary.id
@ -461,7 +461,7 @@ module.exports = {
if (book.language) data.languages.add(book.language) if (book.language) data.languages.add(book.language)
} }
const series = await Database.models.series.findAll({ const series = await Database.seriesModel.findAll({
where: { where: {
libraryId: oldLibrary.id libraryId: oldLibrary.id
}, },
@ -469,7 +469,7 @@ module.exports = {
}) })
series.forEach((s) => data.series.push({ id: s.id, name: s.name })) series.forEach((s) => data.series.push({ id: s.id, name: s.name }))
const authors = await Database.models.author.findAll({ const authors = await Database.authorModel.findAll({
where: { where: {
libraryId: oldLibrary.id libraryId: oldLibrary.id
}, },

View File

@ -1,15 +1,17 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
module.exports = { module.exports = {
/** /**
* Get all library items that have tags * Get all library items that have tags
* @param {string[]} tags * @param {string[]} tags
* @returns {Promise<LibraryItem[]>} * @returns {Promise<import('../../models/LibraryItem')[]>}
*/ */
async getAllLibraryItemsWithTags(tags) { async getAllLibraryItemsWithTags(tags) {
const libraryItems = [] const libraryItems = []
const booksWithTag = await Database.models.book.findAll({ const booksWithTag = await Database.bookModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), { where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), {
[Sequelize.Op.gte]: 1 [Sequelize.Op.gte]: 1
}), }),
@ -18,16 +20,16 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem model: Database.libraryItemModel
}, },
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -39,7 +41,7 @@ module.exports = {
libraryItem.media = book libraryItem.media = book
libraryItems.push(libraryItem) libraryItems.push(libraryItem)
} }
const podcastsWithTag = await Database.models.podcast.findAll({ const podcastsWithTag = await Database.podcastModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), { where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), {
[Sequelize.Op.gte]: 1 [Sequelize.Op.gte]: 1
}), }),
@ -48,10 +50,10 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem model: Database.libraryItemModel
}, },
{ {
model: Database.models.podcastEpisode model: Database.podcastEpisodeModel
} }
] ]
}) })
@ -70,7 +72,7 @@ module.exports = {
*/ */
async getAllLibraryItemsWithGenres(genres) { async getAllLibraryItemsWithGenres(genres) {
const libraryItems = [] const libraryItems = []
const booksWithGenre = await Database.models.book.findAll({ const booksWithGenre = await Database.bookModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), { where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), {
[Sequelize.Op.gte]: 1 [Sequelize.Op.gte]: 1
}), }),
@ -79,16 +81,16 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem model: Database.libraryItemModel
}, },
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -100,7 +102,7 @@ module.exports = {
libraryItem.media = book libraryItem.media = book
libraryItems.push(libraryItem) libraryItems.push(libraryItem)
} }
const podcastsWithGenre = await Database.models.podcast.findAll({ const podcastsWithGenre = await Database.podcastModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), { where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), {
[Sequelize.Op.gte]: 1 [Sequelize.Op.gte]: 1
}), }),
@ -109,10 +111,10 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem model: Database.libraryItemModel
}, },
{ {
model: Database.models.podcastEpisode model: Database.podcastEpisodeModel
} }
] ]
}) })
@ -127,11 +129,11 @@ module.exports = {
/** /**
* Get all library items that have narrators * Get all library items that have narrators
* @param {string[]} narrators * @param {string[]} narrators
* @returns {Promise<LibraryItem[]>} * @returns {Promise<import('../../models/LibraryItem')[]>}
*/ */
async getAllLibraryItemsWithNarrators(narrators) { async getAllLibraryItemsWithNarrators(narrators) {
const libraryItems = [] const libraryItems = []
const booksWithGenre = await Database.models.book.findAll({ const booksWithGenre = await Database.bookModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(narrators) WHERE json_valid(narrators) AND json_each.value IN (:narrators))`), { where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(narrators) WHERE json_valid(narrators) AND json_each.value IN (:narrators))`), {
[Sequelize.Op.gte]: 1 [Sequelize.Op.gte]: 1
}), }),
@ -140,16 +142,16 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem model: Database.libraryItemModel
}, },
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -162,5 +164,57 @@ module.exports = {
libraryItems.push(libraryItem) libraryItems.push(libraryItem)
} }
return libraryItems return libraryItems
},
/**
* Search library items
* @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}}
*/
search(oldUser, oldLibrary, query, limit) {
if (oldLibrary.isBook) {
return libraryItemsBookFilters.search(oldUser, oldLibrary, query, limit, 0)
} else {
return libraryItemsPodcastFilters.search(oldUser, oldLibrary, query, limit, 0)
}
},
/**
* Get largest items in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, size:number }[]>}
*/
async getLargestItems(libraryId, limit) {
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'mediaId', 'mediaType', 'size'],
where: {
libraryId
},
include: [
{
model: Database.bookModel,
attributes: ['id', 'title']
},
{
model: Database.podcastModel,
attributes: ['id', 'title']
}
],
order: [
['size', 'DESC']
],
limit
})
return libraryItems.map(libraryItem => {
return {
id: libraryItem.id,
title: libraryItem.media.title,
size: libraryItem.size
}
})
} }
} }

View File

@ -1,12 +1,13 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const Logger = require('../../Logger') const Logger = require('../../Logger')
const authorFilters = require('./authorFilters')
module.exports = { module.exports = {
/** /**
* User permissions to restrict books for explicit content & tags * User permissions to restrict books for explicit content & tags
* @param {oldUser} user * @param {import('../../objects/user/User')} user
* @returns {object} { bookWhere:Sequelize.WhereOptions, replacements:string[] } * @returns {{ bookWhere:Sequelize.WhereOptions, replacements:object }}
*/ */
getUserPermissionBookWhereQuery(user) { getUserPermissionBookWhereQuery(user) {
const bookWhere = [] const bookWhere = []
@ -278,7 +279,7 @@ module.exports = {
* @returns {object} { booksToExclude, bookSeriesToInclude } * @returns {object} { booksToExclude, bookSeriesToInclude }
*/ */
async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) { async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) {
const allSeries = await Database.models.series.findAll({ const allSeries = await Database.seriesModel.findAll({
attributes: [ attributes: [
'id', 'id',
'name', 'name',
@ -289,7 +290,7 @@ module.exports = {
where: seriesWhere, where: seriesWhere,
include: [ include: [
{ {
model: Database.models.book, model: Database.bookModel,
attributes: ['id', 'title'], attributes: ['id', 'title'],
through: { through: {
attributes: ['id', 'seriesId', 'bookId', 'sequence'] attributes: ['id', 'seriesId', 'bookId', 'sequence']
@ -373,10 +374,10 @@ module.exports = {
} }
let seriesInclude = { let seriesInclude = {
model: Database.models.bookSeries, model: Database.bookSeriesModel,
attributes: ['id', 'seriesId', 'sequence', 'createdAt'], attributes: ['id', 'seriesId', 'sequence', 'createdAt'],
include: { include: {
model: Database.models.series, model: Database.seriesModel,
attributes: ['id', 'name', 'nameIgnorePrefix'] attributes: ['id', 'name', 'nameIgnorePrefix']
}, },
order: [ order: [
@ -386,10 +387,10 @@ module.exports = {
} }
let authorInclude = { let authorInclude = {
model: Database.models.bookAuthor, model: Database.bookAuthorModel,
attributes: ['authorId', 'createdAt'], attributes: ['authorId', 'createdAt'],
include: { include: {
model: Database.models.author, model: Database.authorModel,
attributes: ['id', 'name'] attributes: ['id', 'name']
}, },
order: [ order: [
@ -404,13 +405,13 @@ module.exports = {
const bookIncludes = [] const bookIncludes = []
if (includeRSSFeed) { if (includeRSSFeed) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.models.feed, model: Database.feedModel,
required: filterGroup === 'feed-open' required: filterGroup === 'feed-open'
}) })
} }
if (filterGroup === 'feed-open' && !includeRSSFeed) { if (filterGroup === 'feed-open' && !includeRSSFeed) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.models.feed, model: Database.feedModel,
required: true required: true
}) })
} else if (filterGroup === 'ebooks' && filterValue === 'supplementary') { } else if (filterGroup === 'ebooks' && filterValue === 'supplementary') {
@ -420,7 +421,7 @@ module.exports = {
} }
} else if (filterGroup === 'missing' && filterValue === 'authors') { } else if (filterGroup === 'missing' && filterValue === 'authors') {
authorInclude = { authorInclude = {
model: Database.models.author, model: Database.authorModel,
attributes: ['id'], attributes: ['id'],
through: { through: {
attributes: [] attributes: []
@ -428,7 +429,7 @@ module.exports = {
} }
} else if ((filterGroup === 'series' && filterValue === 'no-series') || (filterGroup === 'missing' && filterValue === 'series')) { } else if ((filterGroup === 'series' && filterValue === 'no-series') || (filterGroup === 'missing' && filterValue === 'series')) {
seriesInclude = { seriesInclude = {
model: Database.models.series, model: Database.seriesModel,
attributes: ['id'], attributes: ['id'],
through: { through: {
attributes: [] attributes: []
@ -436,7 +437,7 @@ module.exports = {
} }
} else if (filterGroup === 'authors') { } else if (filterGroup === 'authors') {
bookIncludes.push({ bookIncludes.push({
model: Database.models.author, model: Database.authorModel,
attributes: ['id', 'name'], attributes: ['id', 'name'],
where: { where: {
id: filterValue id: filterValue
@ -447,7 +448,7 @@ module.exports = {
}) })
} else if (filterGroup === 'series') { } else if (filterGroup === 'series') {
bookIncludes.push({ bookIncludes.push({
model: Database.models.series, model: Database.seriesModel,
attributes: ['id', 'name'], attributes: ['id', 'name'],
where: { where: {
id: filterValue id: filterValue
@ -471,7 +472,7 @@ module.exports = {
] ]
} else if (filterGroup === 'progress' && user) { } else if (filterGroup === 'progress' && user) {
bookIncludes.push({ bookIncludes.push({
model: Database.models.mediaProgress, model: Database.mediaProgressModel,
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'], attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
where: { where: {
userId: user.id userId: user.id
@ -512,7 +513,7 @@ module.exports = {
where: seriesBookWhere, where: seriesBookWhere,
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
required: true, required: true,
where: libraryItemWhere, where: libraryItemWhere,
include: libraryItemIncludes include: libraryItemIncludes
@ -537,11 +538,11 @@ module.exports = {
if (global.ServerSettings.sortingIgnorePrefix) { if (global.ServerSettings.sortingIgnorePrefix) {
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title']) bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title'])
} else { } else {
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), title)`), 'display_title']) bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title'])
} }
} }
const { rows: books, count } = await Database.models.book.findAndCountAll({ const { rows: books, count } = await Database.bookModel.findAndCountAll({
where: bookWhere, where: bookWhere,
distinct: true, distinct: true,
attributes: bookAttributes, attributes: bookAttributes,
@ -552,7 +553,7 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
required: true, required: true,
where: libraryItemWhere, where: libraryItemWhere,
include: libraryItemIncludes include: libraryItemIncludes
@ -632,7 +633,7 @@ module.exports = {
const libraryItemIncludes = [] const libraryItemIncludes = []
if (include.includes('rssfeed')) { if (include.includes('rssfeed')) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.models.feed model: Database.feedModel
}) })
} }
@ -642,7 +643,7 @@ module.exports = {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
bookWhere.push(...userPermissionBookWhere.bookWhere) bookWhere.push(...userPermissionBookWhere.bookWhere)
const { rows: series, count } = await Database.models.series.findAndCountAll({ const { rows: series, count } = await Database.seriesModel.findAndCountAll({
where: [ where: [
{ {
libraryId libraryId
@ -669,7 +670,7 @@ module.exports = {
...userPermissionBookWhere.replacements ...userPermissionBookWhere.replacements
}, },
include: { include: {
model: Database.models.bookSeries, model: Database.bookSeriesModel,
attributes: ['bookId', 'sequence'], attributes: ['bookId', 'sequence'],
separate: true, separate: true,
subQuery: false, subQuery: false,
@ -682,21 +683,21 @@ module.exports = {
} }
}, },
include: { include: {
model: Database.models.book, model: Database.bookModel,
where: bookWhere, where: bookWhere,
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
include: libraryItemIncludes include: libraryItemIncludes
}, },
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.mediaProgress, model: Database.mediaProgressModel,
where: { where: {
userId: user.id userId: user.id
}, },
@ -751,7 +752,7 @@ module.exports = {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
// Step 1: Get the first book of every series that hasnt been started yet // Step 1: Get the first book of every series that hasnt been started yet
const seriesNotStarted = await Database.models.series.findAll({ const seriesNotStarted = await Database.seriesModel.findAll({
where: [ where: [
{ {
libraryId libraryId
@ -764,12 +765,12 @@ module.exports = {
}, },
attributes: ['id'], attributes: ['id'],
include: { include: {
model: Database.models.bookSeries, model: Database.bookSeriesModel,
attributes: ['bookId', 'sequence'], attributes: ['bookId', 'sequence'],
separate: true, separate: true,
required: true, required: true,
include: { include: {
model: Database.models.book, model: Database.bookModel,
where: userPermissionBookWhere.bookWhere where: userPermissionBookWhere.bookWhere
}, },
order: [ order: [
@ -788,12 +789,12 @@ module.exports = {
const libraryItemIncludes = [] const libraryItemIncludes = []
if (include.includes('rssfeed')) { if (include.includes('rssfeed')) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.models.feed model: Database.feedModel
}) })
} }
// Step 2: Get books not started and not in a series OR is the first book of a series not started (ordered randomly) // 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({ const { rows: books, count } = await Database.bookModel.findAndCountAll({
where: [ where: [
{ {
'$mediaProgresses.isFinished$': { '$mediaProgresses.isFinished$': {
@ -816,32 +817,32 @@ module.exports = {
replacements: userPermissionBookWhere.replacements, replacements: userPermissionBookWhere.replacements,
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
where: { where: {
libraryId libraryId
}, },
include: libraryItemIncludes include: libraryItemIncludes
}, },
{ {
model: Database.models.mediaProgress, model: Database.mediaProgressModel,
where: { where: {
userId: user.id userId: user.id
}, },
required: false required: false
}, },
{ {
model: Database.models.bookAuthor, model: Database.bookAuthorModel,
attributes: ['authorId'], attributes: ['authorId'],
include: { include: {
model: Database.models.author model: Database.authorModel
}, },
separate: true separate: true
}, },
{ {
model: Database.models.bookSeries, model: Database.bookSeriesModel,
attributes: ['seriesId', 'sequence'], attributes: ['seriesId', 'sequence'],
include: { include: {
model: Database.models.series model: Database.seriesModel
}, },
separate: true separate: true
} }
@ -883,10 +884,10 @@ module.exports = {
return [] return []
} }
const books = await Database.models.book.findAll({ const books = await Database.bookModel.findAll({
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
where: { where: {
id: { id: {
[Sequelize.Op.in]: collection.books [Sequelize.Op.in]: collection.books
@ -894,13 +895,13 @@ module.exports = {
} }
}, },
{ {
model: Database.models.author, model: Database.authorModel,
through: { through: {
attributes: [] attributes: []
} }
}, },
{ {
model: Database.models.series, model: Database.seriesModel,
through: { through: {
attributes: ['sequence'] attributes: ['sequence']
} }
@ -918,12 +919,260 @@ module.exports = {
/** /**
* Get library items for series * Get library items for series
* @param {oldSeries} oldSeries * @param {import('../../objects/entities/Series')} oldSeries
* @param {[oldUser]} oldUser * @param {import('../../objects/user/User')} [oldUser]
* @returns {Promise<oldLibraryItem[]>} * @returns {Promise<import('../../objects/LibraryItem')[]>}
*/ */
async getLibraryItemsForSeries(oldSeries, oldUser) { async getLibraryItemsForSeries(oldSeries, oldUser) {
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null) const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null)
return libraryItems.map(li => Database.models.libraryItem.getOldLibraryItem(li)) return libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li))
},
/**
* Search books, authors, series
* @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}}
*/
async search(oldUser, oldLibrary, query, limit, offset) {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(oldUser)
// Search title, subtitle, asin, isbn
const books = await Database.bookModel.findAll({
where: [
{
[Sequelize.Op.or]: [
{
title: {
[Sequelize.Op.substring]: query
}
},
{
subtitle: {
[Sequelize.Op.substring]: query
}
},
{
asin: {
[Sequelize.Op.substring]: query
}
},
{
isbn: {
[Sequelize.Op.substring]: query
}
}
]
},
...userPermissionBookWhere.bookWhere
],
replacements: userPermissionBookWhere.replacements,
include: [
{
model: Database.libraryItemModel,
where: {
libraryId: oldLibrary.id
}
},
{
model: Database.bookSeriesModel,
include: {
model: Database.seriesModel
},
separate: true
},
{
model: Database.bookAuthorModel,
include: {
model: Database.authorModel
},
separate: true
}
],
subQuery: false,
distinct: true,
limit,
offset
})
const itemMatches = []
for (const book of books) {
const libraryItem = book.libraryItem
delete book.libraryItem
libraryItem.media = book
let matchText = null
let matchKey = null
for (const key of ['title', 'subtitle', 'asin', 'isbn']) {
if (book[key]?.toLowerCase().includes(query)) {
matchText = book[key]
matchKey = key
break
}
}
if (matchKey) {
itemMatches.push({
matchText,
matchKey,
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
})
}
}
// Search narrators
const narratorMatches = []
const [narratorResults] = await Database.sequelize.query(`SELECT value, count(*) AS numBooks FROM books b, libraryItems li, json_each(b.narrators) WHERE json_valid(b.narrators) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of narratorResults) {
narratorMatches.push({
name: row.value,
numBooks: row.numBooks
})
}
// Search tags
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.tags) WHERE json_valid(b.tags) AND json_each.value LIKE :query AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of tagResults) {
tagMatches.push({
name: row.value,
numItems: row.numItems
})
}
// Search series
const allSeries = await Database.seriesModel.findAll({
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId: oldLibrary.id
},
replacements: userPermissionBookWhere.replacements,
include: {
separate: true,
model: Database.bookSeriesModel,
include: {
model: Database.bookModel,
where: userPermissionBookWhere.bookWhere,
include: {
model: Database.libraryItemModel
}
}
},
subQuery: false,
distinct: true,
limit,
offset
})
const seriesMatches = []
for (const series of allSeries) {
const books = series.bookSeries.map((bs) => {
const libraryItem = bs.book.libraryItem
libraryItem.media = bs.book
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
})
seriesMatches.push({
series: series.getOldSeries().toJSON(),
books
})
}
// Search authors
const authorMatches = await authorFilters.search(oldLibrary.id, query, limit, offset)
return {
book: itemMatches,
narrators: narratorMatches,
tags: tagMatches,
series: seriesMatches,
authors: authorMatches
}
},
/**
* Genres with num books
* @param {string} libraryId
* @returns {{genre:string, count:number}[]}
*/
async getGenresWithCount(libraryId) {
const genres = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
replacements: {
libraryId
},
raw: true
})
for (const row of genreResults) {
genres.push({
genre: row.value,
count: row.numItems
})
}
return genres
},
/**
* Get stats for book library
* @param {string} libraryId
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
*/
async getBookLibraryStats(libraryId) {
const [statResults] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, SUM(json_array_length(b.audioFiles)) AS numAudioFiles, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.libraryId = :libraryId;`, {
replacements: {
libraryId
}
})
return statResults[0]
},
/**
* Get longest books in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
*/
async getLongestBooks(libraryId, limit) {
const books = await Database.bookModel.findAll({
attributes: ['id', 'title', 'duration'],
include: {
model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId
}
},
order: [
['duration', 'DESC']
],
limit
})
return books.map(book => {
return {
id: book.libraryItem.id,
title: book.title,
duration: book.duration
}
})
} }
} }

View File

@ -6,8 +6,8 @@ const Logger = require('../../Logger')
module.exports = { module.exports = {
/** /**
* User permissions to restrict podcasts for explicit content & tags * User permissions to restrict podcasts for explicit content & tags
* @param {oldUser} user * @param {import('../../objects/user/User')} user
* @returns {object} { podcastWhere:Sequelize.WhereOptions, replacements:string[] } * @returns {{ podcastWhere:Sequelize.WhereOptions, replacements:object }}
*/ */
getUserPermissionPodcastWhereQuery(user) { getUserPermissionPodcastWhereQuery(user) {
const podcastWhere = [] const podcastWhere = []
@ -112,7 +112,7 @@ module.exports = {
const libraryItemIncludes = [] const libraryItemIncludes = []
if (includeRSSFeed) { if (includeRSSFeed) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.models.feed, model: Database.feedModel,
required: filterGroup === 'feed-open' required: filterGroup === 'feed-open'
}) })
} }
@ -146,7 +146,7 @@ module.exports = {
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements } replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere) podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
const { rows: podcasts, count } = await Database.models.podcast.findAndCountAll({ const { rows: podcasts, count } = await Database.podcastModel.findAndCountAll({
where: podcastWhere, where: podcastWhere,
replacements, replacements,
distinct: true, distinct: true,
@ -158,7 +158,7 @@ module.exports = {
}, },
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
required: true, required: true,
where: libraryItemWhere, where: libraryItemWhere,
include: libraryItemIncludes include: libraryItemIncludes
@ -219,7 +219,7 @@ module.exports = {
} }
if (filterGroup === 'progress') { if (filterGroup === 'progress') {
podcastEpisodeIncludes.push({ podcastEpisodeIncludes.push({
model: Database.models.mediaProgress, model: Database.mediaProgressModel,
where: { where: {
userId: user.id userId: user.id
}, },
@ -255,16 +255,16 @@ module.exports = {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const { rows: podcastEpisodes, count } = await Database.models.podcastEpisode.findAndCountAll({ const { rows: podcastEpisodes, count } = await Database.podcastEpisodeModel.findAndCountAll({
where: podcastEpisodeWhere, where: podcastEpisodeWhere,
replacements: userPermissionPodcastWhere.replacements, replacements: userPermissionPodcastWhere.replacements,
include: [ include: [
{ {
model: Database.models.podcast, model: Database.podcastModel,
where: userPermissionPodcastWhere.podcastWhere, where: userPermissionPodcastWhere.podcastWhere,
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.libraryItemModel,
where: libraryItemWhere where: libraryItemWhere
} }
] ]
@ -283,7 +283,7 @@ module.exports = {
const podcast = ep.podcast.toJSON() const podcast = ep.podcast.toJSON()
delete podcast.libraryItem delete podcast.libraryItem
libraryItem.media = podcast libraryItem.media = podcast
libraryItem.recentEpisode = ep.getOldPodcastEpisode(libraryItem.id) libraryItem.recentEpisode = ep.getOldPodcastEpisode(libraryItem.id).toJSON()
return libraryItem return libraryItem
}) })
@ -291,5 +291,239 @@ module.exports = {
libraryItems, libraryItems,
count count
} }
},
/**
* Search podcasts
* @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary
* @param {string} query
* @param {number} limit
* @param {number} offset
* @returns {{podcast:object[], tags:object[]}}
*/
async search(oldUser, oldLibrary, query, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
// Search title, author, itunesId, itunesArtistId
const podcasts = await Database.podcastModel.findAll({
where: [
{
[Sequelize.Op.or]: [
{
title: {
[Sequelize.Op.substring]: query
}
},
{
author: {
[Sequelize.Op.substring]: query
}
},
{
itunesId: {
[Sequelize.Op.substring]: query
}
},
{
itunesArtistId: {
[Sequelize.Op.substring]: query
}
}
]
},
...userPermissionPodcastWhere.podcastWhere
],
replacements: userPermissionPodcastWhere.replacements,
include: [
{
model: Database.libraryItemModel,
where: {
libraryId: oldLibrary.id
}
}
],
subQuery: false,
distinct: true,
limit,
offset
})
const itemMatches = []
for (const podcast of podcasts) {
const libraryItem = podcast.libraryItem
delete podcast.libraryItem
libraryItem.media = podcast
let matchText = null
let matchKey = null
for (const key of ['title', 'author', 'itunesId', 'itunesArtistId']) {
if (podcast[key]?.toLowerCase().includes(query)) {
matchText = podcast[key]
matchKey = key
break
}
}
if (matchKey) {
itemMatches.push({
matchText,
matchKey,
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
})
}
}
// Search tags
const tagMatches = []
const [tagResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.tags) WHERE json_valid(p.tags) AND json_each.value LIKE :query AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value LIMIT :limit OFFSET :offset;`, {
replacements: {
query: `%${query}%`,
libraryId: oldLibrary.id,
limit,
offset
},
raw: true
})
for (const row of tagResults) {
tagMatches.push({
name: row.value,
numItems: row.numItems
})
}
return {
podcast: itemMatches,
tags: tagMatches
}
},
/**
* Most recent podcast episodes not finished
* @param {import('../../objects/user/User')} oldUser
* @param {import('../../objects/Library')} oldLibrary
* @param {number} limit
* @param {number} offset
* @returns {Promise<object[]>}
*/
async getRecentEpisodes(oldUser, oldLibrary, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(oldUser)
const episodes = await Database.podcastEpisodeModel.findAll({
where: {
'$mediaProgresses.isFinished$': {
[Sequelize.Op.or]: [null, false]
}
},
replacements: userPermissionPodcastWhere.replacements,
include: [
{
model: Database.podcastModel,
where: userPermissionPodcastWhere.podcastWhere,
required: true,
include: {
model: Database.libraryItemModel,
where: {
libraryId: oldLibrary.id
}
}
},
{
model: Database.mediaProgressModel,
where: {
userId: oldUser.id
},
required: false
}
],
order: [
['publishedAt', 'DESC']
],
subQuery: false,
limit,
offset
})
const episodeResults = episodes.map((ep) => {
const libraryItem = ep.podcast.libraryItem
libraryItem.media = ep.podcast
const oldPodcast = Database.podcastModel.getOldPodcast(libraryItem)
const oldPodcastEpisode = ep.getOldPodcastEpisode(libraryItem.id).toJSONExpanded()
oldPodcastEpisode.podcast = oldPodcast
oldPodcastEpisode.libraryId = libraryItem.libraryId
return oldPodcastEpisode
})
return episodeResults
},
/**
* Get stats for podcast library
* @param {string} libraryId
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
*/
async getPodcastLibraryStats(libraryId) {
const [statResults] = await Database.sequelize.query(`SELECT SUM(json_extract(pe.audioFile, '$.duration')) AS totalDuration, SUM(li.size) AS totalSize, COUNT(DISTINCT(li.id)) AS totalItems, COUNT(pe.id) AS numAudioFiles FROM libraryItems li, podcasts p LEFT OUTER JOIN podcastEpisodes pe ON pe.podcastId = p.id WHERE p.id = li.mediaId AND li.libraryId = :libraryId;`, {
replacements: {
libraryId
}
})
return statResults[0]
},
/**
* Genres with num podcasts
* @param {string} libraryId
* @returns {{genre:string, count:number}[]}
*/
async getGenresWithCount(libraryId) {
const genres = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
replacements: {
libraryId
},
raw: true
})
for (const row of genreResults) {
genres.push({
genre: row.value,
count: row.numItems
})
}
return genres
},
/**
* Get longest podcasts in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
*/
async getLongestPodcasts(libraryId, limit) {
const podcasts = await Database.podcastModel.findAll({
attributes: [
'id',
'title',
[Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']
],
include: {
model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId
}
},
order: [
['duration', 'DESC']
],
limit
})
return podcasts.map(podcast => {
return {
id: podcast.libraryItem.id,
title: podcast.title,
duration: podcast.dataValues.duration
}
})
} }
} }

View File

@ -0,0 +1,206 @@
const Sequelize = require('sequelize')
const Logger = require('../../Logger')
const Database = require('../../Database')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
module.exports = {
decode(text) {
return Buffer.from(decodeURIComponent(text), 'base64').toString()
},
/**
* Get series filtered and sorted
*
* @param {import('../../objects/Library')} library
* @param {import('../../objects/user/User')} user
* @param {string} filterBy
* @param {string} sortBy
* @param {boolean} sortDesc
* @param {string[]} include
* @param {number} limit
* @param {number} offset
* @returns {Promise<{ series:object[], count:number }>}
*/
async getFilteredSeries(library, user, filterBy, sortBy, sortDesc, include, limit, offset) {
let filterValue = null
let filterGroup = null
if (filterBy) {
const searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'publishers', 'languages']
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
filterGroup = group || filterBy
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
}
const seriesIncludes = []
if (include.includes('rssfeed')) {
seriesIncludes.push({
model: Database.feedModel
})
}
const userPermissionBookWhere = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user)
const seriesWhere = [
{
libraryId: library.id
}
]
// Handle library setting to hide single book series
// TODO: Merge with existing query
if (library.settings.hideSingleBookSeries) {
seriesWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id)`), {
[Sequelize.Op.gt]: 1
}))
}
// Handle filters
// TODO: Simplify and break-out
let attrQuery = null
if (['genres', 'tags', 'narrators'].includes(filterGroup)) {
attrQuery = `SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (SELECT count(*) FROM json_each(b.${filterGroup}) WHERE json_valid(b.${filterGroup}) AND json_each.value = :filterValue) > 0`
userPermissionBookWhere.replacements.filterValue = filterValue
} else if (filterGroup === 'authors') {
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs, bookAuthors ba WHERE bs.seriesId = series.id AND bs.bookId = b.id AND ba.bookId = b.id AND ba.authorId = :filterValue'
userPermissionBookWhere.replacements.filterValue = filterValue
} else if (filterGroup === 'publishers') {
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND b.publisher = :filterValue'
userPermissionBookWhere.replacements.filterValue = filterValue
} else if (filterGroup === 'languages') {
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND b.language = :filterValue'
userPermissionBookWhere.replacements.filterValue = filterValue
} else if (filterGroup === 'progress') {
if (filterValue === 'not-finished') {
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
} else if (filterValue === 'finished') {
const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
} else if (filterValue === 'not-started') {
const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished = 1 OR mp.currentTime > 0)'
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
} else if (filterValue === 'in-progress') {
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.currentTime > 0 OR mp.ebookProgress > 0) AND mp.isFinished = 0'
}
}
// Handle user permissions to only include series with at least 1 book
// TODO: Simplify to a single query
if (userPermissionBookWhere.bookWhere.length) {
if (!attrQuery) attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id'
if (!user.canAccessExplicitContent) {
attrQuery += ' AND b.explicit = 0'
}
if (!user.permissions.accessAllTags && user.itemTagsSelected.length) {
if (user.permissions.selectedTagsNotAccessible) {
attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) = 0'
} else {
attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) > 0'
}
}
}
if (attrQuery) {
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${attrQuery})`), {
[Sequelize.Op.gt]: 0
}))
}
const order = []
let seriesAttributes = {
include: []
}
// Handle sort order
const dir = sortDesc ? 'DESC' : 'ASC'
if (sortBy === 'numBooks') {
seriesAttributes.include.push([Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks'])
order.push(['numBooks', dir])
} else if (sortBy === 'addedAt') {
order.push(['createdAt', dir])
} else if (sortBy === 'name') {
if (global.ServerSettings.sortingIgnorePrefix) {
order.push([Sequelize.literal('nameIgnorePrefix COLLATE NOCASE'), dir])
} else {
order.push([Sequelize.literal('`series`.`name` COLLATE NOCASE'), dir])
}
} else if (sortBy === 'totalDuration') {
seriesAttributes.include.push([Sequelize.literal('(SELECT SUM(b.duration) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'totalDuration'])
order.push(['totalDuration', dir])
} else if (sortBy === 'lastBookAdded') {
seriesAttributes.include.push([Sequelize.literal('(SELECT MAX(b.createdAt) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'mostRecentBookAdded'])
order.push(['mostRecentBookAdded', dir])
} else if (sortBy === 'lastBookUpdated') {
seriesAttributes.include.push([Sequelize.literal('(SELECT MAX(b.updatedAt) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'mostRecentBookUpdated'])
order.push(['mostRecentBookUpdated', dir])
}
const { rows: series, count } = await Database.seriesModel.findAndCountAll({
where: seriesWhere,
limit,
offset,
distinct: true,
subQuery: false,
benchmark: true,
logging: (sql, timeMs) => {
console.log(`[Query] Series filter/sort. Elapsed ${timeMs}ms`)
},
attributes: seriesAttributes,
replacements: userPermissionBookWhere.replacements,
include: [
{
model: Database.bookSeriesModel,
include: {
model: Database.bookModel,
where: userPermissionBookWhere.bookWhere,
include: [
{
model: Database.libraryItemModel
}
]
},
separate: true
},
...seriesIncludes
],
order
})
// Map series to old series
const allOldSeries = []
for (const s of series) {
const oldSeries = s.getOldSeries().toJSON()
if (s.dataValues.totalDuration) {
oldSeries.totalDuration = s.dataValues.totalDuration
}
if (s.feeds?.length) {
oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
}
// TODO: Sort books by sequence in query
s.bookSeries.sort((a, b) => {
if (!a.sequence) return 1
if (!b.sequence) return -1
return a.sequence.localeCompare(b.sequence, undefined, {
numeric: true,
sensitivity: 'base'
})
})
oldSeries.books = s.bookSeries.map(bs => {
const libraryItem = bs.book.libraryItem.toJSON()
delete bs.book.libraryItem
libraryItem.media = bs.book
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified()
return oldLibraryItem
})
allOldSeries.push(oldSeries)
}
return {
series: allOldSeries,
count
}
}
}