mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 11:21:36 +00:00
Merge branch 'refs/heads/master' into mf/rssInboundManager
# Conflicts: # client/components/widgets/PodcastDetailsEdit.vue # client/pages/config/rss-feeds.vue # client/strings/en-us.json # server/controllers/PodcastController.js # server/models/Podcast.js # server/routers/ApiRouter.js # server/utils/queries/libraryItemsPodcastFilters.js
This commit is contained in:
commit
2d2c6242dd
462 changed files with 33361 additions and 21220 deletions
|
|
@ -1,3 +1,4 @@
|
|||
const { Request, Response, NextFunction } = require('express')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
|
@ -13,8 +14,23 @@ const CoverManager = require('../managers/CoverManager')
|
|||
|
||||
const LibraryItem = require('../objects/LibraryItem')
|
||||
|
||||
class PodcastController {
|
||||
/**
|
||||
* @typedef RequestUserObject
|
||||
* @property {import('../models/User')} user
|
||||
*
|
||||
* @typedef {Request & RequestUserObject} RequestWithUser
|
||||
*/
|
||||
|
||||
class PodcastController {
|
||||
/**
|
||||
* POST /api/podcasts
|
||||
* Create podcast
|
||||
*
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async create(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
|
||||
|
|
@ -22,13 +38,13 @@ class PodcastController {
|
|||
}
|
||||
const payload = req.body
|
||||
|
||||
const library = await Database.libraryModel.getOldById(payload.libraryId)
|
||||
const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
|
||||
const folder = library.folders.find(fold => fold.id === payload.folderId)
|
||||
const folder = library.libraryFolders.find((fold) => fold.id === payload.folderId)
|
||||
if (!folder) {
|
||||
Logger.error(`[PodcastController] Create: Folder not found "${payload.folderId}"`)
|
||||
return res.status(404).send('Folder not found')
|
||||
|
|
@ -37,20 +53,24 @@ class PodcastController {
|
|||
const podcastPath = filePathToPOSIX(payload.path)
|
||||
|
||||
// Check if a library item with this podcast folder exists already
|
||||
const existingLibraryItem = (await Database.libraryItemModel.count({
|
||||
where: {
|
||||
path: podcastPath
|
||||
}
|
||||
})) > 0
|
||||
const existingLibraryItem =
|
||||
(await Database.libraryItemModel.count({
|
||||
where: {
|
||||
path: podcastPath
|
||||
}
|
||||
})) > 0
|
||||
if (existingLibraryItem) {
|
||||
Logger.error(`[PodcastController] Podcast already exists at path "${podcastPath}"`)
|
||||
return res.status(400).send('Podcast already exists')
|
||||
}
|
||||
|
||||
const success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
||||
Logger.error(`[PodcastController] Failed to ensure podcast dir "${podcastPath}"`, error)
|
||||
return false
|
||||
})
|
||||
const success = await fs
|
||||
.ensureDir(podcastPath)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
Logger.error(`[PodcastController] Failed to ensure podcast dir "${podcastPath}"`, error)
|
||||
return false
|
||||
})
|
||||
if (!success) return res.status(400).send('Invalid podcast path')
|
||||
|
||||
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||
|
|
@ -109,8 +129,8 @@ class PodcastController {
|
|||
* @typedef getPodcastFeedReqBody
|
||||
* @property {string} rssFeed
|
||||
*
|
||||
* @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req
|
||||
* @param {import('express').Response} res
|
||||
* @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObject} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getPodcastFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
|
|
@ -168,6 +188,14 @@ class PodcastController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/podcasts/opml
|
||||
*
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getFeedsFromOPMLText(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
|
||||
|
|
@ -178,13 +206,57 @@ class PodcastController {
|
|||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const rssFeedsData = await this.podcastManager.getOPMLFeeds(req.body.opmlText)
|
||||
res.json(rssFeedsData)
|
||||
res.json({
|
||||
feeds: this.podcastManager.getParsedOPMLFileFeeds(req.body.opmlText)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/podcasts/opml/create
|
||||
*
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async bulkCreatePodcastsFromOpmlFeedUrls(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const rssFeeds = req.body.feeds
|
||||
if (!Array.isArray(rssFeeds) || !rssFeeds.length || rssFeeds.some((feed) => !validateUrl(feed))) {
|
||||
return res.status(400).send('Invalid request body. "feeds" must be an array of RSS feed URLs')
|
||||
}
|
||||
|
||||
const libraryId = req.body.libraryId
|
||||
const folderId = req.body.folderId
|
||||
if (!libraryId || !folderId) {
|
||||
return res.status(400).send('Invalid request body. "libraryId" and "folderId" are required')
|
||||
}
|
||||
|
||||
const folder = await Database.libraryFolderModel.findByPk(folderId)
|
||||
if (!folder || folder.libraryId !== libraryId) {
|
||||
return res.status(404).send('Folder not found')
|
||||
}
|
||||
const autoDownloadEpisodes = !!req.body.autoDownloadEpisodes
|
||||
this.podcastManager.createPodcastsFromFeedUrls(rssFeeds, folder, autoDownloadEpisodes, this.cronManager)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/podcasts/:id/checknew
|
||||
*
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async checkNewEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to check/download episodes`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
|
|
@ -202,21 +274,37 @@ class PodcastController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/podcasts/:id/clear-queue
|
||||
*
|
||||
* @this {import('../routers/ApiRouter')}
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
clearEpisodeDownloadQueue(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`)
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempting to clear download queue`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
this.podcastManager.clearDownloadQueue(req.params.id)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/podcasts/:id/downloads
|
||||
*
|
||||
* @this {import('../routers/ApiRouter')}
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
getEpisodeDownloads(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
|
||||
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(libraryItem.id)
|
||||
res.json({
|
||||
downloads: downloadsInQueue.map(d => d.toJSONForClient())
|
||||
downloads: downloadsInQueue.map((d) => d.toJSONForClient())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -227,8 +315,8 @@ class PodcastController {
|
|||
return res.status(500).send('Podcast does not have an RSS feed URL')
|
||||
}
|
||||
|
||||
var searchTitle = req.query.title
|
||||
if (!searchTitle) {
|
||||
const searchTitle = req.query.title
|
||||
if (!searchTitle || typeof searchTitle !== 'string') {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
const episodes = await findMatchingEpisodes(rssFeedUrl, searchTitle)
|
||||
|
|
@ -237,9 +325,17 @@ class PodcastController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/podcasts/:id/download-episodes
|
||||
*
|
||||
* @this {import('../routers/ApiRouter')}
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async downloadEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const libraryItem = req.libraryItem
|
||||
|
|
@ -252,10 +348,17 @@ class PodcastController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/podcasts/:id/match-episodes
|
||||
/**
|
||||
* POST: /api/podcasts/:id/match-episodes
|
||||
*
|
||||
* @this {import('../routers/ApiRouter')}
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async quickMatchEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +374,12 @@ class PodcastController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH: /api/podcasts/:id/episode/:episodeId
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async updateEpisode(req, res) {
|
||||
const libraryItem = req.libraryItem
|
||||
|
||||
|
|
@ -287,12 +396,17 @@ class PodcastController {
|
|||
res.json(libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
// GET: api/podcasts/:id/episode/:episodeId
|
||||
/**
|
||||
* GET: /api/podcasts/:id/episode/:episodeId
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getEpisode(req, res) {
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = req.libraryItem
|
||||
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
|
||||
const episode = libraryItem.media.episodes.find((ep) => ep.id === episodeId)
|
||||
if (!episode) {
|
||||
Logger.error(`[PodcastController] getEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
|
|
@ -301,13 +415,18 @@ class PodcastController {
|
|||
res.json(episode)
|
||||
}
|
||||
|
||||
// DELETE: api/podcasts/:id/episode/:episodeId
|
||||
/**
|
||||
* DELETE: /api/podcasts/:id/episode/:episodeId
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async removeEpisode(req, res) {
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = req.libraryItem
|
||||
const hardDelete = req.query.hard === '1'
|
||||
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
|
||||
const episode = libraryItem.media.episodes.find((ep) => ep.id === episodeId)
|
||||
if (!episode) {
|
||||
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
|
|
@ -316,11 +435,14 @@ class PodcastController {
|
|||
if (hardDelete) {
|
||||
const audioFile = episode.audioFile
|
||||
// TODO: this will trigger the watcher. should maybe handle this gracefully
|
||||
await fs.remove(audioFile.metadata.path).then(() => {
|
||||
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
|
||||
}).catch((error) => {
|
||||
Logger.error(`[PodcastController] Failed to hard delete episode file at "${audioFile.metadata.path}"`, error)
|
||||
})
|
||||
await fs
|
||||
.remove(audioFile.metadata.path)
|
||||
.then(() => {
|
||||
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error(`[PodcastController] Failed to hard delete episode file at "${audioFile.metadata.path}"`, error)
|
||||
})
|
||||
}
|
||||
|
||||
// Remove episode from Podcast and library file
|
||||
|
|
@ -369,6 +491,12 @@ class PodcastController {
|
|||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async middleware(req, res, next) {
|
||||
const item = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!item?.media) return res.sendStatus(404)
|
||||
|
|
@ -383,10 +511,10 @@ class PodcastController {
|
|||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[PodcastController] User attempted to delete without permission`, req.user.username)
|
||||
Logger.warn(`[PodcastController] User "${req.user.username}" attempted to delete without permission`)
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||
Logger.warn('[PodcastController] User attempted to update without permission', req.user.username)
|
||||
Logger.warn(`[PodcastController] User "${req.user.username}" attempted to update without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue