mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-31 12:15:49 -04:00
download multiple items
This commit is contained in:
parent
7f8de7915c
commit
0123dacb29
@ -23,6 +23,7 @@ const RssFeedManager = require('../managers/RssFeedManager')
|
||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||
const authorFilters = require('../utils/queries/authorFilters')
|
||||
const zipHelpers = require('../utils/zipHelpers')
|
||||
|
||||
/**
|
||||
* @typedef RequestUserObject
|
||||
@ -1419,6 +1420,51 @@ class LibraryController {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/library/:id/download
|
||||
* Downloads multiple library items
|
||||
*
|
||||
* @param {LibraryItemControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async downloadMultiple(req, res) {
|
||||
if (!req.user.canDownload) {
|
||||
Logger.warn(`User "${req.user.username}" attempted to download without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if(req.query.ids === undefined) {
|
||||
res.status(400).send('Library not found')
|
||||
}
|
||||
|
||||
const itemIds = req.query.ids.split(',')
|
||||
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
attributes: ['id', 'libraryId', 'path'],
|
||||
where: {
|
||||
id: itemIds,
|
||||
libraryId: req.params.id,
|
||||
mediaType: 'book'
|
||||
}
|
||||
})
|
||||
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for items "${itemIds}"`)
|
||||
|
||||
const filename = `LibraryItems-${Date.now()}.zip`
|
||||
const libraryItemPaths = libraryItems.map((li) => li.path)
|
||||
|
||||
console.log(libraryItemPaths)
|
||||
|
||||
try {
|
||||
await zipHelpers.zipDirectoriesPipe(libraryItemPaths, filename, res)
|
||||
Logger.info(`[LibraryItemController] Downloaded item "${filename}" at "${libraryItemPaths}"`)
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] Download failed for item "${filename}" at "${libraryItemPaths}"`, error)
|
||||
//LibraryItemController.handleDownloadError(error, res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
|
@ -94,6 +94,7 @@ class ApiRouter {
|
||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||
this.router.post('/libraries/:id/remove-metadata', LibraryController.middleware.bind(this), LibraryController.removeAllMetadataFiles.bind(this))
|
||||
this.router.get('/libraries/:id/podcast-titles', LibraryController.middleware.bind(this), LibraryController.getPodcastTitles.bind(this))
|
||||
this.router.get('/libraries/:id/download', LibraryController.middleware.bind(this), LibraryController.downloadMultiple.bind(this))
|
||||
|
||||
//
|
||||
// Item Routes
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const archiver = require('../libs/archiver')
|
||||
const { lstatSync } = require('node:fs')
|
||||
|
||||
module.exports.zipDirectoryPipe = (path, filename, res) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -50,3 +51,75 @@ module.exports.zipDirectoryPipe = (path, filename, res) => {
|
||||
archive.finalize()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a zip archive containing multiple directories and streams it to the response.
|
||||
*
|
||||
* @param {string[]} paths - Array of directory paths to include in the zip archive.
|
||||
* @param {string} filename - Name of the zip file to be sent as attachment.
|
||||
* @param {object} res - Response object to pipe the archive data to.
|
||||
* @returns {Promise<void>} - Promise that resolves when the zip operation completes.
|
||||
*/
|
||||
module.exports.zipDirectoriesPipe = (paths, filename, res) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// create a file to stream archive data to
|
||||
res.attachment(filename)
|
||||
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 0 } // Sets the compression level.
|
||||
})
|
||||
|
||||
// listen for all archive data to be written
|
||||
// 'close' event is fired only when a file descriptor is involved
|
||||
res.on('close', () => {
|
||||
Logger.info(archive.pointer() + ' total bytes')
|
||||
Logger.debug('archiver has been finalized and the output file descriptor has closed.')
|
||||
resolve()
|
||||
})
|
||||
|
||||
// This event is fired when the data source is drained no matter what was the data source.
|
||||
// It is not part of this library but rather from the NodeJS Stream API.
|
||||
// @see: https://nodejs.org/api/stream.html#stream_event_end
|
||||
res.on('end', () => {
|
||||
Logger.debug('Data has been drained')
|
||||
})
|
||||
|
||||
// good practice to catch warnings (ie stat failures and other non-blocking errors)
|
||||
archive.on('warning', function (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// log warning
|
||||
Logger.warn(`[DownloadManager] Archiver warning: ${err.message}`)
|
||||
} else {
|
||||
// throw error
|
||||
Logger.error(`[DownloadManager] Archiver error: ${err.message}`)
|
||||
// throw err
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
archive.on('error', function (err) {
|
||||
Logger.error(`[DownloadManager] Archiver error: ${err.message}`)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
// pipe archive data to the file
|
||||
archive.pipe(res)
|
||||
|
||||
// Add each path as a directory in the zip
|
||||
paths.forEach(path => {
|
||||
|
||||
const paths = path.split('/')
|
||||
|
||||
// Check if path is file or directory
|
||||
if (lstatSync(path).isDirectory()) {
|
||||
const dirName = path.split('/').pop()
|
||||
|
||||
// Add the directory to the archive with its name as the root folder
|
||||
archive.directory(path, dirName);
|
||||
} else {
|
||||
archive.file(path, { name: paths[paths.length - 1] });
|
||||
}
|
||||
});
|
||||
|
||||
archive.finalize()
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user