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}
>
-