New: Show ExtraFiles in UI

This commit is contained in:
Qstick 2020-03-03 22:55:17 -05:00
parent 3576f529ec
commit f338941cfc
15 changed files with 505 additions and 8 deletions

View File

@ -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 {
<div className={styles.contentContainer}>
{
!isPopulated && !movieFilesError && !movieCreditsError &&
!isPopulated && !movieFilesError && !movieCreditsError && !extraFilesError &&
<LoadingIndicator />
}
{
!isFetching && movieFilesError && !movieCreditsError &&
!isFetching && movieFilesError &&
<div>Loading movie files failed</div>
}
{
!isFetching && movieCreditsError &&
<div>Loading movie credits failed</div>
}
{
!isFetching && extraFilesError &&
<div>Loading movie extra files failed</div>
}
<Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ selectedTabIndex: tabIndex })}>
<TabList
className={styles.tabList}
@ -537,6 +549,9 @@ class MovieDetails extends Component {
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
@ -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,

View File

@ -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,

View File

@ -0,0 +1,10 @@
.container {
margin-top: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
&:last-of-type {
margin-bottom: 0;
}
}

View File

@ -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 (
<MovieFileEditorTableContentConnector
movieId={movieId}
/>
<div className={styles.container}>
<MovieFileEditorTableContentConnector
movieId={movieId}
/>
</div>
);
}

View File

@ -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';
}

View File

@ -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 (
<TableRow>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCell
className={styles.extension}
title={extension}
>
{extension}
</TableRowCell>
<TableRowCell
className={styles.type}
title={type}
>
{titleCase(type)}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.INFO}
/>
</TableRowCell>
</TableRow>
);
}
}
ExtraFileRow.propTypes = {
id: PropTypes.number.isRequired,
extension: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
relativePath: PropTypes.string.isRequired
};
export default ExtraFileRow;

View File

@ -0,0 +1,10 @@
.container {
margin-top: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
&:last-of-type {
margin-bottom: 0;
}
}

View File

@ -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 (
<div className={styles.container}>
<ExtraFileTableContentConnector
movieId={movieId}
/>
</div>
);
}
ExtraFileTable.propTypes = {
movieId: PropTypes.number.isRequired
};
export default ExtraFileTable;

View File

@ -0,0 +1,10 @@
.actions {
display: flex;
margin-right: auto;
}
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}

View File

@ -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 (
<div>
{
!items.length &&
<div className={styles.blankpad}>
No extra files to manage.
</div>
}
{
!!items.length &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<ExtraFileRow
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
ExtraFileTableContent.propTypes = {
movieId: PropTypes.number,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default ExtraFileTableContent;

View File

@ -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 (
<ExtraFileTableContent
{...otherProps}
/>
);
}
}
ExtraFileTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(ExtraFileTableContentConnector);

View File

@ -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);

View File

@ -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,

View File

@ -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<ExtraFileResource>
{
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IExtraFileService<OtherExtraFile> _otherFileService;
public ExtraFileModule(IExtraFileService<SubtitleFile> subtitleFileService, IExtraFileService<MetadataFile> metadataFileService, IExtraFileService<OtherExtraFile> otherExtraFileService)
: base("/extrafile")
{
_subtitleFileService = subtitleFileService;
_metadataFileService = metadataFileService;
_otherFileService = otherExtraFileService;
GetResourceAll = GetFiles;
}
private List<ExtraFileResource> GetFiles()
{
if (!Request.Query.MovieId.HasValue)
{
throw new BadRequestException("MovieId is missing");
}
var extraFiles = new List<ExtraFileResource>();
List<SubtitleFile> subtitleFiles = _subtitleFileService.GetFilesByMovie(Request.Query.MovieId);
List<MetadataFile> metadataFiles = _metadataFileService.GetFilesByMovie(Request.Query.MovieId);
List<OtherExtraFile> otherExtraFiles = _otherFileService.GetFilesByMovie(Request.Query.MovieId);
extraFiles.AddRange(subtitleFiles.ToResource());
extraFiles.AddRange(metadataFiles.ToResource());
extraFiles.AddRange(otherExtraFiles.ToResource());
return extraFiles;
}
}
}

View File

@ -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<ExtraFileResource> ToResource(this IEnumerable<SubtitleFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<MetadataFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<OtherExtraFile> movies)
{
return movies.Select(ToResource).ToList();
}
}
}