mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-11 01:17:50 +02:00
Update library filter data to load from db and cache, update rss feed routes to load library items from db
This commit is contained in:
parent
8d03b23f46
commit
3651fffbee
@ -22,6 +22,9 @@ class Database {
|
|||||||
this.authors = []
|
this.authors = []
|
||||||
this.series = []
|
this.series = []
|
||||||
|
|
||||||
|
// Cached library filter data
|
||||||
|
this.libraryFilterData = {}
|
||||||
|
|
||||||
this.serverSettings = null
|
this.serverSettings = null
|
||||||
this.notificationSettings = null
|
this.notificationSettings = null
|
||||||
this.emailSettings = null
|
this.emailSettings = null
|
||||||
@ -436,6 +439,34 @@ class Database {
|
|||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
return this.models.device.createFromOld(oldDevice)
|
return this.models.device.createFromOld(oldDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeTagFromFilterData(tag) {
|
||||||
|
for (const libraryId in this.libraryFilterData) {
|
||||||
|
this.libraryFilterData[libraryId].tags = this.libraryFilterData[libraryId].tags.filter(t => t !== tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagToFilterData(tag) {
|
||||||
|
for (const libraryId in this.libraryFilterData) {
|
||||||
|
if (!this.libraryFilterData[libraryId].tags.includes(tag)) {
|
||||||
|
this.libraryFilterData[libraryId].tags.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeGenreFromFilterData(genre) {
|
||||||
|
for (const libraryId in this.libraryFilterData) {
|
||||||
|
this.libraryFilterData[libraryId].genres = this.libraryFilterData[libraryId].genres.filter(g => g !== genre)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addGenreToFilterData(genre) {
|
||||||
|
for (const libraryId in this.libraryFilterData) {
|
||||||
|
if (!this.libraryFilterData[libraryId].genres.includes(genre)) {
|
||||||
|
this.libraryFilterData[libraryId].genres.push(genre)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new Database()
|
module.exports = new Database()
|
@ -5,12 +5,14 @@ const Logger = require('../Logger')
|
|||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Library = require('../objects/Library')
|
const Library = require('../objects/Library')
|
||||||
const libraryHelpers = require('../utils/libraryHelpers')
|
const libraryHelpers = require('../utils/libraryHelpers')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
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
|
||||||
})
|
})
|
||||||
|
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
|
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||||
|
|
||||||
class LibraryController {
|
class LibraryController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -80,9 +82,11 @@ class LibraryController {
|
|||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeArray = (req.query.include || '').split(',')
|
const includeArray = (req.query.include || '').split(',')
|
||||||
if (includeArray.includes('filterdata')) {
|
if (includeArray.includes('filterdata')) {
|
||||||
|
const filterdata = await libraryFilters.getFilterData(req.library)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
filterdata,
|
||||||
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
issues: filterdata.numIssues,
|
||||||
numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
||||||
library: req.library
|
library: req.library
|
||||||
})
|
})
|
||||||
@ -90,9 +94,15 @@ class LibraryController {
|
|||||||
return res.json(req.library)
|
return res.json(req.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id/episode-downloads
|
||||||
|
* Get podcast episodes in download queue
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getEpisodeDownloadQueue(req, res) {
|
async getEpisodeDownloadQueue(req, res) {
|
||||||
const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id)
|
const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id)
|
||||||
return res.json(libraryDownloadQueueDetails)
|
res.json(libraryDownloadQueueDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
@ -214,8 +224,12 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/items
|
/**
|
||||||
// TODO: Optimize this method, items are iterated through several times but can be combined
|
* GET: /api/libraries/:id/items
|
||||||
|
* TODO: Remove after implementing getLibraryItemsNew
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getLibraryItems(req, res) {
|
async getLibraryItems(req, res) {
|
||||||
let libraryItems = req.libraryItems
|
let libraryItems = req.libraryItems
|
||||||
|
|
||||||
@ -431,7 +445,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {*} req
|
||||||
@ -498,7 +512,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* api/libraries/:id/series/:seriesId
|
* GET: /api/libraries/:id/series/:seriesId
|
||||||
*
|
*
|
||||||
* Optional includes (e.g. `?include=rssfeed,progress`)
|
* Optional includes (e.g. `?include=rssfeed,progress`)
|
||||||
* rssfeed: adds `rssFeed` to series object if a feed is open
|
* rssfeed: adds `rssFeed` to series object if a feed is open
|
||||||
@ -513,7 +527,7 @@ class LibraryController {
|
|||||||
const series = Database.series.find(se => se.id === req.params.seriesId)
|
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
const libraryItemsInSeries = req.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
||||||
|
|
||||||
const seriesJson = series.toJSON()
|
const seriesJson = series.toJSON()
|
||||||
if (include.includes('progress')) {
|
if (include.includes('progress')) {
|
||||||
@ -533,7 +547,12 @@ class LibraryController {
|
|||||||
res.json(seriesJson)
|
res.json(seriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/collections
|
/**
|
||||||
|
* GET: /api/libraries/:id/collections
|
||||||
|
* Get all collections for library
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getCollectionsForLibrary(req, res) {
|
async getCollectionsForLibrary(req, res) {
|
||||||
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)
|
||||||
|
|
||||||
@ -563,10 +582,15 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/playlists
|
/**
|
||||||
|
* GET: /api/libraries/:id/playlists
|
||||||
|
* Get playlists for user in library
|
||||||
|
* @param {*} req
|
||||||
|
* @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.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
|
||||||
playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
|
playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded()))
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
@ -584,8 +608,14 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id/filterdata
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getLibraryFilterData(req, res) {
|
async getLibraryFilterData(req, res) {
|
||||||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
const filterData = await libraryFilters.getFilterData(req.library)
|
||||||
|
res.json(filterData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,6 +229,10 @@ class MiscController {
|
|||||||
let tagMerged = false
|
let tagMerged = false
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
|
// Update filter data
|
||||||
|
Database.removeTagFromFilterData(tag)
|
||||||
|
Database.addTagToFilterData(newTag)
|
||||||
|
|
||||||
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag, newTag])
|
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag, newTag])
|
||||||
for (const libraryItem of libraryItemsWithTag) {
|
for (const libraryItem of libraryItemsWithTag) {
|
||||||
let existingTags = libraryItem.media.tags
|
let existingTags = libraryItem.media.tags
|
||||||
@ -275,6 +279,9 @@ class MiscController {
|
|||||||
// Get all items with tag
|
// Get all items with tag
|
||||||
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag])
|
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag])
|
||||||
|
|
||||||
|
// Update filterdata
|
||||||
|
Database.removeTagFromFilterData(tag)
|
||||||
|
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
// Remove tag from items
|
// Remove tag from items
|
||||||
for (const libraryItem of libraryItemsWithTag) {
|
for (const libraryItem of libraryItemsWithTag) {
|
||||||
@ -356,6 +363,10 @@ class MiscController {
|
|||||||
let genreMerged = false
|
let genreMerged = false
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
|
// Update filter data
|
||||||
|
Database.removeGenreFromFilterData(genre)
|
||||||
|
Database.addGenreToFilterData(newGenre)
|
||||||
|
|
||||||
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre, newGenre])
|
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre, newGenre])
|
||||||
for (const libraryItem of libraryItemsWithGenre) {
|
for (const libraryItem of libraryItemsWithGenre) {
|
||||||
let existingGenres = libraryItem.media.genres
|
let existingGenres = libraryItem.media.genres
|
||||||
@ -399,6 +410,9 @@ class MiscController {
|
|||||||
|
|
||||||
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||||
|
|
||||||
|
// Update filter data
|
||||||
|
Database.removeGenreFromFilterData(genre)
|
||||||
|
|
||||||
// Get all items with genre
|
// Get all items with genre
|
||||||
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre])
|
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre])
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
|
||||||
class RSSFeedController {
|
class RSSFeedController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -8,7 +9,7 @@ class RSSFeedController {
|
|||||||
async openRSSFeedForItem(req, res) {
|
async openRSSFeedForItem(req, res) {
|
||||||
const options = req.body || {}
|
const options = req.body || {}
|
||||||
|
|
||||||
const item = Database.libraryItems.find(li => li.id === req.params.itemId)
|
const item = await Database.models.libraryItem.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
|
||||||
@ -45,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.getOldById(req.params.collectionId)
|
const collection = await Database.models.collection.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
|
||||||
@ -60,7 +61,7 @@ class RSSFeedController {
|
|||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
const collectionExpanded = await collection.getOldJsonExpanded()
|
||||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||||
|
|
||||||
// Check collection has audio tracks
|
// Check collection has audio tracks
|
||||||
@ -95,8 +96,9 @@ class RSSFeedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seriesJson = series.toJSON()
|
const seriesJson = series.toJSON()
|
||||||
|
|
||||||
// Get books in series that have audio tracks
|
// Get books in series that have audio tracks
|
||||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
|
||||||
|
|
||||||
// Check series has audio tracks
|
// Check series has audio tracks
|
||||||
if (!seriesJson.books.length) {
|
if (!seriesJson.books.length) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
|
||||||
class SeriesController {
|
class SeriesController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -25,7 +26,7 @@ class SeriesController {
|
|||||||
const libraryItemsInSeries = req.libraryItemsInSeries
|
const libraryItemsInSeries = req.libraryItemsInSeries
|
||||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||||
const mediaProgress = req.user.getMediaProgress(li.id)
|
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||||
return mediaProgress && mediaProgress.isFinished
|
return mediaProgress?.isFinished
|
||||||
})
|
})
|
||||||
seriesJson.progress = {
|
seriesJson.progress = {
|
||||||
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
||||||
@ -62,18 +63,17 @@ class SeriesController {
|
|||||||
res.json(req.series.toJSON())
|
res.json(req.series.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
const series = Database.series.find(se => se.id === req.params.id)
|
const series = Database.series.find(se => se.id === req.params.id)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter out any library items not accessible to user
|
* Filter out any library items not accessible to user
|
||||||
*/
|
*/
|
||||||
const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
||||||
const libraryItemsAccessible = libraryItems.filter(li => req.user.checkCanAccessLibraryItem(li))
|
if (!libraryItems.length) {
|
||||||
if (libraryItems.length && !libraryItemsAccessible.length) {
|
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user)
|
||||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user)
|
return res.sendStatus(404)
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
@ -85,7 +85,7 @@ class SeriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.series = series
|
req.series = series
|
||||||
req.libraryItemsInSeries = libraryItemsAccessible
|
req.libraryItemsInSeries = libraryItems
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,15 +99,15 @@ class ToolsController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
|
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
const item = await Database.models.libraryItem.getOldById(req.params.id)
|
||||||
if (!item || !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
|
||||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||||
|
@ -265,7 +265,7 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncSession(user, session, syncData) {
|
async syncSession(user, session, syncData) {
|
||||||
const libraryItem = Database.libraryItems.find(li => li.id === session.libraryItemId)
|
const libraryItem = await Database.models.libraryItem.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
|
||||||
|
@ -150,7 +150,7 @@ class PodcastManager {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItem = Database.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
const libraryItem = await Database.models.libraryItem.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
|
||||||
|
@ -6,6 +6,7 @@ const Database = require('../Database')
|
|||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Feed = require('../objects/Feed')
|
const Feed = require('../objects/Feed')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
|
||||||
class RssFeedManager {
|
class RssFeedManager {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -18,15 +19,15 @@ class RssFeedManager {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (feedObj.entityType === 'libraryItem') {
|
} else if (feedObj.entityType === 'libraryItem') {
|
||||||
if (!Database.libraryItems.some(li => li.id === feedObj.entityId)) {
|
const libraryItemExists = await Database.models.libraryItem.checkExistsById(feedObj.entityId)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
} else if (feedObj.entityType === 'series') {
|
} else if (feedObj.entityType === 'series') {
|
||||||
const series = Database.series.find(s => s.id === feedObj.entityId)
|
const series = Database.series.find(s => s.id === feedObj.entityId)
|
||||||
const hasSeriesBook = series ? Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) : false
|
if (!series) {
|
||||||
if (!hasSeriesBook) {
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
|
||||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -102,9 +103,9 @@ 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.getOldById(feed.entityId)
|
const collection = await Database.models.collection.findByPk(feed.entityId)
|
||||||
if (collection) {
|
if (collection) {
|
||||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
const collectionExpanded = await collection.getOldJsonExpanded()
|
||||||
|
|
||||||
// Find most recently updated item in collection
|
// Find most recently updated item in collection
|
||||||
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
||||||
@ -125,8 +126,9 @@ class RssFeedManager {
|
|||||||
const series = Database.series.find(s => s.id === feed.entityId)
|
const series = Database.series.find(s => s.id === feed.entityId)
|
||||||
if (series) {
|
if (series) {
|
||||||
const seriesJson = series.toJSON()
|
const seriesJson = series.toJSON()
|
||||||
|
|
||||||
// Get books in series that have audio tracks
|
// Get books in series that have audio tracks
|
||||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
|
||||||
|
|
||||||
// Find most recently updated item in series
|
// Find most recently updated item in series
|
||||||
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
const { DataTypes, Model, Op } = require('sequelize')
|
const { DataTypes, Model, Op, literal } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const oldPlaylist = require('../objects/Playlist')
|
const oldPlaylist = require('../objects/Playlist')
|
||||||
const { areEquivalent } = require('../utils/index')
|
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = (sequelize) => {
|
||||||
class Playlist extends Model {
|
class Playlist extends Model {
|
||||||
@ -101,49 +100,6 @@ module.exports = (sequelize) => {
|
|||||||
return this.create(playlist)
|
return this.create(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fullUpdateFromOld(oldPlaylist, playlistMediaItems) {
|
|
||||||
const existingPlaylist = await this.findByPk(oldPlaylist.id, {
|
|
||||||
include: sequelize.models.playlistMediaItem
|
|
||||||
})
|
|
||||||
if (!existingPlaylist) return false
|
|
||||||
|
|
||||||
let hasUpdates = false
|
|
||||||
const playlist = this.getFromOld(oldPlaylist)
|
|
||||||
|
|
||||||
for (const pmi of playlistMediaItems) {
|
|
||||||
const existingPmi = existingPlaylist.playlistMediaItems.find(i => i.mediaItemId === pmi.mediaItemId)
|
|
||||||
if (!existingPmi) {
|
|
||||||
await sequelize.models.playlistMediaItem.create(pmi)
|
|
||||||
hasUpdates = true
|
|
||||||
} else if (existingPmi.order != pmi.order) {
|
|
||||||
await existingPmi.update({ order: pmi.order })
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const pmi of existingPlaylist.playlistMediaItems) {
|
|
||||||
// Pmi was removed
|
|
||||||
if (!playlistMediaItems.some(i => i.mediaItemId === pmi.mediaItemId)) {
|
|
||||||
await pmi.destroy()
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasPlaylistUpdates = false
|
|
||||||
for (const key in playlist) {
|
|
||||||
let existingValue = existingPlaylist[key]
|
|
||||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
|
||||||
|
|
||||||
if (!areEquivalent(playlist[key], existingValue)) {
|
|
||||||
hasPlaylistUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasPlaylistUpdates) {
|
|
||||||
existingPlaylist.update(playlist)
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
return hasUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
static getFromOld(oldPlaylist) {
|
static getFromOld(oldPlaylist) {
|
||||||
return {
|
return {
|
||||||
id: oldPlaylist.id,
|
id: oldPlaylist.id,
|
||||||
@ -196,7 +152,7 @@ module.exports = (sequelize) => {
|
|||||||
* Get playlists for user and optionally for library
|
* Get playlists for user and optionally for library
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {[string]} libraryId optional
|
* @param {[string]} libraryId optional
|
||||||
* @returns {Promise<oldPlaylist[]>}
|
* @returns {Promise<Playlist[]>}
|
||||||
*/
|
*/
|
||||||
static async getPlaylistsForUserAndLibrary(userId, libraryId = null) {
|
static async getPlaylistsForUserAndLibrary(userId, libraryId = null) {
|
||||||
if (!userId && !libraryId) return []
|
if (!userId && !libraryId) return []
|
||||||
@ -225,9 +181,12 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
order: [['playlistMediaItems', 'order', 'ASC']]
|
order: [
|
||||||
|
[literal('name COLLATE NOCASE'), 'ASC'],
|
||||||
|
['playlistMediaItems', 'order', 'ASC']
|
||||||
|
]
|
||||||
})
|
})
|
||||||
return playlists.map(p => this.getOldPlaylist(p))
|
return playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,21 +70,21 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.post('/libraries', LibraryController.create.bind(this))
|
this.router.post('/libraries', LibraryController.create.bind(this))
|
||||||
this.router.get('/libraries', LibraryController.findAll.bind(this))
|
this.router.get('/libraries', LibraryController.findAll.bind(this))
|
||||||
this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this))
|
this.router.get('/libraries/:id', LibraryController.middlewareNew.bind(this), LibraryController.findOne.bind(this))
|
||||||
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
|
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
|
||||||
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
||||||
|
|
||||||
this.router.get('/libraries/:id/items2', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryItemsNew.bind(this))
|
this.router.get('/libraries/:id/items2', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryItemsNew.bind(this))
|
||||||
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.middleware.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.middleware.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.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.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.middleware.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/personalized2', 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/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
||||||
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.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.middleware.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.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||||
@ -447,11 +447,14 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Database.removeLibraryItem(libraryItem.id)
|
await Database.removeLibraryItem(libraryItem.id)
|
||||||
SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
|
|
||||||
|
SocketAuthority.emitter('item_removed', {
|
||||||
|
id: libraryItem.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkRemoveEmptySeries(seriesToCheck, excludeLibraryItemId = null) {
|
async checkRemoveEmptySeries(seriesToCheck, excludeLibraryItemId = null) {
|
||||||
if (!seriesToCheck || !seriesToCheck.length) return
|
if (!seriesToCheck?.length) return
|
||||||
|
|
||||||
for (const series of seriesToCheck) {
|
for (const series of seriesToCheck) {
|
||||||
const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
|
const Logger = require('../../Logger')
|
||||||
const Database = require('../../Database')
|
const Database = require('../../Database')
|
||||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||||
|
const { createNewSortInstance } = require('../../libs/fastSort')
|
||||||
|
const naturalSort = createNewSortInstance({
|
||||||
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
decode(text) {
|
decode(text) {
|
||||||
@ -381,5 +386,109 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
getLibraryItemsForCollection(collection) {
|
getLibraryItemsForCollection(collection) {
|
||||||
return libraryItemsBookFilters.getLibraryItemsForCollection(collection)
|
return libraryItemsBookFilters.getLibraryItemsForCollection(collection)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filter data used in filter menus
|
||||||
|
* @param {oldLibrary} oldLibrary
|
||||||
|
* @returns {Promise<object>}
|
||||||
|
*/
|
||||||
|
async getFilterData(oldLibrary) {
|
||||||
|
const cachedFilterData = Database.libraryFilterData[oldLibrary.id]
|
||||||
|
if (cachedFilterData) {
|
||||||
|
const cacheElapsed = Date.now() - cachedFilterData.loadedAt
|
||||||
|
// Cache library filters for 30 mins
|
||||||
|
// TODO: Keep cached filter data up-to-date on updates
|
||||||
|
if (cacheElapsed < 1000 * 60 * 30) {
|
||||||
|
return cachedFilterData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const start = Date.now() // Temp for checking load times
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
authors: [],
|
||||||
|
genres: new Set(),
|
||||||
|
tags: new Set(),
|
||||||
|
series: [],
|
||||||
|
narrators: new Set(),
|
||||||
|
languages: new Set(),
|
||||||
|
publishers: new Set(),
|
||||||
|
numIssues: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldLibrary.isPodcast) {
|
||||||
|
const podcasts = await Database.models.podcast.findAll({
|
||||||
|
include: {
|
||||||
|
model: Database.models.libraryItem,
|
||||||
|
attributes: [],
|
||||||
|
where: {
|
||||||
|
libraryId: oldLibrary.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: ['tags', 'genres']
|
||||||
|
})
|
||||||
|
for (const podcast of podcasts) {
|
||||||
|
if (podcast.tags?.length) {
|
||||||
|
podcast.tags.forEach((tag) => data.tags.add(tag))
|
||||||
|
}
|
||||||
|
if (podcast.genres?.length) {
|
||||||
|
podcast.genres.forEach((genre) => data.genres.add(genre))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const books = await Database.models.book.findAll({
|
||||||
|
include: {
|
||||||
|
model: Database.models.libraryItem,
|
||||||
|
attributes: ['isMissing', 'isInvalid'],
|
||||||
|
where: {
|
||||||
|
libraryId: oldLibrary.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: ['tags', 'genres', 'publisher', 'narrators', 'language']
|
||||||
|
})
|
||||||
|
for (const book of books) {
|
||||||
|
if (book.libraryItem.isMissing || book.libraryItem.isInvalid) data.numIssues++
|
||||||
|
if (book.tags?.length) {
|
||||||
|
book.tags.forEach((tag) => data.tags.add(tag))
|
||||||
|
}
|
||||||
|
if (book.genres?.length) {
|
||||||
|
book.genres.forEach((genre) => data.genres.add(genre))
|
||||||
|
}
|
||||||
|
if (book.narrators?.length) {
|
||||||
|
book.narrators.forEach((narrator) => data.narrators.add(narrator))
|
||||||
|
}
|
||||||
|
if (book.publisher) data.publishers.add(book.publisher)
|
||||||
|
if (book.language) data.languages.add(book.language)
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = await Database.models.series.findAll({
|
||||||
|
where: {
|
||||||
|
libraryId: oldLibrary.id
|
||||||
|
},
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
})
|
||||||
|
series.forEach((s) => data.series.push({ id: s.id, name: s.name }))
|
||||||
|
|
||||||
|
const authors = await Database.models.author.findAll({
|
||||||
|
where: {
|
||||||
|
libraryId: oldLibrary.id
|
||||||
|
},
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
})
|
||||||
|
authors.forEach((a) => data.authors.push({ id: a.id, name: a.name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
data.loadedAt = Date.now()
|
||||||
|
Database.libraryFilterData[oldLibrary.id] = data
|
||||||
|
|
||||||
|
Logger.debug(`Loaded filterdata in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -914,5 +914,16 @@ module.exports = {
|
|||||||
libraryItem.media = book
|
libraryItem.media = book
|
||||||
return libraryItem
|
return libraryItem
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get library items for series
|
||||||
|
* @param {oldSeries} oldSeries
|
||||||
|
* @param {[oldUser]} oldUser
|
||||||
|
* @returns {Promise<oldLibraryItem[]>}
|
||||||
|
*/
|
||||||
|
async getLibraryItemsForSeries(oldSeries, oldUser) {
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user