From 85d32a160609e63a40e26d5021d1c39437773a3f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 29 Dec 2024 16:25:27 -0600 Subject: [PATCH] Update Share download endpoint & share modal UI --- client/components/modals/ShareModal.vue | 11 ++-- server/controllers/ShareController.js | 88 +++++++++++-------------- server/models/MediaItemShare.js | 28 ++++++++ 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index 2009c41d61..5b37988476 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -19,12 +19,13 @@
-

{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}

+

{{ $strings.LabelDownloadable }}

+

{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}

{{ $strings.LabelPermanent }}

diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 129d7da505..93c6e9fbcf 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -212,7 +212,7 @@ class ShareController { } /** - * Public route + * Public route - requires share_session_id cookie * * GET: /api/share/:slug/download * Downloads media item share @@ -221,62 +221,52 @@ class ShareController { * @param {Response} res */ async downloadMediaItemShare(req, res) { + if (!req.cookies.share_session_id) { + return res.status(404).send('Share session not set') + } + const { slug } = req.params + const mediaItemShare = ShareManager.findBySlug(slug) + if (!mediaItemShare) { + return res.status(404) + } + if (!mediaItemShare.isDownloadable) { + return res.status(403).send('Download is not allowed for this item') + } - // Find matching MediaItemShare based on slug - const mediaItemShare = await ShareManager.findBySlug(slug) - // If the file isDownloadable, download the file - if (mediaItemShare.isDownloadable) { - // Get mediaItemId and type - const { mediaItemId, mediaItemType } = mediaItemShare + const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id) + if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) { + return res.status(404).send('Share session not found') + } - // Get the library item from the mediaItemId - const libraryItem = await Database.libraryItemModel.findOne({ - where: { - mediaId: mediaItemId - } - }) + const libraryItem = await Database.libraryItemModel.findByPk(playbackSession.libraryItemId, { + attributes: ['id', 'path', 'relPath', 'isFile'] + }) + if (!libraryItem) { + return res.status(404).send('Library item not found') + } - const itemPath = libraryItem.path - const itemRelPath = libraryItem.relPath - let itemTitle - - // Get the title based on the mediaItemType - if (mediaItemType === 'podcastEpisode') { - const podcastEpisode = await Database.podcastEpisodeModel.findOne({ - where: { - id: mediaItemId - } - }) - itemTitle = podcastEpisode.title - } else if (mediaItemType === 'book') { - const book = await Database.bookModel.findOne({ - where: { - id: mediaItemId - } - }) - itemTitle = book.title - } + const itemPath = libraryItem.path + const itemTitle = playbackSession.displayTitle - Logger.info(`[ShareController] Requested download for book "${itemTitle}" at "${itemPath}"`) + Logger.info(`[ShareController] Requested download for book "${itemTitle}" at "${itemPath}"`) - try { - if (libraryItem.isFile) { - const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(itemPath)) - if (audioMimeType) { - res.setHeader('Content-Type', audioMimeType) - } - await new Promise((resolve, reject) => res.download(itemPath, itemRelPath, (error) => (error ? reject(error) : resolve()))) - } else { - const filename = `${itemTitle}.zip` - await zipHelpers.zipDirectoryPipe(itemPath, filename, res) + try { + if (libraryItem.isFile) { + const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(itemPath)) + if (audioMimeType) { + res.setHeader('Content-Type', audioMimeType) } - - Logger.info(`[ShareController] Downloaded item "${itemTitle}" at "${itemPath}"`) - } catch (error) { - Logger.error(`[ShareController] Download failed for item "${itemTitle}" at "${itemPath}"`, error) - res.status(500).send('Failed to download the item') + await new Promise((resolve, reject) => res.download(itemPath, libraryItem.relPath, (error) => (error ? reject(error) : resolve()))) + } else { + const filename = `${itemTitle}.zip` + await zipHelpers.zipDirectoryPipe(itemPath, filename, res) } + + Logger.info(`[ShareController] Downloaded item "${itemTitle}" at "${itemPath}"`) + } catch (error) { + Logger.error(`[ShareController] Download failed for item "${itemTitle}" at "${itemPath}"`, error) + res.status(500).send('Failed to download the item') } } diff --git a/server/models/MediaItemShare.js b/server/models/MediaItemShare.js index 065dff10cf..2d7b3896a7 100644 --- a/server/models/MediaItemShare.js +++ b/server/models/MediaItemShare.js @@ -32,6 +32,34 @@ const { DataTypes, Model } = require('sequelize') class MediaItemShare extends Model { constructor(values, options) { super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.mediaItemId + /** @type {string} */ + this.mediaItemType + /** @type {string} */ + this.slug + /** @type {string} */ + this.pash + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.expiresAt + /** @type {Object} */ + this.extraData + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + /** @type {boolean} */ + this.isDownloadable + + // Expanded properties + + /** @type {import('./Book')|import('./PodcastEpisode')} */ + this.mediaItem } toJSONForClient() {