diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js
index 2956d9e55..c753000e2 100644
--- a/frontend/src/Movie/Details/MovieDetails.js
+++ b/frontend/src/Movie/Details/MovieDetails.js
@@ -25,6 +25,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Popover from 'Components/Tooltip/Popover';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
+import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import MoviePoster from 'Movie/MoviePoster';
@@ -182,6 +183,7 @@ class MovieDetails extends Component {
isSmallScreen,
movieFilesError,
movieCreditsError,
+ extraFilesError,
hasMovieFiles,
previousMovie,
nextMovie,
@@ -457,15 +459,25 @@ class MovieDetails extends Component {
{
- !isPopulated && !movieFilesError && !movieCreditsError &&
+ !isPopulated && !movieFilesError && !movieCreditsError && !extraFilesError &&
}
{
- !isFetching && movieFilesError && !movieCreditsError &&
+ !isFetching && movieFilesError &&
Loading movie files failed
}
+ {
+ !isFetching && movieCreditsError &&
+
Loading movie credits failed
+ }
+
+ {
+ !isFetching && extraFilesError &&
+
Loading movie extra files failed
+ }
+
this.setState({ selectedTabIndex: tabIndex })}>
+
@@ -623,6 +638,7 @@ MovieDetails.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
movieFilesError: PropTypes.object,
movieCreditsError: PropTypes.object,
+ extraFilesError: PropTypes.object,
hasMovieFiles: PropTypes.bool.isRequired,
previousMovie: PropTypes.object.isRequired,
nextMovie: PropTypes.object.isRequired,
diff --git a/frontend/src/Movie/Details/MovieDetailsConnector.js b/frontend/src/Movie/Details/MovieDetailsConnector.js
index 7584531ca..b656a0a02 100644
--- a/frontend/src/Movie/Details/MovieDetailsConnector.js
+++ b/frontend/src/Movie/Details/MovieDetailsConnector.js
@@ -9,6 +9,7 @@ import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
+import { fetchExtraFiles, clearExtraFiles } from 'Store/Actions/extraFileActions';
import { fetchMovieCredits, clearMovieCredits } from 'Store/Actions/movieCreditsActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
@@ -59,15 +60,33 @@ const selectMovieCredits = createSelector(
}
);
+const selectExtraFiles = createSelector(
+ (state) => state.extraFiles,
+ (extraFiles) => {
+ const {
+ isFetching,
+ isPopulated,
+ error
+ } = extraFiles;
+
+ return {
+ isExtraFilesFetching: isFetching,
+ isExtraFilesPopulated: isPopulated,
+ extraFilesError: error
+ };
+ }
+);
+
function createMapStateToProps() {
return createSelector(
(state, { titleSlug }) => titleSlug,
selectMovieFiles,
selectMovieCredits,
+ selectExtraFiles,
createAllMoviesSelector(),
createCommandsSelector(),
createDimensionsSelector(),
- (titleSlug, movieFiles, movieCredits, allMovies, commands, dimensions) => {
+ (titleSlug, movieFiles, movieCredits, extraFiles, allMovies, commands, dimensions) => {
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
const movie = sortedMovies[movieIndex];
@@ -90,6 +109,12 @@ function createMapStateToProps() {
movieCreditsError
} = movieCredits;
+ const {
+ isExtraFilesFetching,
+ isExtraFilesPopulated,
+ extraFilesError
+ } = extraFiles;
+
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
const isMovieRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_MOVIE, movieId: movie.id }));
@@ -107,8 +132,8 @@ function createMapStateToProps() {
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
);
- const isFetching = isMovieFilesFetching && isMovieCreditsFetching;
- const isPopulated = isMovieFilesPopulated && isMovieCreditsPopulated;
+ const isFetching = isMovieFilesFetching || isMovieCreditsFetching || isExtraFilesFetching;
+ const isPopulated = isMovieFilesPopulated && isMovieCreditsPopulated && isExtraFilesPopulated;
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
acc.push(alternateTitle.title);
return acc;
@@ -127,6 +152,7 @@ function createMapStateToProps() {
isPopulated,
movieFilesError,
movieCreditsError,
+ extraFilesError,
hasMovieFiles,
sizeOnDisk,
previousMovie,
@@ -142,6 +168,8 @@ const mapDispatchToProps = {
clearMovieFiles,
fetchMovieCredits,
clearMovieCredits,
+ fetchExtraFiles,
+ clearExtraFiles,
clearReleases,
cancelFetchReleases,
fetchNetImportSchema,
@@ -200,6 +228,7 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id;
this.props.fetchMovieFiles({ movieId });
+ this.props.fetchExtraFiles({ movieId });
this.props.fetchMovieCredits({ movieId });
this.props.fetchQueueDetails({ movieId });
this.props.fetchNetImportSchema();
@@ -208,6 +237,7 @@ class MovieDetailsConnector extends Component {
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearMovieFiles();
+ this.props.clearExtraFiles();
this.props.clearMovieCredits();
this.props.clearQueueDetails();
this.props.clearReleases();
@@ -263,6 +293,8 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired,
+ fetchExtraFiles: PropTypes.func.isRequired,
+ clearExtraFiles: PropTypes.func.isRequired,
fetchMovieCredits: PropTypes.func.isRequired,
clearMovieCredits: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTable.css b/frontend/src/MovieFile/Editor/MovieFileEditorTable.css
new file mode 100644
index 000000000..50d1a9e93
--- /dev/null
+++ b/frontend/src/MovieFile/Editor/MovieFileEditorTable.css
@@ -0,0 +1,10 @@
+.container {
+ margin-top: 20px;
+ border: 1px solid $borderColor;
+ border-radius: 4px;
+ background-color: $white;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTable.js b/frontend/src/MovieFile/Editor/MovieFileEditorTable.js
index 6d3d37393..5c71c631a 100644
--- a/frontend/src/MovieFile/Editor/MovieFileEditorTable.js
+++ b/frontend/src/MovieFile/Editor/MovieFileEditorTable.js
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieFileEditorTableContentConnector from './MovieFileEditorTableContentConnector';
+import styles from './MovieFileEditorTable.css';
function MovieFileEditorTable(props) {
const {
@@ -8,9 +9,11 @@ function MovieFileEditorTable(props) {
} = props;
return (
-
+
+
+
);
}
diff --git a/frontend/src/MovieFile/Extras/ExtraFileRow.css b/frontend/src/MovieFile/Extras/ExtraFileRow.css
new file mode 100644
index 000000000..fcf05d5a5
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileRow.css
@@ -0,0 +1,10 @@
+.relativePath {
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
+
+ word-break: break-all;
+}
+
+.extension,
+.type {
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
+}
diff --git a/frontend/src/MovieFile/Extras/ExtraFileRow.js b/frontend/src/MovieFile/Extras/ExtraFileRow.js
new file mode 100644
index 000000000..cc626b3e7
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileRow.js
@@ -0,0 +1,63 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import IconButton from 'Components/Link/IconButton';
+import { icons } from 'Helpers/Props';
+import titleCase from 'Utilities/String/titleCase';
+import TableRow from 'Components/Table/TableRow';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import styles from './ExtraFileRow.css';
+
+class ExtraFileRow extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ relativePath,
+ extension,
+ type
+ } = this.props;
+
+ return (
+
+
+ {relativePath}
+
+
+
+ {extension}
+
+
+
+ {titleCase(type)}
+
+
+
+
+
+
+ );
+ }
+
+}
+
+ExtraFileRow.propTypes = {
+ id: PropTypes.number.isRequired,
+ extension: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ relativePath: PropTypes.string.isRequired
+};
+
+export default ExtraFileRow;
diff --git a/frontend/src/MovieFile/Extras/ExtraFileTable.css b/frontend/src/MovieFile/Extras/ExtraFileTable.css
new file mode 100644
index 000000000..50d1a9e93
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileTable.css
@@ -0,0 +1,10 @@
+.container {
+ margin-top: 20px;
+ border: 1px solid $borderColor;
+ border-radius: 4px;
+ background-color: $white;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/frontend/src/MovieFile/Extras/ExtraFileTable.js b/frontend/src/MovieFile/Extras/ExtraFileTable.js
new file mode 100644
index 000000000..46381c26f
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileTable.js
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ExtraFileTableContentConnector from './ExtraFileTableContentConnector';
+import styles from './ExtraFileTable.css';
+
+function ExtraFileTable(props) {
+ const {
+ movieId
+ } = props;
+
+ return (
+
+
+
+
+ );
+}
+
+ExtraFileTable.propTypes = {
+ movieId: PropTypes.number.isRequired
+};
+
+export default ExtraFileTable;
diff --git a/frontend/src/MovieFile/Extras/ExtraFileTableContent.css b/frontend/src/MovieFile/Extras/ExtraFileTableContent.css
new file mode 100644
index 000000000..2bb243f27
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileTableContent.css
@@ -0,0 +1,10 @@
+.actions {
+ display: flex;
+ margin-right: auto;
+}
+
+.blankpad {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-left: 2em;
+}
diff --git a/frontend/src/MovieFile/Extras/ExtraFileTableContent.js b/frontend/src/MovieFile/Extras/ExtraFileTableContent.js
new file mode 100644
index 000000000..ddfc81e6a
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileTableContent.js
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { icons } from 'Helpers/Props';
+import IconButton from 'Components/Link/IconButton';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import ExtraFileRow from './ExtraFileRow';
+import styles from './ExtraFileTableContent.css';
+
+const columns = [
+ {
+ name: 'relativePath',
+ label: 'Extra File',
+ isVisible: true
+ },
+ {
+ name: 'extension',
+ label: 'Extension',
+ isVisible: true
+ },
+ {
+ name: 'type',
+ label: 'Type',
+ isVisible: true
+ },
+ {
+ name: 'action',
+ label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
+ isVisible: true
+ }
+];
+
+class ExtraFileTableContent extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ items
+ } = this.props;
+
+ return (
+
+ {
+ !items.length &&
+
+ No extra files to manage.
+
+ }
+
+ {
+ !!items.length &&
+
+
+ {
+ items.map((item) => {
+ return (
+
+ );
+ })
+ }
+
+
+ }
+
+
+ );
+ }
+}
+
+ExtraFileTableContent.propTypes = {
+ movieId: PropTypes.number,
+ items: PropTypes.arrayOf(PropTypes.object).isRequired
+};
+
+export default ExtraFileTableContent;
diff --git a/frontend/src/MovieFile/Extras/ExtraFileTableContentConnector.js b/frontend/src/MovieFile/Extras/ExtraFileTableContentConnector.js
new file mode 100644
index 000000000..38a2720f6
--- /dev/null
+++ b/frontend/src/MovieFile/Extras/ExtraFileTableContentConnector.js
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createMovieSelector from 'Store/Selectors/createMovieSelector';
+import ExtraFileTableContent from './ExtraFileTableContent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.extraFiles,
+ createMovieSelector(),
+ (
+ ExtraFiles
+ ) => {
+ return {
+ items: ExtraFiles.items,
+ error: null
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ };
+}
+
+class ExtraFileTableContentConnector extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+ExtraFileTableContentConnector.propTypes = {
+ movieId: PropTypes.number.isRequired
+};
+
+export default connect(createMapStateToProps, createMapDispatchToProps)(ExtraFileTableContentConnector);
diff --git a/frontend/src/Store/Actions/extraFileActions.js b/frontend/src/Store/Actions/extraFileActions.js
new file mode 100644
index 000000000..04d2c728a
--- /dev/null
+++ b/frontend/src/Store/Actions/extraFileActions.js
@@ -0,0 +1,49 @@
+import { createAction } from 'redux-actions';
+import { createThunk, handleThunks } from 'Store/thunks';
+import createFetchHandler from './Creators/createFetchHandler';
+import createHandleActions from './Creators/createHandleActions';
+
+//
+// Variables
+
+export const section = 'extraFiles';
+
+//
+// State
+
+export const defaultState = {
+ isFetching: false,
+ isPopulated: false,
+ error: null,
+ items: []
+};
+
+//
+// Actions Types
+
+export const FETCH_EXTRA_FILES = 'extraFiles/fetchExtraFiles';
+export const CLEAR_EXTRA_FILES = 'extraFiles/clearExtraFiles';
+
+//
+// Action Creators
+
+export const fetchExtraFiles = createThunk(FETCH_EXTRA_FILES);
+export const clearExtraFiles = createAction(CLEAR_EXTRA_FILES);
+
+//
+// Action Handlers
+
+export const actionHandlers = handleThunks({
+ [FETCH_EXTRA_FILES]: createFetchHandler(section, '/extraFile')
+});
+
+//
+// Reducers
+
+export const reducers = createHandleActions({
+
+ [CLEAR_EXTRA_FILES]: (state) => {
+ return Object.assign({}, state, defaultState);
+ }
+
+}, defaultState, section);
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index 03b67a0bc..03d201680 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -6,6 +6,7 @@ import * as captcha from './captchaActions';
import * as customFilters from './customFilterActions';
import * as commands from './commandActions';
import * as movieFiles from './movieFileActions';
+import * as extraFiles from './extraFileActions';
import * as history from './historyActions';
import * as importMovie from './importMovieActions';
import * as interactiveImportActions from './interactiveImportActions';
@@ -33,6 +34,7 @@ export default [
commands,
customFilters,
movieFiles,
+ extraFiles,
history,
importMovie,
interactiveImportActions,
diff --git a/src/Radarr.Api.V3/ExtraFiles/ExtraFileModule.cs b/src/Radarr.Api.V3/ExtraFiles/ExtraFileModule.cs
new file mode 100644
index 000000000..25407ff3c
--- /dev/null
+++ b/src/Radarr.Api.V3/ExtraFiles/ExtraFileModule.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using NzbDrone.Core.Extras.Files;
+using NzbDrone.Core.Extras.Metadata.Files;
+using NzbDrone.Core.Extras.Others;
+using NzbDrone.Core.Extras.Subtitles;
+using Radarr.Http;
+using Radarr.Http.REST;
+
+namespace Radarr.Api.V3.ExtraFiles
+{
+ public class ExtraFileModule : RadarrRestModule
+ {
+ private readonly IExtraFileService _subtitleFileService;
+ private readonly IExtraFileService _metadataFileService;
+ private readonly IExtraFileService _otherFileService;
+
+ public ExtraFileModule(IExtraFileService subtitleFileService, IExtraFileService metadataFileService, IExtraFileService otherExtraFileService)
+ : base("/extrafile")
+ {
+ _subtitleFileService = subtitleFileService;
+ _metadataFileService = metadataFileService;
+ _otherFileService = otherExtraFileService;
+ GetResourceAll = GetFiles;
+ }
+
+ private List GetFiles()
+ {
+ if (!Request.Query.MovieId.HasValue)
+ {
+ throw new BadRequestException("MovieId is missing");
+ }
+
+ var extraFiles = new List();
+
+ List subtitleFiles = _subtitleFileService.GetFilesByMovie(Request.Query.MovieId);
+ List metadataFiles = _metadataFileService.GetFilesByMovie(Request.Query.MovieId);
+ List otherExtraFiles = _otherFileService.GetFilesByMovie(Request.Query.MovieId);
+
+ extraFiles.AddRange(subtitleFiles.ToResource());
+ extraFiles.AddRange(metadataFiles.ToResource());
+ extraFiles.AddRange(otherExtraFiles.ToResource());
+
+ return extraFiles;
+ }
+ }
+}
diff --git a/src/Radarr.Api.V3/ExtraFiles/ExtraFileResource.cs b/src/Radarr.Api.V3/ExtraFiles/ExtraFileResource.cs
new file mode 100644
index 000000000..9d465ee7d
--- /dev/null
+++ b/src/Radarr.Api.V3/ExtraFiles/ExtraFileResource.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using System.Linq;
+using NzbDrone.Core.Extras.Files;
+using NzbDrone.Core.Extras.Metadata.Files;
+using NzbDrone.Core.Extras.Others;
+using NzbDrone.Core.Extras.Subtitles;
+using Radarr.Http.REST;
+
+namespace Radarr.Api.V3.ExtraFiles
+{
+ public class ExtraFileResource : RestResource
+ {
+ public int MovieId { get; set; }
+ public int? MovieFileId { get; set; }
+ public string RelativePath { get; set; }
+ public string Extension { get; set; }
+ public ExtraFileType Type { get; set; }
+ }
+
+ public static class ExtraFileResourceMapper
+ {
+ public static ExtraFileResource ToResource(this MetadataFile model)
+ {
+ if (model == null)
+ {
+ return null;
+ }
+
+ return new ExtraFileResource
+ {
+ Id = model.Id,
+ MovieId = model.MovieId,
+ MovieFileId = model.MovieFileId,
+ RelativePath = model.RelativePath,
+ Extension = model.Extension,
+ Type = ExtraFileType.Metadata
+ };
+ }
+
+ public static ExtraFileResource ToResource(this SubtitleFile model)
+ {
+ if (model == null)
+ {
+ return null;
+ }
+
+ return new ExtraFileResource
+ {
+ Id = model.Id,
+ MovieId = model.MovieId,
+ MovieFileId = model.MovieFileId,
+ RelativePath = model.RelativePath,
+ Extension = model.Extension,
+ Type = ExtraFileType.Subtitle
+ };
+ }
+
+ public static ExtraFileResource ToResource(this OtherExtraFile model)
+ {
+ if (model == null)
+ {
+ return null;
+ }
+
+ return new ExtraFileResource
+ {
+ Id = model.Id,
+ MovieId = model.MovieId,
+ MovieFileId = model.MovieFileId,
+ RelativePath = model.RelativePath,
+ Extension = model.Extension,
+ Type = ExtraFileType.Other
+ };
+ }
+
+ public static List ToResource(this IEnumerable movies)
+ {
+ return movies.Select(ToResource).ToList();
+ }
+
+ public static List ToResource(this IEnumerable movies)
+ {
+ return movies.Select(ToResource).ToList();
+ }
+
+ public static List ToResource(this IEnumerable movies)
+ {
+ return movies.Select(ToResource).ToList();
+ }
+ }
+}