mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-27 03:31:46 +00:00
Merge branch 'advplyr:master' into social
This commit is contained in:
commit
2a5ac8f094
65 changed files with 3170 additions and 374 deletions
|
|
@ -440,26 +440,7 @@ class Server {
|
|||
return
|
||||
}
|
||||
|
||||
// Check if user has session open
|
||||
var session = this.playbackSessionManager.getUserSession(user.id)
|
||||
if (session) {
|
||||
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
|
||||
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||
if (!sessionLibraryItem) {
|
||||
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
||||
this.playbackSessionManager.removeSession(session.id)
|
||||
session = null
|
||||
} else if (session.mediaType === 'podcast' && !sessionLibraryItem.media.checkHasEpisode(session.episodeId)) {
|
||||
Logger.error(`[Server] Library Item for session "${session.id}" episode ${session.episodeId} does not exist "${session.libraryItemId}"`)
|
||||
this.playbackSessionManager.removeSession(session.id)
|
||||
session = null
|
||||
}
|
||||
if (session) {
|
||||
session = session.toJSONForClient(sessionLibraryItem)
|
||||
}
|
||||
} else {
|
||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||
}
|
||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||
|
||||
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||
|
||||
|
|
@ -470,7 +451,6 @@ class Server {
|
|||
metadataPath: global.MetadataPath,
|
||||
configPath: global.ConfigPath,
|
||||
user: client.user.toJSONForBrowser(),
|
||||
session,
|
||||
librariesScanning: this.scanner.librariesScanning,
|
||||
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,18 +24,11 @@ class CollectionController {
|
|||
}
|
||||
|
||||
findOne(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(req.collection.toJSONExpanded(this.db.libraryItems))
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
const collection = req.collection
|
||||
var wasUpdated = collection.update(req.body)
|
||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
if (wasUpdated) {
|
||||
|
|
@ -46,10 +39,7 @@ class CollectionController {
|
|||
}
|
||||
|
||||
async delete(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
const collection = req.collection
|
||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.removeEntity('collection', collection.id)
|
||||
this.emitter('collection_removed', jsonExpanded)
|
||||
|
|
@ -57,10 +47,7 @@ class CollectionController {
|
|||
}
|
||||
|
||||
async addBook(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
const collection = req.collection
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(500).send('Book not found')
|
||||
|
|
@ -80,11 +67,7 @@ class CollectionController {
|
|||
|
||||
// DELETE: api/collections/:id/book/:bookId
|
||||
async removeBook(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
||||
const collection = req.collection
|
||||
if (collection.books.includes(req.params.bookId)) {
|
||||
collection.removeBook(req.params.bookId)
|
||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
|
|
@ -96,10 +79,7 @@ class CollectionController {
|
|||
|
||||
// POST: api/collections/:id/batch/add
|
||||
async addBatch(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
const collection = req.collection
|
||||
if (!req.body.books || !req.body.books.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
|
|
@ -120,10 +100,7 @@ class CollectionController {
|
|||
|
||||
// POST: api/collections/:id/batch/remove
|
||||
async removeBatch(req, res) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
const collection = req.collection
|
||||
if (!req.body.books || !req.body.books.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
|
|
@ -141,5 +118,25 @@ class CollectionController {
|
|||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
req.collection = collection
|
||||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[CollectionController] User attempted to delete without permission`, req.user.username)
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||
Logger.warn('[CollectionController] User attempted to update without permission', req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new CollectionController()
|
||||
|
|
@ -165,6 +165,7 @@ class LibraryController {
|
|||
if (payload.filterBy) {
|
||||
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
||||
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
||||
if (filterSeries === 'No Series') filterSeries = null
|
||||
|
||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||
payload.total = libraryItems.length
|
||||
|
|
|
|||
|
|
@ -51,11 +51,6 @@ class LibraryItemController {
|
|||
|
||||
var hasUpdates = libraryItem.update(req.body)
|
||||
if (hasUpdates) {
|
||||
// Turn on podcast auto download cron if not already on
|
||||
if (libraryItem.mediaType == 'podcast' && req.body.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
|
||||
this.podcastManager.schedulePodcastEpisodeCron()
|
||||
}
|
||||
|
||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
|
|
|||
|
|
@ -96,9 +96,9 @@ class MeController {
|
|||
|
||||
var shouldUpdate = false
|
||||
itemProgressPayloads.forEach((itemProgress) => {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.id) // Make sure this library item exists
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
if (libraryItem) {
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress)
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)
|
||||
if (wasUpdated) shouldUpdate = true
|
||||
} else {
|
||||
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ class PodcastController {
|
|||
}
|
||||
|
||||
// Turn on podcast auto download cron if not already on
|
||||
if (libraryItem.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) {
|
||||
this.podcastManager.schedulePodcastEpisodeCron()
|
||||
if (libraryItem.media.autoDownloadEpisodes) {
|
||||
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +140,9 @@ class PodcastController {
|
|||
return res.status(500).send('Podcast has no rss feed url')
|
||||
}
|
||||
|
||||
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem)
|
||||
const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3
|
||||
|
||||
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload)
|
||||
res.json({
|
||||
episodes: newEpisodes || []
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class BackupManager {
|
|||
constructor(db, emitter) {
|
||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
|
||||
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
|
||||
|
||||
this.db = db
|
||||
this.emitter = emitter
|
||||
|
|
@ -119,6 +120,7 @@ class BackupManager {
|
|||
await zip.extract('config/', global.ConfigPath)
|
||||
if (backup.backupMetadataCovers) {
|
||||
await zip.extract('metadata-items/', this.ItemsMetadataPath)
|
||||
await zip.extract('metadata-authors/', this.AuthorsMetadataPath)
|
||||
}
|
||||
await this.db.reinit()
|
||||
this.emitter('backup_applied')
|
||||
|
|
@ -178,8 +180,6 @@ class BackupManager {
|
|||
async runBackup() {
|
||||
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
|
||||
Logger.info(`[BackupManager] Running Backup`)
|
||||
var metadataItemsPath = this.serverSettings.backupMetadataCovers ? this.ItemsMetadataPath : null
|
||||
|
||||
var newBackup = new Backup()
|
||||
|
||||
const newBackData = {
|
||||
|
|
@ -188,7 +188,10 @@ class BackupManager {
|
|||
}
|
||||
newBackup.setData(newBackData)
|
||||
|
||||
var zipResult = await this.zipBackup(metadataItemsPath, newBackup).then(() => true).catch((error) => {
|
||||
var metadataAuthorsPath = this.AuthorsMetadataPath
|
||||
if (!await fs.pathExists(metadataAuthorsPath)) metadataAuthorsPath = null
|
||||
|
||||
var zipResult = await this.zipBackup(metadataAuthorsPath, newBackup).then(() => true).catch((error) => {
|
||||
Logger.error(`[BackupManager] Backup Failed ${error}`)
|
||||
return false
|
||||
})
|
||||
|
|
@ -228,7 +231,7 @@ class BackupManager {
|
|||
}
|
||||
}
|
||||
|
||||
zipBackup(metadataItemsPath, backup) {
|
||||
zipBackup(metadataAuthorsPath, backup) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// create a file to stream archive data to
|
||||
const output = fs.createWriteStream(backup.fullPath)
|
||||
|
|
@ -299,10 +302,16 @@ class BackupManager {
|
|||
archive.directory(this.db.AuthorsPath, 'config/authors')
|
||||
archive.directory(this.db.SeriesPath, 'config/series')
|
||||
|
||||
if (metadataItemsPath) {
|
||||
Logger.debug(`[BackupManager] Backing up Metadata Items "${metadataItemsPath}"`)
|
||||
archive.directory(metadataItemsPath, 'metadata-items')
|
||||
if (this.serverSettings.backupMetadataCovers) {
|
||||
Logger.debug(`[BackupManager] Backing up Metadata Items "${this.ItemsMetadataPath}"`)
|
||||
archive.directory(this.ItemsMetadataPath, 'metadata-items')
|
||||
|
||||
if (metadataAuthorsPath) {
|
||||
Logger.debug(`[BackupManager] Backing up Metadata Authors "${metadataAuthorsPath}"`)
|
||||
archive.directory(metadataAuthorsPath, 'metadata-authors')
|
||||
}
|
||||
}
|
||||
|
||||
archive.append(backup.detailsString, { name: 'details' })
|
||||
|
||||
archive.finalize()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class PlaybackSessionManager {
|
|||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.sessions = []
|
||||
this.localSessionLock = {}
|
||||
}
|
||||
|
||||
getSession(sessionId) {
|
||||
|
|
@ -58,18 +59,26 @@ class PlaybackSessionManager {
|
|||
}
|
||||
|
||||
async syncLocalSessionRequest(user, sessionJson, res) {
|
||||
if (this.localSessionLock[sessionJson.id]) {
|
||||
Logger.debug(`[PlaybackSessionManager] syncLocalSessionRequest: Local session is locked and already syncing`)
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
|
||||
var libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`)
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
|
||||
this.localSessionLock[sessionJson.id] = true // Lock local session
|
||||
|
||||
var session = await this.db.getPlaybackSession(sessionJson.id)
|
||||
if (!session) {
|
||||
// New session from local
|
||||
session = new PlaybackSession(sessionJson)
|
||||
await this.db.insertEntity('session', session)
|
||||
} else {
|
||||
session.currentTime = sessionJson.currentTime
|
||||
session.timeListening = sessionJson.timeListening
|
||||
session.updatedAt = sessionJson.updatedAt
|
||||
session.date = date.format(new Date(), 'YYYY-MM-DD')
|
||||
|
|
@ -94,6 +103,9 @@ class PlaybackSessionManager {
|
|||
data: itemProgress.toJSON()
|
||||
})
|
||||
}
|
||||
|
||||
delete this.localSessionLock[sessionJson.id] // Unlock local session
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
|
@ -256,4 +268,4 @@ class PlaybackSessionManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
module.exports = PlaybackSessionManager
|
||||
module.exports = PlaybackSessionManager
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class PodcastManager {
|
|||
return libraryItem.media.autoDownloadEpisodes
|
||||
}
|
||||
|
||||
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter) {
|
||||
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter, maxNewEpisodes = 3) {
|
||||
if (!podcastLibraryItem.media.metadata.feedUrl) {
|
||||
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
|
||||
return false
|
||||
|
|
@ -234,15 +234,18 @@ class PodcastManager {
|
|||
|
||||
// Filter new and not already has
|
||||
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
||||
// Max new episodes for safety = 3
|
||||
newEpisodes = newEpisodes.slice(0, 3)
|
||||
|
||||
if (maxNewEpisodes > 0) {
|
||||
newEpisodes = newEpisodes.slice(0, maxNewEpisodes)
|
||||
}
|
||||
|
||||
return newEpisodes
|
||||
}
|
||||
|
||||
async checkAndDownloadNewEpisodes(libraryItem) {
|
||||
async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
|
||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
|
||||
if (newEpisodes.length) {
|
||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class RssFeedManager {
|
|||
return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId)
|
||||
}
|
||||
|
||||
getFeed(req, res) {
|
||||
async getFeed(req, res) {
|
||||
var feed = this.feeds[req.params.id]
|
||||
if (!feed) {
|
||||
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
|
||||
|
|
@ -38,6 +38,15 @@ class RssFeedManager {
|
|||
return
|
||||
}
|
||||
|
||||
if (feed.entityType === 'item') {
|
||||
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
||||
if (libraryItem && (!feed.entityUpdatedAt || libraryItem.updatedAt > feed.entityUpdatedAt)) {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
||||
feed.updateFromItem(libraryItem)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
}
|
||||
}
|
||||
|
||||
var xml = feed.buildXml()
|
||||
res.set('Content-Type', 'text/xml')
|
||||
res.send(xml)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Feed {
|
|||
this.userId = null
|
||||
this.entityType = null
|
||||
this.entityId = null
|
||||
this.entityUpdatedAt = null
|
||||
|
||||
this.coverPath = null
|
||||
this.serverAddress = null
|
||||
|
|
@ -79,6 +80,7 @@ class Feed {
|
|||
this.userId = userId
|
||||
this.entityType = 'item'
|
||||
this.entityId = libraryItem.id
|
||||
this.entityUpdatedAt = libraryItem.updatedAt
|
||||
this.coverPath = media.coverPath || null
|
||||
this.serverAddress = serverAddress
|
||||
this.feedUrl = feedUrl
|
||||
|
|
@ -111,6 +113,39 @@ class Feed {
|
|||
this.updatedAt = Date.now()
|
||||
}
|
||||
|
||||
updateFromItem(libraryItem) {
|
||||
const media = libraryItem.media
|
||||
const mediaMetadata = media.metadata
|
||||
const isPodcast = libraryItem.mediaType === 'podcast'
|
||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||
|
||||
this.entityUpdatedAt = libraryItem.updatedAt
|
||||
|
||||
this.meta.title = mediaMetadata.title
|
||||
this.meta.description = mediaMetadata.description
|
||||
this.meta.author = author
|
||||
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
|
||||
this.meta.explicit = !!mediaMetadata.explicit
|
||||
|
||||
this.episodes = []
|
||||
if (isPodcast) { // PODCAST EPISODES
|
||||
media.episodes.forEach((episode) => {
|
||||
var feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
} else { // AUDIOBOOK EPISODES
|
||||
media.tracks.forEach((audioTrack) => {
|
||||
var feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
}
|
||||
|
||||
this.updatedAt = Date.now()
|
||||
this.xml = null
|
||||
}
|
||||
|
||||
buildXml() {
|
||||
if (this.xml) return this.xml
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class FeedEpisode {
|
|||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt - timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||
|
||||
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
||||
const media = libraryItem.media
|
||||
|
|
|
|||
|
|
@ -116,16 +116,16 @@ class ApiRouter {
|
|||
//
|
||||
// Collection Routes
|
||||
//
|
||||
this.router.post('/collections', CollectionController.create.bind(this))
|
||||
this.router.post('/collections', CollectionController.middleware.bind(this), CollectionController.create.bind(this))
|
||||
this.router.get('/collections', CollectionController.findAll.bind(this))
|
||||
this.router.get('/collections/:id', CollectionController.findOne.bind(this))
|
||||
this.router.patch('/collections/:id', CollectionController.update.bind(this))
|
||||
this.router.delete('/collections/:id', CollectionController.delete.bind(this))
|
||||
this.router.get('/collections/:id', CollectionController.middleware.bind(this), CollectionController.findOne.bind(this))
|
||||
this.router.patch('/collections/:id', CollectionController.middleware.bind(this), CollectionController.update.bind(this))
|
||||
this.router.delete('/collections/:id', CollectionController.middleware.bind(this), CollectionController.delete.bind(this))
|
||||
|
||||
this.router.post('/collections/:id/book', CollectionController.addBook.bind(this))
|
||||
this.router.delete('/collections/:id/book/:bookId', CollectionController.removeBook.bind(this))
|
||||
this.router.post('/collections/:id/batch/add', CollectionController.addBatch.bind(this))
|
||||
this.router.post('/collections/:id/batch/remove', CollectionController.removeBatch.bind(this))
|
||||
this.router.post('/collections/:id/book', CollectionController.middleware.bind(this), CollectionController.addBook.bind(this))
|
||||
this.router.delete('/collections/:id/book/:bookId', CollectionController.middleware.bind(this), CollectionController.removeBook.bind(this))
|
||||
this.router.post('/collections/:id/batch/add', CollectionController.middleware.bind(this), CollectionController.addBatch.bind(this))
|
||||
this.router.post('/collections/:id/batch/remove', CollectionController.middleware.bind(this), CollectionController.removeBatch.bind(this))
|
||||
|
||||
//
|
||||
// Current User Routes (Me)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const { ScanResult, LogLevel } = require('../utils/constants')
|
|||
|
||||
const MediaFileScanner = require('./MediaFileScanner')
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const PodcastFinder = require('../finders/PodcastFinder')
|
||||
const LibraryItem = require('../objects/LibraryItem')
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const ScanOptions = require('./ScanOptions')
|
||||
|
|
@ -28,7 +29,12 @@ class Scanner {
|
|||
this.cancelLibraryScan = {}
|
||||
this.librariesScanning = []
|
||||
|
||||
// Watcher file update scan vars
|
||||
this.pendingFileUpdatesToScan = []
|
||||
this.scanningFilesChanged = false
|
||||
|
||||
this.bookFinder = new BookFinder()
|
||||
this.podcastFinder = new PodcastFinder()
|
||||
}
|
||||
|
||||
isLibraryScanning(libraryId) {
|
||||
|
|
@ -494,7 +500,16 @@ class Scanner {
|
|||
}
|
||||
|
||||
async scanFilesChanged(fileUpdates) {
|
||||
if (!fileUpdates.length) return
|
||||
if (!fileUpdates || !fileUpdates.length) return
|
||||
|
||||
// If already scanning files from watcher then add these updates to queue
|
||||
if (this.scanningFilesChanged) {
|
||||
this.pendingFileUpdatesToScan.push(fileUpdates)
|
||||
Logger.debug(`[Scanner] Already scanning files from watcher - file updates pushed to queue (size ${this.pendingFileUpdatesToScan.length})`)
|
||||
return
|
||||
}
|
||||
this.scanningFilesChanged = true
|
||||
|
||||
// files grouped by folder
|
||||
var folderGroups = this.getFileUpdatesGrouped(fileUpdates)
|
||||
|
||||
|
|
@ -520,6 +535,13 @@ class Scanner {
|
|||
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
|
||||
Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
|
||||
}
|
||||
|
||||
this.scanningFilesChanged = false
|
||||
|
||||
if (this.pendingFileUpdatesToScan.length) {
|
||||
Logger.debug(`[Scanner] File updates finished scanning with more updates in queue (${this.pendingFileUpdatesToScan.length})`)
|
||||
this.scanFilesChanged(this.pendingFileUpdatesToScan.shift())
|
||||
}
|
||||
}
|
||||
|
||||
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
||||
|
|
@ -652,16 +674,6 @@ class Scanner {
|
|||
var provider = options.provider || 'google'
|
||||
var searchTitle = options.title || libraryItem.media.metadata.title
|
||||
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
||||
var searchISBN = options.isbn || libraryItem.media.metadata.isbn
|
||||
var searchASIN = options.asin || libraryItem.media.metadata.asin
|
||||
|
||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
|
||||
if (!results.length) {
|
||||
return {
|
||||
warning: `No ${provider} match found`
|
||||
}
|
||||
}
|
||||
var matchData = results[0]
|
||||
|
||||
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true
|
||||
if (this.db.serverSettings.scannerPreferMatchedMetadata) {
|
||||
|
|
@ -669,18 +681,110 @@ class Scanner {
|
|||
options.overrideDetails = true
|
||||
}
|
||||
|
||||
// Update cover if not set OR overrideCover flag
|
||||
var updatePayload = {}
|
||||
var hasUpdated = false
|
||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
} else {
|
||||
|
||||
if (libraryItem.mediaType === 'book') {
|
||||
var searchISBN = options.isbn || libraryItem.media.metadata.isbn
|
||||
var searchASIN = options.asin || libraryItem.media.metadata.asin
|
||||
|
||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
|
||||
if (!results.length) {
|
||||
return {
|
||||
warning: `No ${provider} match found`
|
||||
}
|
||||
}
|
||||
var matchData = results[0]
|
||||
|
||||
// Update cover if not set OR overrideCover flag
|
||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
} else {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options)
|
||||
} else { // Podcast quick match
|
||||
var results = await this.podcastFinder.search(searchTitle)
|
||||
if (!results.length) {
|
||||
return {
|
||||
warning: `No ${provider} match found`
|
||||
}
|
||||
}
|
||||
var matchData = results[0]
|
||||
|
||||
// Update cover if not set OR overrideCover flag
|
||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
} else {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
updatePayload = this.quickMatchPodcastBuildUpdatePayload(libraryItem, matchData, options)
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||
if (libraryItem.media.update(updatePayload)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
return {
|
||||
updated: hasUpdated,
|
||||
libraryItem: libraryItem.toJSONExpanded()
|
||||
}
|
||||
}
|
||||
|
||||
quickMatchPodcastBuildUpdatePayload(libraryItem, matchData, options) {
|
||||
const updatePayload = {}
|
||||
updatePayload.metadata = {}
|
||||
|
||||
const matchDataTransformed = {
|
||||
title: matchData.title || null,
|
||||
author: matchData.artistName || null,
|
||||
genres: matchData.genres || [],
|
||||
itunesId: matchData.id || null,
|
||||
itunesPageUrl: matchData.pageUrl || null,
|
||||
itunesArtistId: matchData.artistId || null,
|
||||
releaseDate: matchData.releaseDate || null,
|
||||
imageUrl: matchData.cover || null,
|
||||
description: matchData.descriptionPlain || null
|
||||
}
|
||||
|
||||
for (const key in matchDataTransformed) {
|
||||
if (matchDataTransformed[key]) {
|
||||
if (key === 'genres') {
|
||||
if ((!libraryItem.media.metadata.genres || options.overrideDetails)) {
|
||||
updatePayload.metadata[key] = matchDataTransformed[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||
}
|
||||
} else if (!libraryItem.media.metadata[key] || options.overrideDetails) {
|
||||
updatePayload.metadata[key] = matchDataTransformed[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(updatePayload.metadata).length) {
|
||||
delete updatePayload.metadata
|
||||
}
|
||||
|
||||
return updatePayload
|
||||
}
|
||||
|
||||
async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) {
|
||||
// Update media metadata if not set OR overrideDetails flag
|
||||
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'asin', 'isbn']
|
||||
const updatePayload = {}
|
||||
|
|
@ -743,22 +847,11 @@ class Scanner {
|
|||
updatePayload.metadata.series = seriesPayload
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||
if (libraryItem.media.update(updatePayload)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
if (!Object.keys(updatePayload.metadata).length) {
|
||||
delete updatePayload.metadata
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
return {
|
||||
updated: hasUpdated,
|
||||
libraryItem: libraryItem.toJSONExpanded()
|
||||
}
|
||||
return updatePayload
|
||||
}
|
||||
|
||||
async matchLibraryItems(library) {
|
||||
|
|
|
|||
2235
server/utils/htmlEntities.js
Normal file
2235
server/utils/htmlEntities.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
const sanitizeHtml = require('../libs/sanitizeHtml')
|
||||
const {entities} = require("./htmlEntities");
|
||||
|
||||
function sanitize(html) {
|
||||
const sanitizerOptions = {
|
||||
|
|
@ -17,12 +18,22 @@ function sanitize(html) {
|
|||
}
|
||||
module.exports.sanitize = sanitize
|
||||
|
||||
function stripAllTags(html) {
|
||||
function stripAllTags(html, shouldDecodeEntities = true) {
|
||||
const sanitizerOptions = {
|
||||
allowedTags: [],
|
||||
disallowedTagsMode: 'discard'
|
||||
}
|
||||
|
||||
return sanitizeHtml(html, sanitizerOptions)
|
||||
let sanitized = sanitizeHtml(html, sanitizerOptions)
|
||||
return shouldDecodeEntities ? decodeHTMLEntities(sanitized) : sanitized
|
||||
}
|
||||
module.exports.stripAllTags = stripAllTags
|
||||
|
||||
function decodeHTMLEntities(strToDecode) {
|
||||
return strToDecode.replace(/\&([^;]+);?/g, function (entity) {
|
||||
if (entity in entities) {
|
||||
return entities[entity]
|
||||
}
|
||||
return entity;
|
||||
})
|
||||
}
|
||||
module.exports.stripAllTags = stripAllTags
|
||||
Loading…
Add table
Add a link
Reference in a new issue