From 4e4bf3507f20c0f8581c66804f8ef406c41952d8 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 7 Dec 2024 19:04:18 -0800 Subject: [PATCH] Convert MediaInfo to TypeScript --- .../src/Episode/Summary/EpisodeFileRow.tsx | 25 +++-- frontend/src/Episode/Summary/MediaInfo.js | 33 ------ frontend/src/Episode/Summary/MediaInfo.tsx | 27 +++++ .../EpisodeFileLanguageConnector.js | 17 --- .../src/EpisodeFile/EpisodeFileLanguages.tsx | 15 +++ frontend/src/EpisodeFile/MediaInfo.js | 105 ------------------ frontend/src/EpisodeFile/MediaInfo.tsx | 92 +++++++++++++++ .../src/EpisodeFile/MediaInfoConnector.js | 21 ---- frontend/src/Series/Details/EpisodeRow.js | 16 +-- frontend/src/Utilities/Object/getEntries.ts | 9 ++ .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 4 +- 11 files changed, 166 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/Episode/Summary/MediaInfo.js create mode 100644 frontend/src/Episode/Summary/MediaInfo.tsx delete mode 100644 frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js create mode 100644 frontend/src/EpisodeFile/EpisodeFileLanguages.tsx delete mode 100644 frontend/src/EpisodeFile/MediaInfo.js create mode 100644 frontend/src/EpisodeFile/MediaInfo.tsx delete mode 100644 frontend/src/EpisodeFile/MediaInfoConnector.js create mode 100644 frontend/src/Utilities/Object/getEntries.ts diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.tsx b/frontend/src/Episode/Summary/EpisodeFileRow.tsx index a6b084f78..d2bf5f4ba 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.tsx +++ b/frontend/src/Episode/Summary/EpisodeFileRow.tsx @@ -9,26 +9,27 @@ import Popover from 'Components/Tooltip/Popover'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import Language from 'Language/Language'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; import formatBytes from 'Utilities/Number/formatBytes'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; import MediaInfo from './MediaInfo'; import styles from './EpisodeFileRow.css'; -interface EpisodeFileRowProps { - path: string; - size: number; - languages: Language[]; - quality: QualityModel; - qualityCutoffNotMet: boolean; - customFormats: CustomFormat[]; - customFormatScore: number; - mediaInfo: object; +interface EpisodeFileRowProps + extends Pick< + EpisodeFile, + | 'path' + | 'size' + | 'languages' + | 'quality' + | 'customFormats' + | 'customFormatScore' + | 'qualityCutoffNotMet' + | 'mediaInfo' + > { columns: Column[]; onDeleteEpisodeFile(): void; } diff --git a/frontend/src/Episode/Summary/MediaInfo.js b/frontend/src/Episode/Summary/MediaInfo.js deleted file mode 100644 index af023266b..000000000 --- a/frontend/src/Episode/Summary/MediaInfo.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; - -function MediaInfo(props) { - return ( - - { - Object.keys(props).map((key) => { - const title = key - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()); - - const value = props[key]; - - if (!value) { - return null; - } - - return ( - - ); - }) - } - - ); -} - -export default MediaInfo; diff --git a/frontend/src/Episode/Summary/MediaInfo.tsx b/frontend/src/Episode/Summary/MediaInfo.tsx new file mode 100644 index 000000000..d0a895175 --- /dev/null +++ b/frontend/src/Episode/Summary/MediaInfo.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import MediaInfoProps from 'typings/MediaInfo'; +import getEntries from 'Utilities/Object/getEntries'; + +function MediaInfo(props: MediaInfoProps) { + return ( + + {getEntries(props).map(([key, value]) => { + const title = key + .replace(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase()); + + if (!value) { + return null; + } + + return ( + + ); + })} + + ); +} + +export default MediaInfo; diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js b/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js deleted file mode 100644 index 9178f37c0..000000000 --- a/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import EpisodeLanguages from 'Episode/EpisodeLanguages'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; - -function createMapStateToProps() { - return createSelector( - createEpisodeFileSelector(), - (episodeFile) => { - return { - languages: episodeFile ? episodeFile.languages : undefined - }; - } - ); -} - -export default connect(createMapStateToProps)(EpisodeLanguages); diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx new file mode 100644 index 000000000..c3ab2bbe1 --- /dev/null +++ b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import EpisodeLanguages from 'Episode/EpisodeLanguages'; +import useEpisodeFile from './useEpisodeFile'; + +interface EpisodeFileLanguagesProps { + episodeFileId: number; +} + +function EpisodeFileLanguages({ episodeFileId }: EpisodeFileLanguagesProps) { + const episodeFile = useEpisodeFile(episodeFileId); + + return ; +} + +export default EpisodeFileLanguages; diff --git a/frontend/src/EpisodeFile/MediaInfo.js b/frontend/src/EpisodeFile/MediaInfo.js deleted file mode 100644 index bcf196469..000000000 --- a/frontend/src/EpisodeFile/MediaInfo.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import getLanguageName from 'Utilities/String/getLanguageName'; -import translate from 'Utilities/String/translate'; -import * as mediaInfoTypes from './mediaInfoTypes'; - -function formatLanguages(languages) { - if (!languages) { - return null; - } - - const splitLanguages = _.uniq(languages.split('/')).map((l) => { - const simpleLanguage = l.split('_')[0]; - - if (simpleLanguage === 'und') { - return translate('Unknown'); - } - - return getLanguageName(simpleLanguage); - } - ); - - if (splitLanguages.length > 3) { - return ( - - {splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more - - ); - } - - return ( - - {splitLanguages.join(', ')} - - ); -} - -function MediaInfo(props) { - const { - type, - audioChannels, - audioCodec, - audioLanguages, - subtitles, - videoCodec, - videoDynamicRangeType - } = props; - - if (type === mediaInfoTypes.AUDIO) { - return ( - - { - audioCodec ? audioCodec : '' - } - - { - audioCodec && audioChannels ? ' - ' : '' - } - - { - audioChannels ? audioChannels.toFixed(1) : '' - } - - ); - } - - if (type === mediaInfoTypes.AUDIO_LANGUAGES) { - return formatLanguages(audioLanguages); - } - - if (type === mediaInfoTypes.SUBTITLES) { - return formatLanguages(subtitles); - } - - if (type === mediaInfoTypes.VIDEO) { - return ( - - {videoCodec} - - ); - } - - if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) { - return ( - - {videoDynamicRangeType} - - ); - } - - return null; -} - -MediaInfo.propTypes = { - type: PropTypes.string.isRequired, - audioChannels: PropTypes.number, - audioCodec: PropTypes.string, - audioLanguages: PropTypes.string, - subtitles: PropTypes.string, - videoCodec: PropTypes.string, - videoDynamicRangeType: PropTypes.string -}; - -export default MediaInfo; diff --git a/frontend/src/EpisodeFile/MediaInfo.tsx b/frontend/src/EpisodeFile/MediaInfo.tsx new file mode 100644 index 000000000..2a72ee5bb --- /dev/null +++ b/frontend/src/EpisodeFile/MediaInfo.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import getLanguageName from 'Utilities/String/getLanguageName'; +import translate from 'Utilities/String/translate'; +import useEpisodeFile from './useEpisodeFile'; + +function formatLanguages(languages: string | undefined) { + if (!languages) { + return null; + } + + const splitLanguages = [...new Set(languages.split('/'))].map((l) => { + const simpleLanguage = l.split('_')[0]; + + if (simpleLanguage === 'und') { + return translate('Unknown'); + } + + return getLanguageName(simpleLanguage); + }); + + if (splitLanguages.length > 3) { + return ( + + {splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '} + more + + ); + } + + return {splitLanguages.join(', ')}; +} + +export type MediaInfoType = + | 'audio' + | 'audioLanguages' + | 'subtitles' + | 'video' + | 'videoDynamicRangeType'; + +interface MediaInfoProps { + episodeFileId?: number; + type: MediaInfoType; +} + +function MediaInfo({ episodeFileId, type }: MediaInfoProps) { + const episodeFile = useEpisodeFile(episodeFileId); + + if (!episodeFile?.mediaInfo) { + return null; + } + + const { + audioChannels, + audioCodec, + audioLanguages, + subtitles, + videoCodec, + videoDynamicRangeType, + } = episodeFile.mediaInfo; + + if (type === 'audio') { + return ( + + {audioCodec ? audioCodec : ''} + + {audioCodec && audioChannels ? ' - ' : ''} + + {audioChannels ? audioChannels.toFixed(1) : ''} + + ); + } + + if (type === 'audioLanguages') { + return formatLanguages(audioLanguages); + } + + if (type === 'subtitles') { + return formatLanguages(subtitles); + } + + if (type === 'video') { + return {videoCodec}; + } + + if (type === 'videoDynamicRangeType') { + return {videoDynamicRangeType}; + } + + return null; +} + +export default MediaInfo; diff --git a/frontend/src/EpisodeFile/MediaInfoConnector.js b/frontend/src/EpisodeFile/MediaInfoConnector.js deleted file mode 100644 index bbb963cf4..000000000 --- a/frontend/src/EpisodeFile/MediaInfoConnector.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import MediaInfo from './MediaInfo'; - -function createMapStateToProps() { - return createSelector( - createEpisodeFileSelector(), - (episodeFile) => { - if (episodeFile) { - return { - ...episodeFile.mediaInfo - }; - } - - return {}; - } - ); -} - -export default connect(createMapStateToProps)(MediaInfo); diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index 85243b6bb..a1dc3e21a 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -13,8 +13,8 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import IndexerFlags from 'Episode/IndexerFlags'; -import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; -import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; +import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; +import MediaInfo from 'EpisodeFile/MediaInfo'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -229,7 +229,7 @@ class EpisodeRow extends Component { key={name} className={styles.languages} > - @@ -242,7 +242,7 @@ class EpisodeRow extends Component { key={name} className={styles.audio} > - @@ -256,7 +256,7 @@ class EpisodeRow extends Component { key={name} className={styles.audioLanguages} > - @@ -270,7 +270,7 @@ class EpisodeRow extends Component { key={name} className={styles.subtitles} > - @@ -284,7 +284,7 @@ class EpisodeRow extends Component { key={name} className={styles.video} > - @@ -298,7 +298,7 @@ class EpisodeRow extends Component { key={name} className={styles.videoDynamicRangeType} > - diff --git a/frontend/src/Utilities/Object/getEntries.ts b/frontend/src/Utilities/Object/getEntries.ts new file mode 100644 index 000000000..ca540c5da --- /dev/null +++ b/frontend/src/Utilities/Object/getEntries.ts @@ -0,0 +1,9 @@ +export type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +function getEntries(obj: T): Entries { + return Object.entries(obj) as Entries; +} + +export default getEntries; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 05fed682c..6915f7b80 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -9,7 +9,7 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; +import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import styles from './CutoffUnmetRow.css'; @@ -123,7 +123,7 @@ function CutoffUnmetRow(props) { key={name} className={styles.languages} > -