Merge branch 'master' into mf/rssInboundManager

# Conflicts:
#	client/pages/config/rss-feeds.vue
#	client/strings/en-us.json
#	server/managers/PodcastManager.js
#	server/models/Podcast.js
This commit is contained in:
mfcar 2023-12-08 23:41:33 -03:00
commit eb8e49e4fc
No known key found for this signature in database
180 changed files with 14188 additions and 5517 deletions

View file

@ -4,14 +4,13 @@ const fs = require('../libs/fsExtra')
const workerThreads = require('worker_threads')
const Logger = require('../Logger')
const TaskManager = require('./TaskManager')
const Task = require('../objects/Task')
const { writeConcatFile } = require('../utils/ffmpegHelpers')
const toneHelpers = require('../utils/toneHelpers')
class AbMergeManager {
constructor(taskManager) {
this.taskManager = taskManager
constructor() {
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
this.pendingTasks = []
@ -45,7 +44,7 @@ class AbMergeManager {
}
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData)
this.taskManager.addTask(task)
TaskManager.addTask(task)
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
if (!await fs.pathExists(taskData.itemCachePath)) {
@ -234,7 +233,7 @@ class AbMergeManager {
}
}
this.taskManager.taskFinished(task)
TaskManager.taskFinished(task)
}
}
module.exports = AbMergeManager

View file

