Merge branch 'master' into social

This commit is contained in:
Ben 2022-08-20 11:52:41 -04:00 committed by GitHub
commit 92c393d2b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
351 changed files with 53592 additions and 1765 deletions

View file

@ -82,31 +82,59 @@ class AuthorController {
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
var hasUpdated = req.author.update(payload)
if (hasUpdated) {
if (authorNameUpdate) { // Update author name on all books
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
itemsWithAuthor.forEach(libraryItem => {
libraryItem.media.metadata.updateAuthor(req.author)
})
if (itemsWithAuthor.length) {
await this.db.updateLibraryItems(itemsWithAuthor)
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
}
// Check if author name matches another author and merge the authors
var existingAuthor = authorNameUpdate ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
if (existingAuthor) {
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
})
if (itemsWithAuthor.length) {
await this.db.updateLibraryItems(itemsWithAuthor)
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
}
await this.db.updateEntity('author', req.author)
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
}).length
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
// Remove old author
await this.db.removeEntity('author', req.author.id)
this.emitter('author_removed', req.author.toJSON())
res.json({
author: req.author.toJSON(),
updated: hasUpdated
})
// Send updated num books for merged author
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
}).length
this.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
res.json({
author: existingAuthor.toJSON(),
merged: true
})
} else { // Regular author update
var hasUpdated = req.author.update(payload)
if (hasUpdated) {
if (authorNameUpdate) { // Update author name on all books
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
itemsWithAuthor.forEach(libraryItem => {
libraryItem.media.metadata.updateAuthor(req.author)
})
if (itemsWithAuthor.length) {
await this.db.updateLibraryItems(itemsWithAuthor)
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
}
}
await this.db.updateEntity('author', req.author)
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
}).length
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
res.json({
author: req.author.toJSON(),
updated: hasUpdated
})
}
}
async search(req, res) {

View file

@ -108,6 +108,9 @@ class LibraryController {
// Update watcher
this.watcher.updateLibrary(library)
// Update auto scan cron
this.cronManager.updateLibraryScanCron(library)
// Remove libraryItems no longer in library
var itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
if (itemsToRemove.length) {
@ -163,7 +166,7 @@ class LibraryController {
// 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
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
payload.total = libraryItems.length
}
@ -176,7 +179,8 @@ class LibraryController {
}
// Handle server setting sortingIgnorePrefix
if (sortKey === 'media.metadata.title' && this.db.serverSettings.sortingIgnorePrefix) {
const sortByTitle = sortKey === 'media.metadata.title'
if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) {
// BookMetadata.js has titleIgnorePrefix getter
sortKey += 'IgnorePrefix'
}
@ -186,6 +190,16 @@ class LibraryController {
var sortArray = [
{
[direction]: (li) => {
// When collapsing by series and sorting by title use the series name instead of the book title
if (payload.mediaType === 'book' && payload.collapseseries && li.media.metadata.seriesName) {
if (sortByTitle) {
return this.db.serverSettings.sortingIgnorePrefix ? li.media.metadata.seriesNameIgnorePrefix : li.media.metadata.seriesName
} else {
// When not sorting by title always show the collapsed series at the end
return direction === 'desc' ? -1 : 'zzzz'
}
}
// Supports dot notation strings i.e. "media.metadata.title"
return sortKey.split('.').reduce((a, b) => a[b], li)
}
@ -262,7 +276,7 @@ class LibraryController {
var series = libraryHelpers.getSeriesFromBooks(libraryItems, payload.minified)
series = sort(series).asc(s => {
return s.name
return this.db.serverSettings.sortingIgnorePrefix ? s.nameIgnorePrefix : s.name
})
payload.total = series.length

View file

@ -79,12 +79,27 @@ class LibraryItemController {
await this.cacheManager.purgeCoverCache(libraryItem.id)
}
// Book specific
if (libraryItem.isBook) {
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
}
// Podcast specific
var isPodcastAutoDownloadUpdated = false
if (libraryItem.isPodcast) {
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
isPodcastAutoDownloadUpdated = true
} else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
isPodcastAutoDownloadUpdated = true
}
}
var hasUpdates = libraryItem.media.update(mediaPayload)
if (hasUpdates) {
if (isPodcastAutoDownloadUpdated) {
this.cronManager.checkUpdatePodcastCron(libraryItem)
}
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded())

View file

@ -1,4 +1,5 @@
const Logger = require('../Logger')
const { sort } = require('../libs/fastSort')
const { isObject, toNumber } = require('../utils/index')
class MeController {
@ -190,6 +191,7 @@ class MeController {
const updatedLocalMediaProgress = []
var numServerProgressUpdates = 0
var localMediaProgress = req.body.localMediaProgress || []
localMediaProgress.forEach(localProgress => {
if (!localProgress.libraryItemId) {
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
@ -216,7 +218,8 @@ class MeController {
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
for (const key in localProgress) {
if (mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) {
// Local media progress ID uses the local library item id and server media progress uses the library item id
if (key !== 'id' && mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) {
// Logger.debug(`[MeController] syncLocalMediaProgress key ${key} changed from ${localProgress[key]} to ${mediaProgress[key]} - ${mediaProgress.id}`)
localProgress[key] = mediaProgress[key]
}
@ -238,5 +241,40 @@ class MeController {
localProgressUpdates: updatedLocalMediaProgress
})
}
// GET: api/me/items-in-progress
async getAllLibraryItemsInProgress(req, res) {
const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25
var itemsInProgress = []
for (const mediaProgress of req.user.mediaProgress) {
if (!mediaProgress.isFinished && mediaProgress.progress > 0) {
const libraryItem = await this.db.getLibraryItem(mediaProgress.libraryItemId)
if (libraryItem) {
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
if (episode) {
const libraryItemWithEpisode = {
...libraryItem.toJSONMinified(),
recentEpisode: episode.toJSON(),
progressLastUpdate: mediaProgress.lastUpdate
}
itemsInProgress.push(libraryItemWithEpisode)
}
} else if (!mediaProgress.episodeId) {
itemsInProgress.push({
...libraryItem.toJSONMinified(),
progressLastUpdate: mediaProgress.lastUpdate
})
}
}
}
}
itemsInProgress = sort(itemsInProgress).desc(li => li.progressLastUpdate).slice(0, limit)
res.json({
libraryItems: itemsInProgress
})
}
}
module.exports = new MeController()

View file

@ -2,7 +2,7 @@ const Path = require('path')
const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const filePerms = require('../utils/filePerms')
const patternValidation = require('../libs/nodeCron/pattern-validation')
const { isObject } = require('../utils/index')
//
@ -239,12 +239,7 @@ class MiscController {
Logger.error('Invalid user in authorize')
return res.sendStatus(401)
}
const userResponse = {
user: req.user,
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
serverSettings: this.db.serverSettings.toJSON(),
Source: global.Source
}
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
res.json(userResponse)
}
@ -290,6 +285,20 @@ class MiscController {
}
}
res.json(userData)
validateCronExpression(req, res) {
const expression = req.body.expression
if (!expression) {
return res.sendStatus(400)
}
try {
patternValidation(expression)
res.sendStatus(200)
} catch (error) {
Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message)
res.status(400).send(error.message)
}
}
}
module.exports = new MiscController()

View file

@ -164,6 +164,25 @@ class PodcastController {
})
}
async findEpisode(req, res) {
const rssFeedUrl = req.libraryItem.media.metadata.feedUrl
if (!rssFeedUrl) {
Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`)
return res.status(500).send('Podcast does not have an RSS feed URL')
}
var searchTitle = req.query.title
if (!searchTitle) {
return res.sendStatus(500)
}
searchTitle = searchTitle.toLowerCase().trim()
const episodes = await this.podcastManager.findEpisode(rssFeedUrl, searchTitle)
res.json({
episodes: episodes || []
})
}
async downloadEpisodes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
@ -185,7 +204,7 @@ class PodcastController {
var episodeId = req.params.episodeId
if (!libraryItem.media.checkHasEpisode(episodeId)) {
return res.status(500).send('Episode not found')
return res.status(404).send('Episode not found')
}
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)

View file

@ -42,7 +42,7 @@ class SessionController {
res.json(payload)
}
getSession(req, res) {
getOpenSession(req, res) {
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
var sessionForClient = req.session.toJSONForClient(libraryItem)
res.json(sessionForClient)
@ -58,12 +58,24 @@ class SessionController {
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
}
// DELETE: api/session/:id
async delete(req, res) {
// if session is open then remove it
const openSession = this.playbackSessionManager.getSession(req.session.id)
if (openSession) {
await this.playbackSessionManager.removeSession(req.session.id)
}
await this.db.removeEntity('session', req.session.id)
res.sendStatus(200)
}
// POST: api/session/local
syncLocal(req, res) {
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
}
middleware(req, res, next) {
openSessionMiddleware(req, res, next) {
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) return res.sendStatus(404)
@ -75,5 +87,21 @@ class SessionController {
req.session = playbackSession
next()
}
async middleware(req, res, next) {
var playbackSession = await this.db.getPlaybackSession(req.params.id)
if (!playbackSession) return res.sendStatus(404)
if (req.method == 'DELETE' && !req.user.canDelete) {
Logger.warn(`[SessionController] User attempted to delete without permission`, req.user)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
Logger.warn('[SessionController] User attempted to update without permission', req.user.username)
return res.sendStatus(403)
}
req.session = playbackSession
next()
}
}
module.exports = new SessionController()

View file

@ -43,7 +43,7 @@ class UserController {
account.id = getId('usr')
account.pash = await this.auth.hashPass(account.password)
delete account.password
account.token = await this.auth.generateAccessToken({ userId: account.id })
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
account.createdAt = Date.now()
var newUser = new User(account)
var success = await this.db.insertEntity('user', newUser)
@ -74,12 +74,14 @@ class UserController {
}
var account = req.body
var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) {
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
if (usernameExists) {
return res.status(500).send('Username already taken')
}
shouldUpdateToken = true
}
// Updating password
@ -90,6 +92,10 @@ class UserController {
var hasUpdated = user.update(account)
if (hasUpdated) {
if (shouldUpdateToken) {
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
}
await this.db.updateEntity('user', user)
}