From a8dfbdd17e280044d8495145bf969f7b8d1808fb Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 13 Jan 2026 20:46:46 +0100 Subject: [PATCH] Fix: (#11352) Add media info columns to movie index table Adds new columns to the movie index table view: - Gigabytes Per Hour - Audio Codec - Video Codec - Resolution --- .../src/Movie/Index/Table/MovieIndexRow.css | 23 ++++++++ .../Movie/Index/Table/MovieIndexRow.css.d.ts | 4 ++ .../src/Movie/Index/Table/MovieIndexRow.tsx | 39 +++++++++++++ .../Index/Table/MovieIndexTableHeader.css | 23 ++++++++ .../Table/MovieIndexTableHeader.css.d.ts | 4 ++ .../src/Store/Actions/movieIndexActions.js | 55 +++++++++++++++++++ .../Utilities/Number/parseRuntimeToHours.ts | 26 +++++++++ src/NzbDrone.Core/Localization/Core/en.json | 3 + 8 files changed, 177 insertions(+) create mode 100644 frontend/src/Utilities/Number/parseRuntimeToHours.ts diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css b/frontend/src/Movie/Index/Table/MovieIndexRow.css index 6b8091960c..bcc4eb6d6c 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css @@ -88,9 +88,32 @@ .sizeOnDisk { composes: cell; + justify-content: flex-end; + flex: 0 0 120px; } +.gigabytesPerHour { + composes: cell; + + justify-content: flex-end; + + flex: 0 0 100px; +} + +.audioCodec, +.videoCodec { + composes: cell; + + flex: 0 0 110px; +} + +.resolution { + composes: cell; + + flex: 0 0 100px; +} + .imdbRating, .tmdbRating, .rottenTomatoesRating, diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts b/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts index 441f0219d4..049e414c24 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'actions': string; 'added': string; + 'audioCodec': string; 'cell': string; 'certification': string; 'checkInput': string; @@ -10,6 +11,7 @@ interface CssExports { 'digitalRelease': string; 'externalLinks': string; 'genres': string; + 'gigabytesPerHour': string; 'imdbRating': string; 'inCinemas': string; 'keywords': string; @@ -23,6 +25,7 @@ interface CssExports { 'qualityProfileId': string; 'releaseDate': string; 'releaseGroups': string; + 'resolution': string; 'rottenTomatoesRating': string; 'runtime': string; 'sizeOnDisk': string; @@ -32,6 +35,7 @@ interface CssExports { 'tags': string; 'tmdbRating': string; 'traktRating': string; + 'videoCodec': string; 'year': string; } export const cssExports: CssExports; diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx index d546a8c511..b3d15dccca 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx @@ -28,6 +28,7 @@ import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import { SelectStateInputProps } from 'typings/props'; import formatRuntime from 'Utilities/Date/formatRuntime'; import formatBytes from 'Utilities/Number/formatBytes'; +import parseRuntimeToHours from 'Utilities/Number/parseRuntimeToHours'; import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import MovieIndexProgressBar from '../ProgressBar/MovieIndexProgressBar'; @@ -330,6 +331,44 @@ function MovieIndexRow(props: MovieIndexRowProps) { ); } + if (name === 'gigabytesPerHour') { + const runtimeHours = parseRuntimeToHours( + movieFile?.mediaInfo?.runTime + ); + const gigabytesPerHour = + runtimeHours > 0 ? sizeOnDisk / 1073741824 / runtimeHours : 0; + + return ( + + {gigabytesPerHour.toFixed(2)} + + ); + } + + if (name === 'audioCodec') { + return ( + + {movieFile?.mediaInfo?.audioCodec ?? ''} + + ); + } + + if (name === 'videoCodec') { + return ( + + {movieFile?.mediaInfo?.videoCodec ?? ''} + + ); + } + + if (name === 'resolution') { + return ( + + {movieFile?.mediaInfo?.resolution ?? ''} + + ); + } + if (name === 'genres') { const joinedGenres = genres.join(', '); diff --git a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css index fc237d8c0c..3e0278cc01 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css +++ b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css @@ -78,9 +78,32 @@ .sizeOnDisk { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + justify-content: flex-end; + flex: 0 0 120px; } +.gigabytesPerHour { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + justify-content: flex-end; + + flex: 0 0 100px; +} + +.audioCodec, +.videoCodec { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 110px; +} + +.resolution { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 100px; +} + .imdbRating, .tmdbRating, .rottenTomatoesRating, diff --git a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts index 39890935ac..37cb4410c1 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts +++ b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts @@ -3,10 +3,12 @@ interface CssExports { 'actions': string; 'added': string; + 'audioCodec': string; 'certification': string; 'collection': string; 'digitalRelease': string; 'genres': string; + 'gigabytesPerHour': string; 'imdbRating': string; 'inCinemas': string; 'keywords': string; @@ -20,6 +22,7 @@ interface CssExports { 'qualityProfileId': string; 'releaseDate': string; 'releaseGroups': string; + 'resolution': string; 'rottenTomatoesRating': string; 'runtime': string; 'sizeOnDisk': string; @@ -29,6 +32,7 @@ interface CssExports { 'tags': string; 'tmdbRating': string; 'traktRating': string; + 'videoCodec': string; 'year': string; } export const cssExports: CssExports; diff --git a/frontend/src/Store/Actions/movieIndexActions.js b/frontend/src/Store/Actions/movieIndexActions.js index e36bee132a..c5248229bf 100644 --- a/frontend/src/Store/Actions/movieIndexActions.js +++ b/frontend/src/Store/Actions/movieIndexActions.js @@ -1,6 +1,7 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import sortByProp from 'Utilities/Array/sortByProp'; +import parseRuntimeToHours from 'Utilities/Number/parseRuntimeToHours'; import translate from 'Utilities/String/translate'; import createHandleActions from './Creators/createHandleActions'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; @@ -175,6 +176,30 @@ export const defaultState = { isSortable: true, isVisible: false }, + { + name: 'gigabytesPerHour', + label: () => translate('GigabytesPerHour'), + isSortable: true, + isVisible: false + }, + { + name: 'audioCodec', + label: () => translate('AudioCodec'), + isSortable: true, + isVisible: false + }, + { + name: 'videoCodec', + label: () => translate('VideoCodec'), + isSortable: true, + isVisible: false + }, + { + name: 'resolution', + label: () => translate('Resolution'), + isSortable: true, + isVisible: false + }, { name: 'genres', label: () => translate('Genres'), @@ -295,6 +320,36 @@ export const defaultState = { traktRating: function({ ratings = {} }) { return ratings.trakt ? ratings.trakt.value : 0; + }, + + gigabytesPerHour: function(item) { + const { statistics = {}, movieFile } = item; + const { sizeOnDisk = 0 } = statistics; + const runtimeHours = parseRuntimeToHours(movieFile?.mediaInfo?.runTime); + + if (runtimeHours === 0) { + return 0; + } + + return sizeOnDisk / runtimeHours; + }, + + audioCodec: function(item) { + const { movieFile } = item; + + return movieFile?.mediaInfo?.audioCodec ?? ''; + }, + + videoCodec: function(item) { + const { movieFile } = item; + + return movieFile?.mediaInfo?.videoCodec ?? ''; + }, + + resolution: function(item) { + const { movieFile } = item; + + return movieFile?.mediaInfo?.resolution ?? ''; } }, diff --git a/frontend/src/Utilities/Number/parseRuntimeToHours.ts b/frontend/src/Utilities/Number/parseRuntimeToHours.ts new file mode 100644 index 0000000000..95f88213e4 --- /dev/null +++ b/frontend/src/Utilities/Number/parseRuntimeToHours.ts @@ -0,0 +1,26 @@ +// Parses a runtime string in "H:MM:SS" or "M:SS" format to decimal hours +function parseRuntimeToHours(runTime: string | undefined): number { + if (!runTime) { + return 0; + } + + const parts = runTime.split(':').map(Number); + + if (parts.some(isNaN)) { + return 0; + } + + if (parts.length === 3) { + // H:MM:SS format + return parts[0] + parts[1] / 60 + parts[2] / 3600; + } + + if (parts.length === 2) { + // M:SS format + return parts[0] / 60 + parts[1] / 3600; + } + + return 0; +} + +export default parseRuntimeToHours; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 23a3b71ab8..8b205580ea 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -95,6 +95,7 @@ "ApplyTagsHelpTextRemove": "Remove: Remove the entered tags", "ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", "AptUpdater": "Use apt to install the update", + "AudioCodec": "Audio Codec", "AudioInfo": "Audio Info", "AudioLanguages": "Audio Languages", "AuthBasic": "Basic (Browser Popup)", @@ -725,6 +726,7 @@ "From": "from", "FullColorEvents": "Full Color Events", "FullColorEventsHelpText": "Altered style to color the entire event with the status color, instead of just the left edge. Does not apply to Agenda", + "GigabytesPerHour": "Gigabytes Per Hour", "General": "General", "GeneralSettings": "General Settings", "GeneralSettingsLoadError": "Unable to load General settings", @@ -1683,6 +1685,7 @@ "ResetQualityDefinitions": "Reset Quality Definitions", "ResetQualityDefinitionsMessageText": "Are you sure you want to reset quality definitions?", "ResetTitles": "Reset Titles", + "Resolution": "Resolution", "Restart": "Restart", "RestartLater": "I'll restart later", "RestartNow": "Restart Now",