@ -0,0 +1,54 @@
const { LRUCache } = require('lru-cache')
const Logger = require('../Logger')
const Database = require('../Database')
class ApiCacheManager {
defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => (item.body.length + JSON.stringify(item.headers).length) }
defaultTtlOptions = { ttl: 30 * 60 * 1000 }
constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) {
this.cache = cache
this.ttlOptions = ttlOptions
}
init(database = Database) {
let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy', 'afterUpsert']
hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook)))
}
clear(model, hook) {
Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`)
this.cache.clear()
}
get middleware() {
return (req, res, next) => {
const key = { user: req.user.username, url: req.url }
const stringifiedKey = JSON.stringify(key)
Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`)
const cached = this.cache.get(stringifiedKey)
if (cached) {
Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`)
res.set(cached.headers)
res.status(cached.statusCode)
res.send(cached.body)
return
}
res.originalSend = res.send
res.send = (body) => {
Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`)
const cached = { body, headers: res.getHeaders(), statusCode: res.statusCode }
if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) {
Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`)
this.cache.set(stringifiedKey, cached, this.ttlOptions)
} else {
this.cache.set(stringifiedKey, cached)
}
res.originalSend(body)
}
next()
}
}
}
module.exports = ApiCacheManager

View file

@ -7,12 +7,12 @@ const fs = require('../libs/fsExtra')
const toneHelpers = require('../utils/toneHelpers')
const TaskManager = require('./TaskManager')
const Task = require('../objects/Task')
class AudioMetadataMangaer {
constructor(taskManager) {
this.taskManager = taskManager
constructor() {
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
this.MAX_CONCURRENT_TASKS = 1
@ -101,7 +101,7 @@ class AudioMetadataMangaer {
async runMetadataEmbed(task) {
this.tasksRunning.push(task)
this.taskManager.addTask(task)
TaskManager.addTask(task)
Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description)
@ -176,7 +176,7 @@ class AudioMetadataMangaer {
}
handleTaskFinished(task) {
this.taskManager.taskFinished(task)
TaskManager.taskFinished(task)
this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id)
if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) {

View file

@ -5,7 +5,7 @@ const readChunk = require('../libs/readChunk')
const imageType = require('../libs/imageType')
const globals = require('../utils/globals')
const { downloadFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils')
const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const CacheManager = require('../managers/CacheManager')
@ -120,13 +120,16 @@ class CoverManager {
await fs.ensureDir(coverDirPath)
var temppath = Path.posix.join(coverDirPath, 'cover')
var success = await downloadFile(url, temppath).then(() => true).catch((err) => {
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
let errorMsg = ''
let success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
errorMsg = err.message || 'Unknown error'
Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg)
return false
})
if (!success) {
return {
error: 'Failed to download image from url'
error: 'Failed to download image from url: ' + errorMsg
}
}
@ -284,7 +287,7 @@ class CoverManager {
await fs.ensureDir(coverDirPath)
const temppath = Path.posix.join(coverDirPath, 'cover')
const success = await downloadFile(url, temppath).then(() => true).catch((err) => {
const success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
return false
})

View file

@ -127,8 +127,7 @@ class CronManager {
}
}
async executePodcastCron(expression, libraryItemIds) {
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
async executePodcastCron(expression) {
const podcastCron = this.podcastCrons.find(cron => cron.expression === expression)
if (!podcastCron) {
Logger.error(`[CronManager] Podcast cron not found for expression ${expression}`)
@ -136,6 +135,9 @@ class CronManager {
}
this.podcastCronExpressionsExecuting.push(expression)
const libraryItemIds = podcastCron.libraryItemIds
Logger.debug(`[CronManager] Start executing podcast cron ${expression} for ${libraryItemIds.length} item(s)`)
// Get podcast library items to check
const libraryItems = []
for (const libraryItemId of libraryItemIds) {

View file

@ -330,14 +330,15 @@ class PlaybackSessionManager {
Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`)
}
// Check for streams that are not in memory and remove
/**
* Remove all stream folders in `/metadata/streams`
*/
async removeOrphanStreams() {
await fs.ensureDir(this.StreamsPath)
try {
const streamsInPath = await fs.readdir(this.StreamsPath)
for (let i = 0; i < streamsInPath.length; i++) {
const streamId = streamsInPath[i]
if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
for (const streamId of streamsInPath) {
if (/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(streamId)) { // Ensure is uuidv4
const session = this.sessions.find(se => se.id === streamId)
if (!session) {
const streamPath = Path.join(this.StreamsPath, streamId)

View file

@ -12,17 +12,17 @@ const opmlGenerator = require('../utils/generators/opmlGenerator')
const prober = require('../utils/prober')
const ffmpegHelpers = require('../utils/ffmpegHelpers')
const TaskManager = require('./TaskManager')
const LibraryFile = require('../objects/files/LibraryFile')
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
const AudioFile = require('../objects/files/AudioFile')
const Task = require("../objects/Task")
class PodcastManager {
constructor(watcher, notificationManager, taskManager) {
constructor(watcher, notificationManager) {
this.watcher = watcher
this.notificationManager = notificationManager
this.taskManager = taskManager
this.downloadQueue = []
this.currentDownload = null
@ -69,14 +69,12 @@ class PodcastManager {
return
}
const task = new Task()
const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`
const taskData = {
libraryId: podcastEpisodeDownload.libraryId,
libraryItemId: podcastEpisodeDownload.libraryItemId,
}
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
this.taskManager.addTask(task)
const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
this.currentDownload = podcastEpisodeDownload
@ -128,7 +126,7 @@ class PodcastManager {
this.currentDownload.setFinished(false)
}
this.taskManager.taskFinished(task)
TaskManager.taskFinished(task)
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
@ -201,7 +199,7 @@ class PodcastManager {
})
// TODO: Should we check for open playback sessions for this episode?
// TODO: remove all user progress for this episode
if (oldestEpisode && oldestEpisode.audioFile) {
if (oldestEpisode?.audioFile) {
Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`)
const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path)
if (successfullyDeleted) {
@ -246,7 +244,7 @@ class PodcastManager {
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
let newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`)
if (!newEpisodes) { // Failed
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
@ -285,9 +283,9 @@ class PodcastManager {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
return false
}
let feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
if (!feed || !feed.episodes) {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id}, URL: ${podcastLibraryItem.media.metadata.feedUrl})`, feed)
const feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
if (!feed?.episodes) {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id}, URL: ${podcastLibraryItem.media.metadata.feedUrl})`, feed)
return false
}

View file

@ -1,20 +1,48 @@
const SocketAuthority = require('../SocketAuthority')
const Task = require('../objects/Task')
class TaskManager {
constructor() {
/** @type {Task[]} */
this.tasks = []
}
/**
* Add task and emit socket task_started event
*
* @param {Task} task
*/
addTask(task) {
this.tasks.push(task)
SocketAuthority.emitter('task_started', task.toJSON())
}
/**
* Remove task and emit task_finished event
*
* @param {Task} task
*/
taskFinished(task) {
if (this.tasks.some(t => t.id === task.id)) {
this.tasks = this.tasks.filter(t => t.id !== task.id)
SocketAuthority.emitter('task_finished', task.toJSON())
}
}
/**
* Create new task and add
*
* @param {string} action
* @param {string} title
* @param {string} description
* @param {boolean} showSuccess
* @param {Object} [data]
*/
createAndAddTask(action, title, description, showSuccess, data = {}) {
const task = new Task()
task.setData(action, title, description, showSuccess, data)
this.addTask(task)
return task
}
}
module.exports = TaskManager
module.exports = new TaskManager()