diff --git a/artifacts/2026-02-15/badge.md b/artifacts/2026-02-15/badge.md new file mode 100644 index 000000000..c130c785b --- /dev/null +++ b/artifacts/2026-02-15/badge.md @@ -0,0 +1,20 @@ +# Cover Size Badge Specification + +## Overview +A new badge is displayed on book covers to indicate the resolution of the cover image. This helps users identifying high-quality covers (e.g., Audible's 2400x2400 format). + +## Logic +The badge is determined client-side once the image has loaded, using the `naturalWidth` and `naturalHeight` properties of the `HTMLImageElement`. + +### Size Tiers +| Tier | Criteria | Label | Color | +| :--- | :--- | :--- | :--- | +| **Big** | Width or Height >= 2400px | BIG | Success (Green) | +| **Medium** | Width or Height >= 1200px | MED | Info (Blue) | +| **Small** | Width or Height < 1200px | SML | Warning (Yellow) | + +## Implementation Details +- **Component**: `BookCover.vue` (and other cover components as needed). +- **Detection**: `imageLoaded` event captures dimensions. +- **UI**: Absolute positioned badge in the bottom-right corner. +- **Responsiveness**: Font size and padding scale with the `sizeMultiplier` of the cover component. diff --git a/artifacts/2026-02-17/badge.md b/artifacts/2026-02-17/badge.md new file mode 100644 index 000000000..cc2a632c7 --- /dev/null +++ b/artifacts/2026-02-17/badge.md @@ -0,0 +1,37 @@ +# Cover Size Badge Specification + +## Overview +Indicates the size tier of a book cover image directly on the cover in various views. This helps users quickly identify high-quality (Audible-grade) covers vs. lower resolution ones. + +## Size Tiers +The badge uses the following logic based on the image's natural dimensions (Width or Height): + +| Tier | Condition | Text | Color | +| :--- | :--- | :--- | :--- | +| **BIG** | Width or Height >= 1200px | BIG | Green (`bg-success`) | +| **MED** | Width or Height >= 450px | MED | Blue (`bg-info`) | +| **SML** | Width or Height < 450px | SML | Yellow (`bg-warning`) | + +## Implementation Details +The detection is performed server-side and stored in the database to ensure accuracy regardless of thumbnail sizes. + +### Dimension Detection +- `coverWidth` and `coverHeight` columns added to `books` and `podcasts` tables. +- A `beforeSave` hook on `Book` and `Podcast` models detects dimensions using `ffprobe` when `coverPath` changes. +- A database migration (`v2.32.7-add-cover-dimensions.js`) populates existing items. + +### Components Impacted +1. **`BookCover.vue`**: Used in detail views and some table rows (e.g., Collections). +2. **`LazyBookCard.vue`**: Used in main library bookshelf views, home page shelves, and search results. + +### Logic +- USE `media.coverWidth` and `media.coverHeight` (from the server) as the primary source. +- FALLBACK to `naturalWidth` and `naturalHeight` from the image's `@load` event if server data is unavailable. +- COMPUTE the badge tier based on the rules above. +- RENDER a small, absolute-positioned badge in the bottom-right corner of the cover. + +### UI Styling +- **Position**: Bottom-right of the cover image. +- **Font Size**: Scales with the `sizeMultiplier` (default `0.6rem`). +- **Pointer Events**: `none` (to avoid interfering with clicks). +- **Z-Index**: `20` (to stay above the cover and some overlays). diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 7c40cbac2..54747d07c 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -122,6 +122,11 @@ folder_open + + +
+ {{ coverBadge.text }} +
@@ -176,7 +181,9 @@ export default { isSelectionMode: false, displayTitleTruncated: false, displaySubtitleTruncated: false, - showCoverBg: false + showCoverBg: false, + naturalWidth: 0, + naturalHeight: 0 } }, watch: { @@ -250,6 +257,18 @@ export default { if (this.collapsedSeries?.name) return this.collapsedSeries.name return this.series?.name || null }, + coverBadge() { + const width = this.media?.coverWidth || this.coverWidth + const height = this.media?.coverHeight || this.coverHeight + if (!width || !height) return null + if (width >= 1200 || height >= 1200) { + return { text: 'BIG', color: 'bg-success' } + } + if (width >= 450 || height >= 450) { + return { text: 'MED', color: 'bg-info' } + } + return { text: 'SML', color: 'bg-warning' } + }, seriesSequence() { return this.series?.sequence || null }, @@ -1148,6 +1167,8 @@ export default { if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) { var { naturalWidth, naturalHeight } = this.$refs.cover + this.naturalWidth = naturalWidth + this.naturalHeight = naturalHeight var aspectRatio = naturalHeight / naturalWidth var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio) diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue index e55d38c17..8495862b3 100644 --- a/client/components/covers/BookCover.vue +++ b/client/components/covers/BookCover.vue @@ -22,6 +22,11 @@ + +
+ {{ coverBadge.text }} +
+

{{ titleCleaned }}

@@ -52,7 +57,9 @@ export default { loading: true, imageFailed: false, showCoverBg: false, - imageReady: false + imageReady: false, + naturalWidth: 0, + naturalHeight: 0 } }, watch: { @@ -133,6 +140,18 @@ export default { }, resolution() { return `${this.naturalWidth}x${this.naturalHeight}px` + }, + coverBadge() { + const width = this.media?.coverWidth || this.naturalWidth + const height = this.media?.coverHeight || this.naturalHeight + if (!width || !height) return null + if (width >= 1200 || height >= 1200) { + return { text: 'BIG', color: 'bg-success' } + } + if (width >= 450 || height >= 450) { + return { text: 'MED', color: 'bg-info' } + } + return { text: 'SML', color: 'bg-warning' } } }, methods: { @@ -154,6 +173,8 @@ export default { if (this.$refs.cover && this.cover !== this.placeholderUrl) { var { naturalWidth, naturalHeight } = this.$refs.cover + this.naturalWidth = naturalWidth + this.naturalHeight = naturalHeight var aspectRatio = naturalHeight / naturalWidth var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio) diff --git a/client/components/modals/libraries/LibraryTools.vue b/client/components/modals/libraries/LibraryTools.vue index 44875b663..16d89625a 100644 --- a/client/components/modals/libraries/LibraryTools.vue +++ b/client/components/modals/libraries/LibraryTools.vue @@ -49,6 +49,18 @@
+
+
+
+

{{ $strings.LabelUpdateCoverDimensions }}

+

{{ $strings.LabelUpdateCoverDimensionsHelp }}

+
+
+
+ {{ $strings.ButtonUpdate }} +
+
+
@@ -198,6 +210,34 @@ export default { .finally(() => { this.$emit('update:processing', false) }) + }, + updateCoverDimensionsClick() { + const payload = { + message: this.$strings.MessageConfirmUpdateCoverDimensions, + persistent: true, + callback: (confirmed) => { + if (confirmed) { + this.updateCoverDimensions() + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, + updateCoverDimensions() { + this.$emit('update:processing', true) + this.$axios + .$post(`/api/libraries/${this.libraryId}/update-cover-dimensions`) + .then((data) => { + this.$toast.success(this.$getString('ToastUpdateCoverDimensionsSuccess', [data.updated])) + }) + .catch((error) => { + console.error('Failed to update cover dimensions', error) + this.$toast.error(this.$strings.ToastUpdateCoverDimensionsFailed) + }) + .finally(() => { + this.$emit('update:processing', false) + }) } }, mounted() {} diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 6d79693a5..25e3a822a 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -289,6 +289,8 @@ "LabelCleanupAuthorsHelp": "Remove authors that have no books in this library.", "LabelUpdateConsolidationStatus": "Update Consolidation Status", "LabelUpdateConsolidationStatusHelp": "Checks all items in this library and updates their consolidation status. This is useful if you have manually moved folders on disk.", + "LabelUpdateCoverDimensions": "Update Cover Dimensions", + "LabelUpdateCoverDimensionsHelp": "Detect and update cover width and height for all items in this library.", "LabelClickForMoreInfo": "Click for more info", "LabelClickToUseCurrentValue": "Click to use current value", "LabelClosePlayer": "Close player", @@ -810,6 +812,7 @@ "MessageConfirmRemoveItemsWithIssues": "Are you sure you want to remove all items with issues?", "MessageConfirmCleanupAuthors": "Are you sure you want to remove all authors with no books in this library?", "MessageConfirmUpdateConsolidationStatus": "Are you sure you want to update the consolidation status for all items in this library? This will re-calculate the 'Not Consolidated' badge for every book.", + "MessageConfirmUpdateCoverDimensions": "Are you sure you want to update cover dimensions for all items in this library? This will use ffprobe to detect dimensions for each cover.", "MessageConfirmRemoveEpisodeNote": "Note: This does not delete the audio file unless toggling \"Hard delete file\"", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", @@ -1194,6 +1197,8 @@ "ToastUserRootRequireName": "Must enter a root username", "ToastUpdateConsolidationStatusSuccess": "Successfully updated consolidation status for {0} items.", "ToastUpdateConsolidationStatusFailed": "Failed to update consolidation status.", + "ToastUpdateCoverDimensionsSuccess": "Successfully updated cover dimensions for {0} items.", + "ToastUpdateCoverDimensionsFailed": "Failed to update cover dimensions.", "TooltipAddChapters": "Add chapter(s)", "TooltipAddOneSecond": "Add 1 second", "TooltipAdjustChapterStart": "Click to adjust start time", diff --git a/package.json b/package.json index 9eff93943..fc0243c42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "audiobookshelf", - "version": "2.32.7", - "buildNumber": 1, + "version": "2.32.8", + "buildNumber": 2, "description": "Self-hosted audiobook and podcast server", "main": "index.js", "scripts": { diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index fd49cdebf..0179fe548 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -1407,6 +1407,39 @@ class LibraryController { }) } + /** + * POST: /api/libraries/:id/update-cover-dimensions + * Recompute cover dimensions for all items in library + * + * @param {LibraryControllerRequest} req + * @param {Response} res + */ + async updateCoverDimensions(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to update cover dimensions`) + return res.sendStatus(403) + } + + const items = await Database.libraryItemModel.findAllExpandedWhere({ + libraryId: req.library.id + }) + + let updatedCount = 0 + for (const item of items) { + if (item.media?.coverPath) { + // Force coverPath to be seen as changed to trigger beforeSave hook + item.media.changed('coverPath', true) + await item.media.save() + updatedCount++ + } + } + + Logger.info(`[LibraryController] Updated cover dimensions for ${updatedCount} items in library "${req.library.name}"`) + res.json({ + updated: updatedCount + }) + } + /** * GET: /api/libraries/:id/podcast-titles * diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index e2e197a89..ecbbe31c8 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -601,6 +601,8 @@ class LibraryItemController { } req.libraryItem.media.coverPath = result.cover + req.libraryItem.media.coverWidth = result.width + req.libraryItem.media.coverHeight = result.height req.libraryItem.media.changed('coverPath', true) await req.libraryItem.media.save() @@ -634,6 +636,8 @@ class LibraryItemController { } if (validationResult.updated) { req.libraryItem.media.coverPath = validationResult.cover + req.libraryItem.media.coverWidth = validationResult.width + req.libraryItem.media.coverHeight = validationResult.height req.libraryItem.media.changed('coverPath', true) await req.libraryItem.media.save() diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index c8f889108..4d5662608 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -6,7 +6,7 @@ const imageType = require('../libs/imageType') const globals = require('../utils/globals') const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils') -const { extractCoverArt } = require('../utils/ffmpegHelpers') +const { extractCoverArt, getImageDimensions } = require('../utils/ffmpegHelpers') const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const CacheManager = require('../managers/CacheManager') @@ -115,11 +115,14 @@ class CoverManager { await this.removeOldCovers(coverDirPath, extname) await CacheManager.purgeCoverCache(libraryItem.id) + const dims = await getImageDimensions(coverFullPath) Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.title}"`) return { - cover: coverFullPath + cover: coverFullPath, + width: dims?.width || null, + height: dims?.height || null } } @@ -197,10 +200,13 @@ class CoverManager { } await CacheManager.purgeCoverCache(libraryItem.id) + const dims = await getImageDimensions(coverPath) return { cover: coverPath, - updated: true + updated: true, + width: dims?.width || null, + height: dims?.height || null } } @@ -321,10 +327,13 @@ class CoverManager { await this.removeOldCovers(coverDirPath, '.' + imgtype.ext) await CacheManager.purgeCoverCache(libraryItemId) + const dims = await getImageDimensions(coverFullPath) Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}"`) return { - cover: coverFullPath + cover: coverFullPath, + width: dims?.width || null, + height: dims?.height || null } } catch (error) { Logger.error(`[CoverManager] Fetch cover image from url "${url}" failed`, error) diff --git a/server/migrations/v2.32.8-add-cover-dimensions.js b/server/migrations/v2.32.8-add-cover-dimensions.js new file mode 100644 index 000000000..85b3cba91 --- /dev/null +++ b/server/migrations/v2.32.8-add-cover-dimensions.js @@ -0,0 +1,89 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a sequelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +const migrationVersion = '2.32.7' +const migrationName = `${migrationVersion}-add-cover-dimensions` +const loggerPrefix = `[${migrationVersion} migration]` + +/** + * Adds coverWidth and coverHeight columns to books and podcasts tables. + * + * @param {MigrationOptions} options + */ +async function up({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + const bookTable = await queryInterface.describeTable('books') + if (!bookTable['coverWidth']) { + await queryInterface.addColumn('books', 'coverWidth', { + type: queryInterface.sequelize.Sequelize.INTEGER + }) + await queryInterface.addColumn('books', 'coverHeight', { + type: queryInterface.sequelize.Sequelize.INTEGER + }) + logger.info(`${loggerPrefix} added cover dimensions columns to table "books"`) + } + + const podcastTable = await queryInterface.describeTable('podcasts') + if (!podcastTable['coverWidth']) { + await queryInterface.addColumn('podcasts', 'coverWidth', { + type: queryInterface.sequelize.Sequelize.INTEGER + }) + await queryInterface.addColumn('podcasts', 'coverHeight', { + type: queryInterface.sequelize.Sequelize.INTEGER + }) + logger.info(`${loggerPrefix} added cover dimensions columns to table "podcasts"`) + } + + // Populate dimensions for existing items + const { getImageDimensions } = require('../utils/ffmpegHelpers') + + const books = await queryInterface.sequelize.query('SELECT id, coverPath FROM books WHERE coverPath IS NOT NULL', { + type: queryInterface.sequelize.QueryTypes.SELECT + }) + logger.info(`${loggerPrefix} Populating cover dimensions for ${books.length} books...`) + for (const book of books) { + const dims = await getImageDimensions(book.coverPath) + if (dims) { + await queryInterface.sequelize.query('UPDATE books SET coverWidth = ?, coverHeight = ? WHERE id = ?', { + replacements: [dims.width, dims.height, book.id], + type: queryInterface.sequelize.QueryTypes.UPDATE + }) + } + } + + const podcasts = await queryInterface.sequelize.query('SELECT id, coverPath FROM podcasts WHERE coverPath IS NOT NULL', { + type: queryInterface.sequelize.QueryTypes.SELECT + }) + logger.info(`${loggerPrefix} Populating cover dimensions for ${podcasts.length} podcasts...`) + for (const podcast of podcasts) { + const dims = await getImageDimensions(podcast.coverPath) + if (dims) { + await queryInterface.sequelize.query('UPDATE podcasts SET coverWidth = ?, coverHeight = ? WHERE id = ?', { + replacements: [dims.width, dims.height, podcast.id], + type: queryInterface.sequelize.QueryTypes.UPDATE + }) + } + } + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +async function down({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + await queryInterface.removeColumn('books', 'coverWidth') + await queryInterface.removeColumn('books', 'coverHeight') + await queryInterface.removeColumn('podcasts', 'coverWidth') + await queryInterface.removeColumn('podcasts', 'coverHeight') + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +module.exports = { up, down } diff --git a/server/models/Book.js b/server/models/Book.js index 96371f3a2..c1bbb14a5 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -164,7 +164,9 @@ class Book extends Model { ebookFile: DataTypes.JSON, chapters: DataTypes.JSON, tags: DataTypes.JSON, - genres: DataTypes.JSON + genres: DataTypes.JSON, + coverWidth: DataTypes.INTEGER, + coverHeight: DataTypes.INTEGER }, { sequelize, @@ -201,6 +203,20 @@ class Book extends Model { Book.addHook('afterCreate', async (instance) => { libraryItemsBookFilters.clearCountCache('afterCreate') }) + + Book.addHook('beforeSave', async (instance) => { + if (instance.changed('coverPath') && instance.coverPath) { + const { getImageDimensions } = require('../utils/ffmpegHelpers') + const dims = await getImageDimensions(instance.coverPath) + if (dims) { + instance.coverWidth = dims.width + instance.coverHeight = dims.height + } else { + instance.coverWidth = null + instance.coverHeight = null + } + } + }) } /** @@ -629,6 +645,8 @@ class Book extends Model { libraryItemId: libraryItemId, metadata: this.oldMetadataToJSON(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], audioFiles: structuredClone(this.audioFiles), chapters: structuredClone(this.chapters), @@ -648,6 +666,8 @@ class Book extends Model { id: this.id, metadata: this.oldMetadataToJSONMinified(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], numTracks: this.includedAudioFiles.length, numAudioFiles: this.audioFiles?.length || 0, @@ -674,6 +694,8 @@ class Book extends Model { libraryItemId: libraryItemId, metadata: this.oldMetadataToJSONExpanded(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], audioFiles: structuredClone(this.audioFiles), chapters: structuredClone(this.chapters), diff --git a/server/models/Podcast.js b/server/models/Podcast.js index a96e1dd02..394df0bef 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -150,7 +150,9 @@ class Podcast extends Model { coverPath: DataTypes.STRING, tags: DataTypes.JSON, genres: DataTypes.JSON, - numEpisodes: DataTypes.INTEGER + numEpisodes: DataTypes.INTEGER, + coverWidth: DataTypes.INTEGER, + coverHeight: DataTypes.INTEGER }, { sequelize, @@ -165,6 +167,20 @@ class Podcast extends Model { Podcast.addHook('afterCreate', async (instance) => { libraryItemsPodcastFilters.clearCountCache('podcast', 'afterCreate') }) + + Podcast.addHook('beforeSave', async (instance) => { + if (instance.changed('coverPath') && instance.coverPath) { + const { getImageDimensions } = require('../utils/ffmpegHelpers') + const dims = await getImageDimensions(instance.coverPath) + if (dims) { + instance.coverWidth = dims.width + instance.coverHeight = dims.height + } else { + instance.coverWidth = null + instance.coverHeight = null + } + } + }) } get hasMediaFiles() { @@ -436,6 +452,8 @@ class Podcast extends Model { libraryItemId: libraryItemId, metadata: this.oldMetadataToJSON(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], episodes: this.podcastEpisodes.map((episode) => episode.toOldJSON(libraryItemId)), autoDownloadEpisodes: this.autoDownloadEpisodes, @@ -452,6 +470,8 @@ class Podcast extends Model { // Minified metadata and expanded metadata are the same metadata: this.oldMetadataToJSONExpanded(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], numEpisodes: this.podcastEpisodes?.length || 0, autoDownloadEpisodes: this.autoDownloadEpisodes, @@ -476,6 +496,8 @@ class Podcast extends Model { libraryItemId: libraryItemId, metadata: this.oldMetadataToJSONExpanded(), coverPath: this.coverPath, + coverWidth: this.coverWidth, + coverHeight: this.coverHeight, tags: [...(this.tags || [])], episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)), autoDownloadEpisodes: this.autoDownloadEpisodes, diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a223de9b2..a46dbed89 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -95,6 +95,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.post('/libraries/:id/update-consolidation', LibraryController.middleware.bind(this), LibraryController.updateConsolidationStatus.bind(this)) + this.router.post('/libraries/:id/update-cover-dimensions', LibraryController.middleware.bind(this), LibraryController.updateCoverDimensions.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)) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index ae36dcf88..63350e0f3 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -19,6 +19,7 @@ const LibraryFile = require('../objects/files/LibraryFile') const RssFeedManager = require('../managers/RssFeedManager') const CoverManager = require('../managers/CoverManager') +const { getImageDimensions } = require('../utils/ffmpegHelpers') const LibraryScan = require('./LibraryScan') const OpfFileScanner = require('./OpfFileScanner') diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 4dcbbcda7..3c16e1f8e 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -96,6 +96,24 @@ async function resizeImage(filePath, outputPath, width, height) { } module.exports.resizeImage = resizeImage +async function getImageDimensions(filePath) { + return new Promise((resolve) => { + Ffmpeg.ffprobe(filePath, (err, metadata) => { + if (err) { + Logger.error(`[FfmpegHelpers] ffprobe Error ${err}`) + return resolve(null) + } + const stream = metadata?.streams?.find((s) => s.codec_type === 'video') + if (stream) { + resolve({ width: stream.width, height: stream.height }) + } else { + resolve(null) + } + }) + }) +} +module.exports.getImageDimensions = getImageDimensions + /** * * @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload