Fixed: Movie Path UI Warning, Duplicate Import Fixes

pull/2/head
Qstick 5 years ago
parent b225435164
commit 7fd391259c

@ -1,5 +1,5 @@
.description {
composes: title from '~Components/DescriptionList/DescriptionListItemDescription.css';
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
overflow-wrap: break-word;
}

@ -307,7 +307,7 @@ QueueRow.propTypes = {
trackedDownloadStatus: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
movie: PropTypes.object.isRequired,
movie: PropTypes.object,
quality: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,

@ -35,14 +35,20 @@
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-weight: 300;
font-size: 24px;
}
.noMoviesText {
margin-top: 80px;
margin-bottom: 20px;
}
.noResults {
margin-bottom: 10px;
font-weight: 300;

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
@ -79,7 +79,8 @@ class AddNewMovie extends Component {
render() {
const {
error,
items
items,
hasExistingMovies
} = this.props;
const term = this.state.term;
@ -160,13 +161,34 @@ class AddNewMovie extends Component {
}
{
!term &&
term ?
null :
<div className={styles.message}>
<div className={styles.helpText}>It's easy to add a new movie, just start typing the name the movie you want to add.</div>
<div className={styles.helpText}>
It's easy to add a new movie, just start typing the name the movie you want to add.
</div>
<div>You can also search using TMDB ID of a movie. eg. tmdb:71663</div>
</div>
}
{
!term && !hasExistingMovies ?
<div className={styles.message}>
<div className={styles.noMoviesText}>
You haven't added any movies yet, do you want to import some or all of your movies first?
</div>
<div>
<Button
to="/add/import"
kind={kinds.PRIMARY}
>
Import Existing Movies
</Button>
</div>
</div> :
null
}
<div />
</PageContentBodyConnector>
</PageContent>
@ -181,6 +203,7 @@ AddNewMovie.propTypes = {
isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingMovies: PropTypes.bool.isRequired,
onMovieLookupChange: PropTypes.func.isRequired,
onClearMovieLookup: PropTypes.func.isRequired
};

@ -11,13 +11,15 @@ import AddNewMovie from './AddNewMovie';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(state) => state.movies.items.length,
(state) => state.router.location,
(addMovie, location) => {
(addMovie, existingMoviesCount, location) => {
const { params } = parseUrl(location.search);
return {
...addMovie,
term: params.term,
...addMovie
hasExistingMovies: existingMoviesCount > 0
};
}
);

@ -55,8 +55,10 @@ class AddNewMovieModalContent extends Component {
rootFolderPath,
monitor,
qualityProfileId,
folder,
tags,
isSmallScreen,
isWindows,
onModalClose,
onInputChange
} = this.props;
@ -97,6 +99,15 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
valueOptions={{
movieFolder: folder,
isWindows
}}
selectedValueOptions={{
movieFolder: folder,
isWindows
}}
helpText={`'${folder}' subfolder will be created automatically`}
onChange={onInputChange}
{...rootFolderPath}
/>
@ -180,8 +191,10 @@ AddNewMovieModalContent.propTypes = {
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onAddMoviePress: PropTypes.func.isRequired

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setAddMovieDefault, addMovie } from 'Store/Actions/addMovieActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import AddNewMovieModalContent from './AddNewMovieModalContent';
@ -11,7 +12,8 @@ function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
createDimensionsSelector(),
(addMovieState, dimensions) => {
createSystemStatusSelector(),
(addMovieState, dimensions, systemStatus) => {
const {
isAdding,
addError,
@ -30,6 +32,7 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}

@ -52,6 +52,7 @@ class AddNewMovieSearchResult extends Component {
status,
overview,
ratings,
folder,
images,
isExistingMovie,
isExclusionMovie,
@ -148,6 +149,7 @@ class AddNewMovieSearchResult extends Component {
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
@ -165,6 +167,7 @@ AddNewMovieSearchResult.propTypes = {
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired,

@ -262,6 +262,7 @@ class EnhancedSelectInput extends Component {
isDisabled,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
optionComponent: OptionComponent
@ -363,6 +364,7 @@ class EnhancedSelectInput extends Component {
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...valueOptions}
{...v}
isMobile={false}
onSelect={this.onSelect}
@ -404,6 +406,7 @@ class EnhancedSelectInput extends Component {
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...valueOptions}
{...v}
isMobile={true}
onSelect={this.onSelect}
@ -431,6 +434,7 @@ EnhancedSelectInput.propTypes = {
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
selectedValueOptions: PropTypes.object.isRequired,
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
optionComponent: PropTypes.elementType,
@ -441,6 +445,7 @@ EnhancedSelectInput.defaultProps = {
className: styles.enhancedSelect,
disabledClassName: styles.isDisabled,
isDisabled: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,
optionComponent: HintedSelectInputOption

@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@ -13,7 +12,7 @@ function createMapStateToProps() {
(state) => state.rootFolders,
(state, { includeNoChange }) => includeNoChange,
(rootFolders, includeNoChange) => {
const values = _.map(rootFolders.items, (rootFolder) => {
const values = rootFolders.items.map((rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
@ -85,7 +84,7 @@ class RootFolderSelectInputConnector extends Component {
onChange
} = this.props;
if (!value || !_.some(values, (v) => v.key === value) || value === ADD_NEW_KEY) {
if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) {
const defaultValue = values[0];
if (defaultValue.key === ADD_NEW_KEY) {

@ -13,6 +13,15 @@
}
}
.value {
display: flex;
}
.movieFolder {
flex: 0 0 auto;
color: $disabledColor;
}
.freeSpace {
margin-left: 15px;
color: $darkGray;

@ -7,14 +7,20 @@ import styles from './RootFolderSelectInputOption.css';
function RootFolderSelectInputOption(props) {
const {
id,
value,
freeSpace,
movieFolder,
isMobile,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
return (
<EnhancedSelectInputOption
id={id}
isMobile={isMobile}
{...otherProps}
>
@ -23,7 +29,18 @@ function RootFolderSelectInputOption(props) {
isMobile && styles.isMobile
)}
>
<div>{value}</div>
<div className={styles.value}>
{value}
{
movieFolder && id !== 'addNew' ?
<div className={styles.movieFolder}>
{slashCharacter}
{movieFolder}
</div> :
null
}
</div>
{
freeSpace != null &&
@ -37,9 +54,12 @@ function RootFolderSelectInputOption(props) {
}
RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
isMobile: PropTypes.bool.isRequired
movieFolder: PropTypes.string,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};
export default RootFolderSelectInputOption;

@ -7,10 +7,20 @@
overflow: hidden;
}
.pathContainer {
@add-mixin truncate;
display: flex;
flex: 1 0 0;
}
.path {
@add-mixin truncate;
flex: 0 1 auto;
}
flex: 1 0 0;
.movieFolder {
flex: 0 1 auto;
color: $disabledColor;
}
.freeSpace {

@ -8,17 +8,32 @@ function RootFolderSelectInputSelectedValue(props) {
const {
value,
freeSpace,
movieFolder,
includeFreeSpace,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.path}>
{value}
<div className={styles.pathContainer}>
<div className={styles.path}>
{value}
</div>
{
movieFolder ?
<div className={styles.movieFolder}>
{slashCharacter}
{movieFolder}
</div> :
null
}
</div>
{
@ -34,6 +49,8 @@ function RootFolderSelectInputSelectedValue(props) {
RootFolderSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
freeSpace: PropTypes.number,
movieFolder: PropTypes.string,
isWindows: PropTypes.bool,
includeFreeSpace: PropTypes.bool.isRequired
};

@ -227,19 +227,9 @@ namespace NzbDrone.Api.Movies
public static Core.Movies.Movie ToModel(this MovieResource resource, Core.Movies.Movie movie)
{
movie.ImdbId = resource.ImdbId;
movie.TmdbId = resource.TmdbId;
movie.Path = resource.Path;
movie.ProfileId = resource.ProfileId;
movie.PathState = resource.PathState;
movie.Monitored = resource.Monitored;
movie.MinimumAvailability = resource.MinimumAvailability;
movie.RootFolderPath = resource.RootFolderPath;
movie.Tags = resource.Tags;
movie.AddOptions = resource.AddOptions;
var updatedmovie = resource.ToModel();
movie.ApplyChanges(updatedmovie);
return movie;
}

@ -81,6 +81,15 @@ namespace NzbDrone.Common.Extensions
return Directory.GetParent(cleanPath)?.FullName;
}
public static string GetCleanPath(this string path)
{
var cleanPath = OsInfo.IsWindows
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
: path.TrimEnd(Path.DirectorySeparatorChar);
return cleanPath;
}
public static bool IsParentPath(this string parentPath, string childPath)
{
if (parentPath != "/" && !parentPath.EndsWith(":\\"))

@ -142,6 +142,22 @@ namespace NzbDrone.Core.Movies
{
return string.Format("[{1} ({2})][{0}, {3}]", ImdbId, Title.NullSafe(), Year.NullSafe(), TmdbId);
}
public void ApplyChanges(Movie otherMovie)
{
TmdbId = otherMovie.TmdbId;
Path = otherMovie.Path;
ProfileId = otherMovie.ProfileId;
PathState = otherMovie.PathState;
Monitored = otherMovie.Monitored;
MinimumAvailability = otherMovie.MinimumAvailability;
RootFolderPath = otherMovie.RootFolderPath;
Tags = otherMovie.Tags;
AddOptions = otherMovie.AddOptions;
}
}
public enum MoviePathState

@ -1,25 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.Movies
{
public class MovieEditedService : IHandle<MovieEditedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
public MovieEditedService(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Handle(MovieEditedEvent message)
{
if (message.Movie.ImdbId != message.OldMovie.ImdbId)
{
_commandQueueManager.Push(new RefreshMovieCommand(message.Movie.Id)); //Probably not needed, as metadata should stay the same.
}
}
}
}

@ -0,0 +1,39 @@
using System;
using System.IO;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Organizer;
namespace Radarr.Api.V2.Movies
{
public class MovieFolderAsRootFolderValidator : PropertyValidator
{
private readonly IBuildFileNames _fileNameBuilder;
public MovieFolderAsRootFolderValidator(IBuildFileNames fileNameBuilder)
: base("Root folder path contains movie folder")
{
_fileNameBuilder = fileNameBuilder;
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null) return true;
var movieResource = context.Instance as MovieResource;
if (movieResource == null) return true;
var rootFolderPath = context.PropertyValue.ToString();
var rootFolder = new DirectoryInfo(rootFolderPath).Name;
var movie = movieResource.ToModel();
var movieFolder = _fileNameBuilder.GetMovieFolder(movie);
if (movieFolder == rootFolder) return false;
var distance = movieFolder.LevenshteinDistance(rootFolder);
return distance >= Math.Max(1, movieFolder.Length * 0.2);
}
}
}

@ -8,6 +8,7 @@ using System.Linq;
using System;
using Radarr.Http;
using Radarr.Http.REST;
using NzbDrone.Core.Organizer;
namespace Radarr.Api.V2.Movies
{
@ -15,12 +16,14 @@ namespace Radarr.Api.V2.Movies
{
private readonly ISearchForNewMovie _searchProxy;
private readonly IProvideMovieInfo _movieInfo;
private readonly IBuildFileNames _fileNameBuilder;
public MovieLookupModule(ISearchForNewMovie searchProxy, IProvideMovieInfo movieInfo)
public MovieLookupModule(ISearchForNewMovie searchProxy, IProvideMovieInfo movieInfo, IBuildFileNames fileNameBuilder)
: base("/movie/lookup")
{
_movieInfo = movieInfo;
_searchProxy = searchProxy;
_fileNameBuilder = fileNameBuilder;
Get("/", x => Search());
Get("/tmdb", x => SearchByTmdbId());
Get("/imdb", x => SearchByImdbId());
@ -51,17 +54,19 @@ namespace Radarr.Api.V2.Movies
return MapToResource(imdbResults);
}
private static IEnumerable<MovieResource> MapToResource(IEnumerable<Movie> movies)
private IEnumerable<MovieResource> MapToResource(IEnumerable<Movie> movies)
{
foreach (var currentSeries in movies)
foreach (var currentMovie in movies)
{
var resource = currentSeries.ToResource();
var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
var resource = currentMovie.ToResource();
var poster = currentMovie.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
if (poster != null)
{
resource.RemotePoster = poster.Url;
}
resource.Folder = _fileNameBuilder.GetMovieFolder(currentMovie);
yield return resource;
}
}

@ -38,7 +38,8 @@ namespace Radarr.Api.V2.Movies
MoviePathValidator moviesPathValidator,
MovieExistsValidator moviesExistsValidator,
MovieAncestorValidator moviesAncestorValidator,
ProfileExistsValidator profileExistsValidator
ProfileExistsValidator profileExistsValidator,
MovieFolderAsRootFolderValidator movieFolderAsRootFolderValidator
)
: base(signalRBroadcaster)
{
@ -65,7 +66,10 @@ namespace Radarr.Api.V2.Movies
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath)
.IsValidPath()
.SetValidator(movieFolderAsRootFolderValidator)
.When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator);

@ -58,6 +58,7 @@ namespace Radarr.Api.V2.Movies
public int TmdbId { get; set; }
public string TitleSlug { get; set; }
public string RootFolderPath { get; set; }
public string Folder { get; set; }
public string Certification { get; set; }
public List<string> Genres { get; set; }
public HashSet<int> Tags { get; set; }
@ -178,19 +179,9 @@ namespace Radarr.Api.V2.Movies
public static Movie ToModel(this MovieResource resource, Movie movie)
{
movie.ImdbId = resource.ImdbId;
movie.TmdbId = resource.TmdbId;
movie.Path = resource.Path;
movie.ProfileId = resource.QualityProfileId;
movie.PathState = resource.PathState;
movie.Monitored = resource.Monitored;
movie.MinimumAvailability = resource.MinimumAvailability;
movie.RootFolderPath = resource.RootFolderPath;
movie.Tags = resource.Tags;
movie.AddOptions = resource.AddOptions;
var updatedmovie = resource.ToModel();
movie.ApplyChanges(updatedmovie);
return movie;
}

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.RootFolders;
using Radarr.Http.REST;
@ -23,7 +24,7 @@ namespace Radarr.Api.V2.RootFolders
{
Id = model.Id,
Path = model.Path,
Path = model.Path.GetCleanPath(),
FreeSpace = model.FreeSpace,
UnmappedFolders = model.UnmappedFolders
};
@ -37,7 +38,7 @@ namespace Radarr.Api.V2.RootFolders
{
Id = resource.Id,
Path = resource.Path,
Path = resource.Path
//FreeSpace
//UnmappedFolders
};

Loading…
Cancel
Save