From 6dd22e7dcb27d2f04a907cb19a7281d5ce3e97ee Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Mar 2015 16:42:37 -0800 Subject: [PATCH] New: Manual Import episodes --- gulp/less.js | 1 + .../FileSystem/FileSystemModule.cs | 45 ++- .../ManualImport/ManualImportModule.cs | 40 +++ .../ManualImport/ManualImportResource.cs | 32 +++ src/NzbDrone.Api/NzbDrone.Api.csproj | 2 + .../Disk/RelativeFileSystemModel.cs | 14 + src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../AbsoluteEpisodeNumberParserFixture.cs | 1 + .../DownloadedEpisodesImportService.cs | 71 ++--- .../Manual/ManualImportCommand.cs | 10 + .../EpisodeImport/Manual/ManualImportFile.cs | 14 + .../EpisodeImport/Manual/ManualImportItem.cs | 21 ++ .../Manual/ManualImportService.cs | 264 ++++++++++++++++++ .../Manual/ManuallyImportedFile.cs | 10 + .../EpisodeImport/ManualImportService.cs | 15 - src/NzbDrone.Core/NzbDrone.Core.csproj | 6 +- .../Parser/Model/LocalEpisode.cs | 5 + src/UI/Activity/Queue/QueueActionsCell.js | 27 +- .../Queue/QueueActionsCellTemplate.hbs | 2 +- src/UI/AppLayout.js | 8 +- src/UI/Cells/ApprovalStatusCellTemplate.hbs | 8 +- src/UI/Cells/Edit/QualityCellEditor.js | 8 +- src/UI/Cells/TemplatedCell.js | 10 +- src/UI/Cells/cells.less | 5 +- src/UI/Content/Bootstrap/bootstrap.less | 2 +- src/UI/Content/icons.less | 4 + src/UI/Content/theme.less | 1 + src/UI/Episode/Search/EpisodeSearchLayout.js | 1 - src/UI/ManualImport/Cells/EpisodesCell.js | 46 +++ src/UI/ManualImport/Cells/PathCell.js | 16 ++ src/UI/ManualImport/Cells/QualityCell.js | 23 ++ src/UI/ManualImport/Cells/SeasonCell.js | 47 ++++ src/UI/ManualImport/Cells/SeriesCell.js | 45 +++ src/UI/ManualImport/EmptyView.js | 5 + src/UI/ManualImport/EmptyViewTemplate.hbs | 1 + .../Episode/SelectEpisodeLayout.js | 81 ++++++ .../Episode/SelectEpisodeLayoutTemplate.hbs | 21 ++ .../ManualImport/Episode/SelectEpisodeRow.js | 20 ++ .../ManualImport/Folder/SelectFolderView.js | 49 ++++ .../Folder/SelectFolderViewTemplate.hbs | 21 ++ src/UI/ManualImport/ManualImportCollection.js | 74 +++++ src/UI/ManualImport/ManualImportLayout.js | 222 +++++++++++++++ .../ManualImportLayoutTemplate.hbs | 20 ++ src/UI/ManualImport/ManualImportModel.js | 4 + src/UI/ManualImport/ManualImportRow.js | 34 +++ .../Quality/SelectQualityLayout.js | 43 +++ .../Quality/SelectQualityLayoutTemplate.hbs | 19 ++ .../ManualImport/Quality/SelectQualityView.js | 37 +++ .../Quality/SelectQualityViewTemplate.hbs | 33 +++ .../ManualImport/Season/SelectSeasonLayout.js | 28 ++ .../Season/SelectSeasonLayoutTemplate.hbs | 29 ++ .../ManualImport/Series/SelectSeriesLayout.js | 92 ++++++ .../Series/SelectSeriesLayoutTemplate.hbs | 30 ++ src/UI/ManualImport/Series/SelectSeriesRow.js | 13 + .../Summary/ManualImportSummaryView.js | 20 ++ .../ManualImportSummaryViewTemplate.hbs | 19 ++ src/UI/ManualImport/manualimport.less | 39 +++ src/UI/Mixins/AsFilteredCollection.js | 7 +- .../Shared/FileBrowser/FileBrowserLayout.js | 57 ++-- src/UI/Shared/FileBrowser/FileBrowserRow.js | 4 +- src/UI/Shared/Modal/ModalController.js | 27 +- .../ModalRegion2.js} | 2 +- src/UI/Wanted/Missing/MissingLayout.js | 20 +- src/UI/index.html | 2 +- src/UI/login.html | 2 - src/UI/vent.js | 3 + 66 files changed, 1766 insertions(+), 117 deletions(-) create mode 100644 src/NzbDrone.Api/ManualImport/ManualImportModule.cs create mode 100644 src/NzbDrone.Api/ManualImport/ManualImportResource.cs create mode 100644 src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs create mode 100644 src/UI/ManualImport/Cells/EpisodesCell.js create mode 100644 src/UI/ManualImport/Cells/PathCell.js create mode 100644 src/UI/ManualImport/Cells/QualityCell.js create mode 100644 src/UI/ManualImport/Cells/SeasonCell.js create mode 100644 src/UI/ManualImport/Cells/SeriesCell.js create mode 100644 src/UI/ManualImport/EmptyView.js create mode 100644 src/UI/ManualImport/EmptyViewTemplate.hbs create mode 100644 src/UI/ManualImport/Episode/SelectEpisodeLayout.js create mode 100644 src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs create mode 100644 src/UI/ManualImport/Episode/SelectEpisodeRow.js create mode 100644 src/UI/ManualImport/Folder/SelectFolderView.js create mode 100644 src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs create mode 100644 src/UI/ManualImport/ManualImportCollection.js create mode 100644 src/UI/ManualImport/ManualImportLayout.js create mode 100644 src/UI/ManualImport/ManualImportLayoutTemplate.hbs create mode 100644 src/UI/ManualImport/ManualImportModel.js create mode 100644 src/UI/ManualImport/ManualImportRow.js create mode 100644 src/UI/ManualImport/Quality/SelectQualityLayout.js create mode 100644 src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs create mode 100644 src/UI/ManualImport/Quality/SelectQualityView.js create mode 100644 src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs create mode 100644 src/UI/ManualImport/Season/SelectSeasonLayout.js create mode 100644 src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs create mode 100644 src/UI/ManualImport/Series/SelectSeriesLayout.js create mode 100644 src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs create mode 100644 src/UI/ManualImport/Series/SelectSeriesRow.js create mode 100644 src/UI/ManualImport/Summary/ManualImportSummaryView.js create mode 100644 src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs create mode 100644 src/UI/ManualImport/manualimport.less rename src/UI/Shared/{FileBrowser/FileBrowserModalRegion.js => Modal/ModalRegion2.js} (97%) diff --git a/gulp/less.js b/gulp/less.js index 14a57f8f4..a4272b425 100644 --- a/gulp/less.js +++ b/gulp/less.js @@ -15,6 +15,7 @@ gulp.task('less', function () { paths.src.root + 'AddSeries/addSeries.less', paths.src.root + 'Calendar/calendar.less', paths.src.root + 'Cells/cells.less', + paths.src.root + 'ManualImport/manualimport.less', paths.src.root + 'Settings/settings.less', paths.src.root + 'System/Logs/logs.less', paths.src.root + 'System/Update/update.less', diff --git a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs index 64ef7d8b0..67c2be7bd 100644 --- a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs +++ b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs @@ -1,19 +1,31 @@ using System; +using System.IO; +using System.Linq; using Nancy; using NzbDrone.Api.Extensions; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; namespace NzbDrone.Api.FileSystem { public class FileSystemModule : NzbDroneApiModule { private readonly IFileSystemLookupService _fileSystemLookupService; + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; - public FileSystemModule(IFileSystemLookupService fileSystemLookupService) + public FileSystemModule(IFileSystemLookupService fileSystemLookupService, + IDiskProvider diskProvider, + IDiskScanService diskScanService) : base("/filesystem") { _fileSystemLookupService = fileSystemLookupService; + _diskProvider = diskProvider; + _diskScanService = diskScanService; Get["/"] = x => GetContents(); + Get["/type"] = x => GetEntityType(); + Get["/mediafiles"] = x => GetMediaFiles(); } private Response GetContents() @@ -29,5 +41,36 @@ private Response GetContents() return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse(); } + + private Response GetEntityType() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (_diskProvider.FileExists(path)) + { + return new { type = "file" }.AsResponse(); + } + + //Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system + return new { type = "folder" }.AsResponse(); + } + + private Response GetMediaFiles() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (!_diskProvider.FolderExists(path)) + { + return new string[0].AsResponse(); + } + + return _diskScanService.GetVideoFiles(path).Select(f => new { + Path = f, + RelativePath = path.GetRelativePath(f), + Name = Path.GetFileName(f) + }).AsResponse(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs new file mode 100644 index 000000000..dafb181a2 --- /dev/null +++ b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.ManualImport +{ + public class ManualImportModule : NzbDroneRestModule + { + private readonly IManualImportService _manualImportService; + + public ManualImportModule(IManualImportService manualImportService) + : base("/manualimport") + { + _manualImportService = manualImportService; + + GetResourceAll = GetMediaFiles; + } + + private List GetMediaFiles() + { + var folderQuery = Request.Query.folder; + var folder = (string)folderQuery.Value; + + var downloadIdQuery = Request.Query.downloadId; + var downloadId = (string)downloadIdQuery.Value; + + return ToListResource(_manualImportService.GetMediaFiles(folder, downloadId)).Select(AddQualityWeight).ToList(); + } + + private ManualImportResource AddQualityWeight(ManualImportResource item) + { + item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight; + item.QualityWeight += item.Quality.Revision.Real * 10; + item.QualityWeight += item.Quality.Revision.Version; + + return item; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs new file mode 100644 index 000000000..72d39e8f9 --- /dev/null +++ b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using NzbDrone.Api.Episodes; +using NzbDrone.Api.REST; +using NzbDrone.Api.Series; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.ManualImport +{ + public class ManualImportResource : RestResource + { + public string Path { get; set; } + public string RelativePath { get; set; } + public string Name { get; set; } + public long Size { get; set; } + public SeriesResource Series { get; set; } + public int? SeasonNumber { get; set; } + public List Episodes { get; set; } + public QualityModel Quality { get; set; } + public int QualityWeight { get; set; } + public string DownloadId { get; set; } + public IEnumerable Rejections { get; set; } + + public int Id + { + get + { + return Path.GetHashCode(); + } + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 8592a6ce5..5305a562f 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -102,6 +102,8 @@ + + diff --git a/src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs b/src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs new file mode 100644 index 000000000..595a132db --- /dev/null +++ b/src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs @@ -0,0 +1,14 @@ +using System; + +namespace NzbDrone.Common.Disk +{ + public class RelativeFileSystemModel + { + public string Name { get; set; } + public string Path { get; set; } + public string RelativePath { get; set; } + public string Extension { get; set; } + public long Size { get; set; } + public DateTime? LastModified { get; set; } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 8db2ba470..c19558116 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -72,6 +72,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index acc75ed1c..9205fd68e 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -77,6 +77,7 @@ public class AbsoluteEpisodeNumberParserFixture : CoreTest [TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep08_(8246e542).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 8, 0, 0)] [TestCase("Knights of Sidonia - 01 [1080p 10b DTSHD-MA eng sub].mkv", "Knights of Sidonia", 1, 0, 0)] [TestCase("Series Title (2010) {01} Episode Title (1).hdtv-720p", "Series Title (2010)", 1, 0, 0)] + [TestCase("[HorribleSubs] Shirobako - 20 [720p].mkv", "Shirobako", 20, 0, 0)] [TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 017 (115) [1280x720][B2CFBC0F]", "Dragon Ball Kai 2014", 17, 0, 0)] [TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 018 (116) [1280x720][C4A3B16E]", "Dragon Ball Kai 2014", 18, 0, 0)] [TestCase("Dragon Ball Kai (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Dragon Ball Kai 2014", 39, 0, 0)] diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 938d87a12..ec9beee5e 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -17,6 +17,7 @@ public interface IDownloadedEpisodesImportService { List ProcessRootFolder(DirectoryInfo directoryInfo); List ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null); + bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series); } public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService @@ -98,6 +99,41 @@ public List ProcessPath(string path, Series series = null, Downloa return new List(); } + public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series) + { + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); + + foreach (var videoFile in videoFiles) + { + var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); + + if (episodeParseResult == null) + { + _logger.Warn("Unable to parse file on import: [{0}]", videoFile); + return false; + } + + var size = _diskProvider.GetFileSize(videoFile); + var quality = QualityParser.ParseQuality(videoFile); + + if (!_detectSample.IsSample(series, quality, videoFile, size, + episodeParseResult.SeasonNumber)) + { + _logger.Warn("Non-sample file detected: [{0}]", videoFile); + return false; + } + } + + if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) + { + _logger.Warn("RAR file detected, will require manual cleanup"); + return false; + } + + return true; + } + private List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null) { var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); @@ -206,41 +242,6 @@ private string GetCleanedUpFolderName(string folder) return folder; } - private bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series) - { - var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); - var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); - - foreach (var videoFile in videoFiles) - { - var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); - - if (episodeParseResult == null) - { - _logger.Warn("Unable to parse file on import: [{0}]", videoFile); - return false; - } - - var size = _diskProvider.GetFileSize(videoFile); - var quality = QualityParser.ParseQuality(videoFile); - - if (!_detectSample.IsSample(series, quality, videoFile, size, - episodeParseResult.SeasonNumber)) - { - _logger.Warn("Non-sample file detected: [{0}]", videoFile); - return false; - } - } - - if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) - { - _logger.Warn("RAR file detected, will require manual cleanup"); - return false; - } - - return true; - } - private ImportResult FileIsLockedResult(string videoFile) { _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs new file mode 100644 index 000000000..a831a6710 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportCommand.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual +{ + public class ManualImportCommand : Command + { + public List Files { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs new file mode 100644 index 000000000..4c9fecc7c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual +{ + public class ManualImportFile + { + public string Path { get; set; } + public int SeriesId { get; set; } + public List EpisodeIds { get; set; } + public QualityModel Quality { get; set; } + public string DownloadId { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs new file mode 100644 index 000000000..bd3954816 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual +{ + public class ManualImportItem + { + public string Path { get; set; } + public string RelativePath { get; set; } + public string Name { get; set; } + public long Size { get; set; } + public Series Series { get; set; } + public int? SeasonNumber { get; set; } + public List Episodes { get; set; } + public QualityModel Quality { get; set; } + public string DownloadId { get; set; } + public IEnumerable Rejections { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs new file mode 100644 index 000000000..7c363dba4 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual +{ + public interface IManualImportService + { + List GetMediaFiles(string path, string downloadId); + } + + public class ManualImportService : IExecute, IManualImportService + { + private readonly IDiskProvider _diskProvider; + private readonly IParsingService _parsingService; + private readonly IDiskScanService _diskScanService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly ISeriesService _seriesService; + private readonly IEpisodeService _episodeService; + private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public ManualImportService(IDiskProvider diskProvider, + IParsingService parsingService, + IDiskScanService diskScanService, + IMakeImportDecision importDecisionMaker, + ISeriesService seriesService, + IEpisodeService episodeService, + IVideoFileInfoReader videoFileInfoReader, + IImportApprovedEpisodes importApprovedEpisodes, + ITrackedDownloadService trackedDownloadService, + IDownloadedEpisodesImportService downloadedEpisodesImportService, + IEventAggregator eventAggregator, + Logger logger) + { + _diskProvider = diskProvider; + _parsingService = parsingService; + _diskScanService = diskScanService; + _importDecisionMaker = importDecisionMaker; + _seriesService = seriesService; + _episodeService = episodeService; + _videoFileInfoReader = videoFileInfoReader; + _importApprovedEpisodes = importApprovedEpisodes; + _trackedDownloadService = trackedDownloadService; + _downloadedEpisodesImportService = downloadedEpisodesImportService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public List GetMediaFiles(string path, string downloadId) + { + if (downloadId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(downloadId); + + if (trackedDownload == null) + { + return new List(); + } + + path = trackedDownload.DownloadItem.OutputPath.FullPath; + } + + if (!_diskProvider.FolderExists(path)) + { + if (!_diskProvider.FileExists(path)) + { + return new List(); + } + + return new List { ProcessFile(path, downloadId) }; + } + + return ProcessFolder(path, downloadId); + } + + private List ProcessFolder(string folder, string downloadId) + { + var directoryInfo = new DirectoryInfo(folder); + var series = _parsingService.GetSeries(directoryInfo.Name); + + if (series == null && downloadId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(downloadId); + series = trackedDownload.RemoteEpisode.Series; + } + + if (series == null) + { + var files = _diskScanService.GetVideoFiles(folder); + + return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList(); + } + + var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); + var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList(); + var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, folderInfo, SceneSource(series, folder)); + + return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); + } + + private ManualImportItem ProcessFile(string file, string downloadId, string folder = null) + { + if (folder.IsNullOrWhiteSpace()) + { + folder = new FileInfo(file).Directory.FullName; + } + + var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(file)); + + if (series == null && downloadId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(downloadId); + series = trackedDownload.RemoteEpisode.Series; + } + + if (series == null) + { + var localEpisode = new LocalEpisode(); + localEpisode.Path = file; + localEpisode.Quality = QualityParser.ParseQuality(file); + localEpisode.Size = _diskProvider.GetFileSize(file); + + return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); + } + + var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, + series, null, SceneSource(series, folder)); + + return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; + } + + private bool SceneSource(Series series, string folder) + { + return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder)); + } + + private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId) + { + var item = new ManualImportItem(); + + item.Path = decision.LocalEpisode.Path; + item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); + item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); + item.DownloadId = downloadId; + + if (decision.LocalEpisode.Series != null) + { + item.Series = decision.LocalEpisode.Series; + } + + if (decision.LocalEpisode.Episodes.Any()) + { + item.SeasonNumber = decision.LocalEpisode.SeasonNumber; + item.Episodes = decision.LocalEpisode.Episodes; + } + + item.Quality = decision.LocalEpisode.Quality; + item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); + item.Rejections = decision.Rejections; + + return item; + } + + public void Execute(ManualImportCommand message) + { + _logger.ProgressInfo("Manually importing {0} files", message.Files.Count); + + var imported = new List(); + var importedTrackedDownload = new List(); + + for (int i = 0; i < message.Files.Count; i++) + { + _logger.ProgressInfo("Processing file {0} of {1}", i + 1, message.Files.Count); + + var file = message.Files[i]; + var series = _seriesService.GetSeries(file.SeriesId); + var episodes = _episodeService.GetEpisodes(file.EpisodeIds); + var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); + var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); + var existingFile = series.Path.IsParentPath(file.Path); + + var localEpisode = new LocalEpisode + { + ExistingFile = false, + Episodes = episodes, + MediaInfo = mediaInfo, + ParsedEpisodeInfo = parsedEpisodeInfo, + Path = file.Path, + Quality = file.Quality, + Series = series, + Size = 0 + }; + + //TODO: Option to copy instead of import + //TODO: Cleanup non-tracked downloads + + var importDecision = new ImportDecision(localEpisode); + + if (file.DownloadId.IsNullOrWhiteSpace()) + { + imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile)); + } + + else + { + var trackedDownload = _trackedDownloadService.Find(file.DownloadId); + var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem).First(); + + imported.Add(importResult); + + importedTrackedDownload.Add(new ManuallyImportedFile + { + TrackedDownload = trackedDownload, + ImportResult = importResult + }); + } + } + + _logger.ProgressInfo("Manually imported {0}", imported.Count); + + foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) + { + var trackedDownload = groupedTrackedDownload.First().TrackedDownload; + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) + { + if (_downloadedEpisodesImportService.ShouldDeleteFolder( + new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), + trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) + { + _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); + } + } + + if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + { + trackedDownload.State = TrackedDownloadStage.Imported; + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + } + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs new file mode 100644 index 000000000..32f904e4d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManuallyImportedFile.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Download.TrackedDownloads; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual +{ + public class ManuallyImportedFile + { + public TrackedDownload TrackedDownload { get; set; } + public ImportResult ImportResult { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs deleted file mode 100644 index d415f98d7..000000000 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NLog; - -namespace NzbDrone.Core.MediaFiles.EpisodeImport -{ - public class ManualImportService - { - public ManualImportService(Logger logger) - { - } - } -} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9c67b5506..a4eec048f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -578,8 +578,12 @@ - + + + + + diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index eefe76e51..648200bf4 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -9,6 +9,11 @@ namespace NzbDrone.Core.Parser.Model { public class LocalEpisode { + public LocalEpisode() + { + Episodes = new List(); + } + public String Path { get; set; } public Int64 Size { get; set; } public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } diff --git a/src/UI/Activity/Queue/QueueActionsCell.js b/src/UI/Activity/Queue/QueueActionsCell.js index 16dd5d7e5..e15102a48 100644 --- a/src/UI/Activity/Queue/QueueActionsCell.js +++ b/src/UI/Activity/Queue/QueueActionsCell.js @@ -11,9 +11,9 @@ module.exports = TemplatedCell.extend({ className : 'queue-actions-cell', events : { - 'click .x-remove' : '_remove', - 'click .x-import' : '_import', - 'click .x-grab' : '_grab' + 'click .x-remove' : '_remove', + 'click .x-manual-import' : '_manualImport', + 'click .x-grab' : '_grab' }, ui : { @@ -30,21 +30,12 @@ module.exports = TemplatedCell.extend({ })); }, - _import : function() { - var self = this; - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/queue/import', - type : 'POST', - data : JSON.stringify(this.model.toJSON()) - }); - - this.$(this.ui.import).spinForPromise(promise); - - promise.success(function() { - //find models that have the same series id and episode ids and remove them - self.model.trigger('destroy', self.model); - }); + _manualImport : function () { + vent.trigger(vent.Commands.ShowManualImport, + { + downloadId: this.model.get('downloadId'), + title: this.model.get('title') + }); }, _grab : function() { diff --git a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs b/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs index e7c0f6c2e..13bee034e 100644 --- a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs +++ b/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs @@ -1,6 +1,6 @@ {{#if_eq status compare="Completed"}} {{#if_eq trackedDownloadStatus compare="Warning"}} - + {{/if_eq}} {{/if_eq}} diff --git a/src/UI/AppLayout.js b/src/UI/AppLayout.js index 613cbcd06..862961423 100644 --- a/src/UI/AppLayout.js +++ b/src/UI/AppLayout.js @@ -1,6 +1,6 @@ var Marionette = require('marionette'); var ModalRegion = require('./Shared/Modal/ModalRegion'); -var FileBrowserModalRegion = require('./Shared/FileBrowser/FileBrowserModalRegion'); +var ModalRegion2 = require('./Shared/Modal/ModalRegion2'); var ControlPanelRegion = require('./Shared/ControlPanel/ControlPanelRegion'); var Layout = Marionette.Layout.extend({ @@ -11,9 +11,9 @@ var Layout = Marionette.Layout.extend({ initialize : function() { this.addRegions({ - modalRegion : ModalRegion, - fileBrowserModalRegion : FileBrowserModalRegion, - controlPanelRegion : ControlPanelRegion + modalRegion : ModalRegion, + modalRegion2 : ModalRegion2, + controlPanelRegion : ControlPanelRegion }); } }); diff --git a/src/UI/Cells/ApprovalStatusCellTemplate.hbs b/src/UI/Cells/ApprovalStatusCellTemplate.hbs index d039a1714..87f28cbcb 100644 --- a/src/UI/Cells/ApprovalStatusCellTemplate.hbs +++ b/src/UI/Cells/ApprovalStatusCellTemplate.hbs @@ -1,5 +1,11 @@
    {{#each this}} -
  • {{this}}
  • +
  • + {{#if reason}} + {{reason}} + {{else}} + {{this}} + {{/if}} +
  • {{/each}}
diff --git a/src/UI/Cells/Edit/QualityCellEditor.js b/src/UI/Cells/Edit/QualityCellEditor.js index 703512be7..57613e778 100644 --- a/src/UI/Cells/Edit/QualityCellEditor.js +++ b/src/UI/Cells/Edit/QualityCellEditor.js @@ -1,6 +1,6 @@ +var _ = require('underscore'); var Backgrid = require('backgrid'); var Marionette = require('marionette'); -var _ = require('underscore'); var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); module.exports = Backgrid.CellEditor.extend({ @@ -59,7 +59,11 @@ module.exports = Backgrid.CellEditor.extend({ }; model.set(column.get('name'), newQuality); - model.save(); + + if (this.column.get('saveAfterEdit')) { + model.save(); + } + model.trigger('backgrid:edited', model, column, new Backgrid.Command(e)); }, diff --git a/src/UI/Cells/TemplatedCell.js b/src/UI/Cells/TemplatedCell.js index 5f7525fdd..1299d4e36 100644 --- a/src/UI/Cells/TemplatedCell.js +++ b/src/UI/Cells/TemplatedCell.js @@ -7,9 +7,13 @@ module.exports = NzbDroneCell.extend({ var templateName = this.column.get('template') || this.template; this.templateFunction = Marionette.TemplateCache.get(templateName); - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); + this.$el.empty(); + + if (this.cellValue) { + var data = this.cellValue.toJSON(); + var html = this.templateFunction(data); + this.$el.html(html); + } this.delegateEvents(); return this; diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index b78da6c1a..e0dea4d3e 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -156,11 +156,12 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce } .queue-actions-cell { - min-width : 55px; - width : 55px; + min-width : 65px; + width : 65px; text-align : right !important; i { + .clickable(); margin-left : 1px; margin-right : 1px; } diff --git a/src/UI/Content/Bootstrap/bootstrap.less b/src/UI/Content/Bootstrap/bootstrap.less index ea9fa689b..765e0c4bc 100644 --- a/src/UI/Content/Bootstrap/bootstrap.less +++ b/src/UI/Content/Bootstrap/bootstrap.less @@ -23,7 +23,7 @@ @import "input-groups.less"; @import "navs.less"; @import "navbar.less"; -//@import "breadcrumbs.less"; +@import "breadcrumbs.less"; @import "pagination.less"; @import "pager.less"; @import "labels.less"; diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 97b80aece..994ada51d 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -156,6 +156,10 @@ .fa-icon-content(@fa-var-inbox); } +.icon-sonarr-import-manual { + .fa-icon-content(@fa-var-user); +} + .icon-sonarr-imported { .fa-icon-content(@fa-var-download); } diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 89507ba40..88c809c6a 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -19,6 +19,7 @@ @import "../Hotkeys/hotkeys"; @import "../Shared/FileBrowser/filebrowser"; @import "badges"; +@import "../ManualImport/manualimport"; .main-region { @media (min-width : @screen-lg-min) { diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js index 7dd727ac2..14ee5ca42 100644 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ b/src/UI/Episode/Search/EpisodeSearchLayout.js @@ -3,7 +3,6 @@ var Marionette = require('marionette'); var ButtonsView = require('./ButtonsView'); var ManualSearchLayout = require('./ManualLayout'); var ReleaseCollection = require('../../Release/ReleaseCollection'); -var SeriesCollection = require('../../Series/SeriesCollection'); var CommandController = require('../../Commands/CommandController'); var LoadingView = require('../../Shared/LoadingView'); var NoResultsView = require('./NoResultsView'); diff --git a/src/UI/ManualImport/Cells/EpisodesCell.js b/src/UI/ManualImport/Cells/EpisodesCell.js new file mode 100644 index 000000000..b06428394 --- /dev/null +++ b/src/UI/ManualImport/Cells/EpisodesCell.js @@ -0,0 +1,46 @@ +var _ = require('underscore'); +var vent = require('../../vent'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SelectEpisodeLayout = require('../Episode/SelectEpisodeLayout'); + +module.exports = NzbDroneCell.extend({ + className : 'episodes-cell editable', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + + var episodes = this.model.get('episodes'); + + if (episodes) + { + var episodeNumbers = _.map(episodes, 'episodeNumber'); + + this.$el.html(episodeNumbers.join(', ')); + } + + return this; + }, + + _onClick : function () { + var series = this.model.get('series'); + var seasonNumber = this.model.get('seasonNumber'); + + if (series === undefined || seasonNumber === undefined) { + return; + } + + var view = new SelectEpisodeLayout({ series: series, seasonNumber: seasonNumber }); + + this.listenTo(view, 'manualimport:selected:episodes', this._setEpisodes); + + vent.trigger(vent.Commands.OpenModal2Command, view); + }, + + _setEpisodes : function (e) { + this.model.set('episodes', e.episodes); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/PathCell.js b/src/UI/ManualImport/Cells/PathCell.js new file mode 100644 index 000000000..7397d1623 --- /dev/null +++ b/src/UI/ManualImport/Cells/PathCell.js @@ -0,0 +1,16 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'path-cell', + + render : function() { + this.$el.empty(); + + var relativePath = this.model.get('relativePath'); + var path = this.model.get('path'); + + this.$el.html('
{1}
'.format(path, relativePath)); + + return this; + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/QualityCell.js b/src/UI/ManualImport/Cells/QualityCell.js new file mode 100644 index 000000000..181ebf254 --- /dev/null +++ b/src/UI/ManualImport/Cells/QualityCell.js @@ -0,0 +1,23 @@ +var vent = require('../../vent'); +var QualityCell = require('../../Cells/QualityCell'); +var SelectQualityLayout = require('../Quality/SelectQualityLayout'); + +module.exports = QualityCell.extend({ + className : 'quality-cell editable', + + events : { + 'click' : '_onClick' + }, + + _onClick : function () { + var view = new SelectQualityLayout(); + + this.listenTo(view, 'manualimport:selected:quality', this._setQuality); + + vent.trigger(vent.Commands.OpenModal2Command, view); + }, + + _setQuality : function (e) { + this.model.set('quality', e.quality); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeasonCell.js b/src/UI/ManualImport/Cells/SeasonCell.js new file mode 100644 index 000000000..80f71df5e --- /dev/null +++ b/src/UI/ManualImport/Cells/SeasonCell.js @@ -0,0 +1,47 @@ +var vent = require('../../vent'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SelectSeasonLayout = require('../Season/SelectSeasonLayout'); + +module.exports = NzbDroneCell.extend({ + className : 'season-cell editable', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + + if (this.model.has('seasonNumber')) { + this.$el.html(this.model.get('seasonNumber')); + } + + this.delegateEvents(); + return this; + }, + + _onClick : function () { + var series = this.model.get('series'); + + if (!series) { + return; + } + + var view = new SelectSeasonLayout({ seasons: series.seasons }); + + this.listenTo(view, 'manualimport:selected:season', this._setSeason); + + vent.trigger(vent.Commands.OpenModal2Command, view); + }, + + _setSeason : function (e) { + if (this.model.has('seasonNumber') && e.seasonNumber === this.model.get('seasonNumber')) { + return; + } + + this.model.set({ + seasonNumber : e.seasonNumber, + episodes : [] + }); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeriesCell.js b/src/UI/ManualImport/Cells/SeriesCell.js new file mode 100644 index 000000000..cb66f6826 --- /dev/null +++ b/src/UI/ManualImport/Cells/SeriesCell.js @@ -0,0 +1,45 @@ +var vent = require('../../vent'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SelectSeriesLayout = require('../Series/SelectSeriesLayout'); + +module.exports = NzbDroneCell.extend({ + className : 'series-title-cell editable', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + + var series = this.model.get('series'); + + if (series) + { + this.$el.html(series.title); + } + + this.delegateEvents(); + return this; + }, + + _onClick : function () { + var view = new SelectSeriesLayout(); + + this.listenTo(view, 'manualimport:selected:series', this._setSeries); + + vent.trigger(vent.Commands.OpenModal2Command, view); + }, + + _setSeries : function (e) { + if (this.model.has('series') && e.model.id === this.model.get('series').id) { + return; + } + + this.model.set({ + series : e.model.toJSON(), + seasonNumber : undefined, + episodes : [] + }); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyView.js b/src/UI/ManualImport/EmptyView.js new file mode 100644 index 000000000..2b4394d3f --- /dev/null +++ b/src/UI/ManualImport/EmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'ManualImport/EmptyViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyViewTemplate.hbs b/src/UI/ManualImport/EmptyViewTemplate.hbs new file mode 100644 index 000000000..fe59eb600 --- /dev/null +++ b/src/UI/ManualImport/EmptyViewTemplate.hbs @@ -0,0 +1 @@ +No video files were found in the selected folder. \ No newline at end of file diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayout.js b/src/UI/ManualImport/Episode/SelectEpisodeLayout.js new file mode 100644 index 000000000..04617a0bc --- /dev/null +++ b/src/UI/ManualImport/Episode/SelectEpisodeLayout.js @@ -0,0 +1,81 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var EpisodeCollection = require('../../Series/EpisodeCollection'); +var LoadingView = require('../../Shared/LoadingView'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var SelectEpisodeRow = require('./SelectEpisodeRow'); + +module.exports = Marionette.Layout.extend({ + template : 'ManualImport/Episode/SelectEpisodeLayoutTemplate', + + regions : { + episodes : '.x-episodes' + }, + + events : { + 'click .x-select' : '_selectEpisodes' + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false + }, + { + name : 'episodeNumber', + label : '#', + cell : EpisodeNumberCell + }, + { + name : 'title', + label : 'Title', + hideSeriesLink : true, + cell : 'string', + sortable : false + }, + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell + } + ], + + initialize : function(options) { + this.series = options.series; + this.seasonNumber = options.seasonNumber; + }, + + onRender : function() { + this.episodes.show(new LoadingView()); + + this.episodeCollection = new EpisodeCollection({ seriesId : this.series.id }); + this.episodeCollection.fetch(); + + this.listenToOnce(this.episodeCollection, 'sync', function () { + + this.episodeView = new Backgrid.Grid({ + columns : this.columns, + collection : this.episodeCollection.bySeason(this.seasonNumber), + className : 'table table-hover season-grid', + row : SelectEpisodeRow + }); + + this.episodes.show(this.episodeView); + }); + }, + + _selectEpisodes : function () { + var episodes = _.map(this.episodeView.getSelectedModels(), function (episode) { + return episode.toJSON(); + }); + + this.trigger('manualimport:selected:episodes', { episodes: episodes }); + vent.trigger(vent.Commands.CloseModal2Command); + } +}); diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs b/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs new file mode 100644 index 000000000..9a7192882 --- /dev/null +++ b/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs @@ -0,0 +1,21 @@ + diff --git a/src/UI/ManualImport/Episode/SelectEpisodeRow.js b/src/UI/ManualImport/Episode/SelectEpisodeRow.js new file mode 100644 index 000000000..6dc90fc99 --- /dev/null +++ b/src/UI/ManualImport/Episode/SelectEpisodeRow.js @@ -0,0 +1,20 @@ +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Row.extend({ + className : 'select-episode-row', + + events : { + 'click' : '_toggle' + }, + + _toggle : function(e) { + + if (e.target.type === 'checkbox') { + return; + } + + var checked = this.$el.find('.select-row-cell :checkbox').prop('checked'); + + this.model.trigger('backgrid:select', this.model, !checked); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Folder/SelectFolderView.js b/src/UI/ManualImport/Folder/SelectFolderView.js new file mode 100644 index 000000000..97691a7e8 --- /dev/null +++ b/src/UI/ManualImport/Folder/SelectFolderView.js @@ -0,0 +1,49 @@ +var Marionette = require('marionette'); +require('../../Mixins/FileBrowser'); + +module.exports = Marionette.ItemView.extend({ + template : 'ManualImport/Folder/SelectFolderViewTemplate', + + ui : { + path : '.x-path', + buttons : '.x-button' + }, + + events: { + 'click .x-manual-import' : '_manualImport', + 'click .x-automatic-import' : '_automaticImport', + 'change .x-path' : '_updateButtons', + 'keyup .x-path' : '_updateButtons' + }, + + onRender : function() { + this.ui.path.fileBrowser(); + this._updateButtons(); + }, + + path : function() { + return this.ui.path.val(); + }, + + _manualImport : function () { + if (this.ui.path.val()) { + this.trigger('manualImport', { folder: this.ui.path.val() }); + } + }, + + _automaticImport : function () { + if (this.ui.path.val()) { + this.trigger('automaticImport', { folder: this.ui.path.val() }); + } + }, + + _updateButtons : function () { + if (this.ui.path.val()) { + this.ui.buttons.removeAttr('disabled'); + } + + else { + this.ui.buttons.attr('disabled', 'disabled'); + } + } +}); diff --git a/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs b/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs new file mode 100644 index 000000000..8e44e5042 --- /dev/null +++ b/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs @@ -0,0 +1,21 @@ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportCollection.js b/src/UI/ManualImport/ManualImportCollection.js new file mode 100644 index 000000000..c7cff70f7 --- /dev/null +++ b/src/UI/ManualImport/ManualImportCollection.js @@ -0,0 +1,74 @@ +var PageableCollection = require('backbone.pageable'); +var ManualImportModel = require('./ManualImportModel'); +var AsSortedCollection = require('../Mixins/AsSortedCollection'); + +var Collection = PageableCollection.extend({ + model : ManualImportModel, + url : window.NzbDrone.ApiRoot + '/manualimport', + + state : { + sortKey : 'quality', + order : 1, + pageSize : 100000 + }, + + mode : 'client', + + originalFetch : PageableCollection.prototype.fetch, + + initialize : function (options) { + options = options || {}; + + if (!options.folder && !options.downloadId) { + throw 'folder or downloadId is required'; + } + + this.folder = options.folder; + this.downloadId = options.downloadId; + }, + + fetch : function(options) { + options = options || {}; + + options.data = { folder : this.folder, downloadId : this.downloadId }; + + return this.originalFetch.call(this, options); + }, + + sortMappings : { + series : { + sortValue : function(model, attr, order) { + var series = model.get(attr); + + if (series) { + return series.sortTitle; + } + + return ''; + } + }, + + quality : { + sortKey : 'qualityWeight' + } + }, + + comparator : function(model1, model2) { + var quality1 = model1.get('quality'); + var quality2 = model2.get('quality'); + + if (quality1 < quality2) { + return 1; + } + + if (quality1 > quality2) { + return -1; + } + + return 0; + } +}); + +Collection = AsSortedCollection.call(Collection); + +module.exports = Collection; \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayout.js b/src/UI/ManualImport/ManualImportLayout.js new file mode 100644 index 000000000..992753d63 --- /dev/null +++ b/src/UI/ManualImport/ManualImportLayout.js @@ -0,0 +1,222 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var CommandController = require('../Commands/CommandController'); +var EmptyView = require('./EmptyView'); +var SelectFolderView = require('./Folder/SelectFolderView'); +var LoadingView = require('../Shared/LoadingView'); +var ManualImportRow = require('./ManualImportRow'); +var SelectAllCell = require('../Cells/SelectAllCell'); +var PathCell = require('./Cells/PathCell'); +var SeriesCell = require('./Cells/SeriesCell'); +var SeasonCell = require('./Cells/SeasonCell'); +var EpisodesCell = require('./Cells/EpisodesCell'); +var QualityCell = require('./Cells/QualityCell'); +var FileSizeCell = require('../Cells/FileSizeCell'); +var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); +var ManualImportCollection = require('./ManualImportCollection'); + +module.exports = Marionette.Layout.extend({ + className : 'modal-lg', + template : 'ManualImport/ManualImportLayoutTemplate', + + regions : { + workspace : '.x-workspace' + }, + + ui : { + importButton : '.x-import' + }, + + events : { + 'click .x-import' : '_import' + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false + }, + { + name : 'relativePath', + label : 'Relative Path', + cell : PathCell, + sortable : true + }, + { + name : 'series', + label : 'Series', + cell : SeriesCell, + sortable : true + }, + { + name : 'seasonNumber', + label : 'Season', + cell : SeasonCell, + sortable : true + }, + { + name : 'episodes', + label : 'Episode(s)', + cell : EpisodesCell, + sortable : false + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + sortable : true + + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell, + sortable : true + }, + { + name : 'rejections', + label : '', + tooltip : 'Rejections', + cell : ApprovalStatusCell, + sortable : false, + sortType : 'fixed', + direction : 'ascending' + } + ], + + initialize : function(options) { + this.folder = options.folder; + this.downloadId = options.downloadId; + this.title = options.title; + + //TODO: remove (just for testing) + this.folder = 'C:\\Test'; +// this.folder = 'E:\\X-Server'; + + this.templateHelpers = { + title : this.title || this.folder + }; + }, + + onRender : function() { + + if (this.folder || this.downloadId) { + this._showLoading(); + this._loadCollection(); + } + + else { + this._showSelectFolder(); + this.ui.importButton.hide(); + } + }, + + _showLoading : function () { + this.workspace.show(new LoadingView()); + }, + + _loadCollection : function () { + this.manualImportCollection = new ManualImportCollection({ folder: this.folder, downloadId: this.downloadId }); + this.manualImportCollection.fetch(); + + this.listenTo(this.manualImportCollection, 'sync', this._showTable); + this.listenTo(this.manualImportCollection, 'backgrid:selected', this._updateButtons); + }, + + _showTable : function () { + if (this.manualImportCollection.length === 0) { + this.workspace.show(new EmptyView()); + return; + } + + this.fileView = new Backgrid.Grid({ + columns : this.columns, + collection : this.manualImportCollection, + className : 'table table-hover', + row : ManualImportRow + }); + + this.workspace.show(this.fileView); + this._updateButtons(); + }, + + _showSelectFolder : function () { + this.selectFolderView = new SelectFolderView(); + this.workspace.show(this.selectFolderView); + + this.listenTo(this.selectFolderView, 'manualImport', this._manualImport); + this.listenTo(this.selectFolderView, 'automaticImport', this._automaticImport); + }, + + _manualImport : function (e) { + this.folder = e.folder; + this.templateHelpers.title = this.folder; + this.render(); + }, + + _automaticImport : function (e) { + CommandController.Execute('downloadedEpisodesScan', { + name : 'downloadedEpisodesScan', + path : e.folder + }); + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _import : function () { + var selected = this.fileView.getSelectedModels(); + + if (selected.length === 0) { + return; + } + + CommandController.Execute('manualImport', { + name : 'manualImport', + files : _.map(selected, function (file) { + return { + path : file.get('path'), + seriesId : file.get('series').id, + episodeIds : _.map(file.get('episodes'), 'id'), + quality : file.get('quality'), + downloadId : file.get('downloadId') + }; + }) + }); + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _updateButtons : function (model, selected) { + if (!this.fileView) { + this.ui.importButton.attr('disabled', 'disabled'); + return; + } + + if (!model) { + return; + } + + var selectedModels = this.fileView.getSelectedModels(); + var selectedCount = 0; + + if (selected) { + selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length : selectedModels.length + 1; + } + + else { + selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length - 1 : selectedModels.length; + } + + if (selectedCount === 0) { + this.ui.importButton.attr('disabled', 'disabled'); + } + + else { + this.ui.importButton.removeAttr('disabled'); + } + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayoutTemplate.hbs b/src/UI/ManualImport/ManualImportLayoutTemplate.hbs new file mode 100644 index 000000000..aa2ea6524 --- /dev/null +++ b/src/UI/ManualImport/ManualImportLayoutTemplate.hbs @@ -0,0 +1,20 @@ + diff --git a/src/UI/ManualImport/ManualImportModel.js b/src/UI/ManualImport/ManualImportModel.js new file mode 100644 index 000000000..dfd34cead --- /dev/null +++ b/src/UI/ManualImport/ManualImportModel.js @@ -0,0 +1,4 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({ +}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportRow.js b/src/UI/ManualImport/ManualImportRow.js new file mode 100644 index 000000000..1974df7bd --- /dev/null +++ b/src/UI/ManualImport/ManualImportRow.js @@ -0,0 +1,34 @@ +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Row.extend({ + className : 'manual-import-row', + + _originalInit : Backgrid.Row.prototype.initialize, + _originalRender : Backgrid.Row.prototype.render, + + initialize : function () { + this._originalInit.apply(this, arguments); + + this.listenTo(this.model, 'change', this._setError); + }, + + render : function () { + this._originalRender.apply(this, arguments); + this._setError(); + + return this; + }, + + _setError : function () { + if (this.model.has('series') && + this.model.has('seasonNumber') && + (this.model.has('episodes') && this.model.get('episodes').length > 0)&& + this.model.has('quality')) { + this.$el.removeClass('manual-import-error'); + } + + else { + this.$el.addClass('manual-import-error'); + } + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayout.js b/src/UI/ManualImport/Quality/SelectQualityLayout.js new file mode 100644 index 000000000..beba005e9 --- /dev/null +++ b/src/UI/ManualImport/Quality/SelectQualityLayout.js @@ -0,0 +1,43 @@ +var _ = require('underscore'); +var vent = require('../../vent'); +var Marionette = require('marionette'); +var LoadingView = require('../../Shared/LoadingView'); +var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); +var SelectQualityView = require('./SelectQualityView'); + +module.exports = Marionette.Layout.extend({ + template : 'ManualImport/Quality/SelectQualityLayoutTemplate', + + regions : { + quality : '.x-quality' + }, + + events : { + 'click .x-select' : '_selectQuality' + }, + + initialize : function() { + this.profileSchemaCollection = new ProfileSchemaCollection(); + this.profileSchemaCollection.fetch(); + + this.listenTo(this.profileSchemaCollection, 'sync', this._showQuality); + }, + + onRender : function() { + this.quality.show(new LoadingView()); + }, + + _showQuality : function () { + var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) { + return quality.quality; + }); + + this.selectQualityView = new SelectQualityView({ qualities: qualities }); + this.quality.show(this.selectQualityView); + }, + + _selectQuality : function () { + this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() }); + vent.trigger(vent.Commands.CloseModal2Command); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs new file mode 100644 index 000000000..f32dc7114 --- /dev/null +++ b/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs @@ -0,0 +1,19 @@ + diff --git a/src/UI/ManualImport/Quality/SelectQualityView.js b/src/UI/ManualImport/Quality/SelectQualityView.js new file mode 100644 index 000000000..8a39fab82 --- /dev/null +++ b/src/UI/ManualImport/Quality/SelectQualityView.js @@ -0,0 +1,37 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'ManualImport/Quality/SelectQualityViewTemplate', + + ui : { + select : '.x-select-quality', + proper : 'x-proper' + }, + + initialize : function(options) { + this.qualities = options.qualities; + + this.templateHelpers = { + qualities: this.qualities + }; + }, + + selectedQuality : function () { + var selected = parseInt(this.ui.select.val(), 10); + var proper = this.ui.proper.prop('checked'); + + var quality = _.find(this.qualities, function(q) { + return q.id === selected; + }); + + + return { + quality : quality, + revision : { + version : proper ? 2 : 1, + real : 0 + } + }; + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs new file mode 100644 index 000000000..a04342280 --- /dev/null +++ b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs @@ -0,0 +1,33 @@ +
+
+ + +
+ + +
+
+ +
+ + +
+
+
+
+
diff --git a/src/UI/ManualImport/Season/SelectSeasonLayout.js b/src/UI/ManualImport/Season/SelectSeasonLayout.js new file mode 100644 index 000000000..6f46f9cd9 --- /dev/null +++ b/src/UI/ManualImport/Season/SelectSeasonLayout.js @@ -0,0 +1,28 @@ +var vent = require('vent'); +var Marionette = require('marionette'); + +module.exports = Marionette.Layout.extend({ + template : 'ManualImport/Season/SelectSeasonLayoutTemplate', + + events : { + 'change .x-select-season' : '_selectSeason' + }, + + initialize : function(options) { + + this.templateHelpers = { + seasons : options.seasons + }; + }, + + _selectSeason : function (e) { + var seasonNumber = parseInt(e.target.value, 10); + + if (seasonNumber === -1) { + return; + } + + this.trigger('manualimport:selected:season', { seasonNumber: seasonNumber }); + vent.trigger(vent.Commands.CloseModal2Command); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs b/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs new file mode 100644 index 000000000..da26506a5 --- /dev/null +++ b/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs @@ -0,0 +1,29 @@ + + + diff --git a/src/UI/ManualImport/Series/SelectSeriesLayout.js b/src/UI/ManualImport/Series/SelectSeriesLayout.js new file mode 100644 index 000000000..ff4541a96 --- /dev/null +++ b/src/UI/ManualImport/Series/SelectSeriesLayout.js @@ -0,0 +1,92 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var SeriesCollection = require('../../Series/SeriesCollection'); +var SelectRow = require('./SelectSeriesRow'); + +module.exports = Marionette.Layout.extend({ + template : 'ManualImport/Series/SelectSeriesLayoutTemplate', + + regions : { + series : '.x-series' + }, + + ui : { + filter : '.x-filter' + }, + + columns : [ + { + name : 'title', + label : 'Title', + cell : 'String', + sortValue : 'sortTitle' + } + ], + + initialize : function() { + var self = this; + + this.seriesCollection = SeriesCollection.clone(); + + _.each(this.seriesCollection.models, function (model) { + model.collection = self.seriesCollection; + }); + + this.listenTo(this.seriesCollection, 'row:selected', this._onSelected); + }, + + onRender : function() { + this.seriesView = new Backgrid.Grid({ + columns : this.columns, + collection : this.seriesCollection, + className : 'table table-hover season-grid', + row : SelectRow + }); + + this.series.show(this.seriesView); + this._setupFilter(); + }, + + _setupFilter : function () { + var self = this; + + //TODO: This should be a mixin (same as Add Series searching) + this.ui.filter.keyup(function(e) { + if (_.contains([ + 9, + 16, + 17, + 18, + 19, + 20, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 91, + 92, + 93 + ], e.keyCode)) { + return; + } + + self._filter(self.ui.filter.val()); + }); + }, + + _filter : function (term) { + this.seriesCollection.setFilter(['title', term, 'contains']); + }, + + _onSelected : function (e) { + this.trigger('manualimport:selected:series', { model: e.model }); + + vent.trigger(vent.Commands.CloseModal2Command); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs b/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs new file mode 100644 index 000000000..826ebd2f2 --- /dev/null +++ b/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs @@ -0,0 +1,30 @@ + + + diff --git a/src/UI/ManualImport/Series/SelectSeriesRow.js b/src/UI/ManualImport/Series/SelectSeriesRow.js new file mode 100644 index 000000000..38a2d5ca6 --- /dev/null +++ b/src/UI/ManualImport/Series/SelectSeriesRow.js @@ -0,0 +1,13 @@ +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Row.extend({ + className : 'select-row select-series-row', + + events : { + 'click' : '_onClick' + }, + + _onClick : function() { + this.model.collection.trigger('row:selected', { model: this.model }); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryView.js b/src/UI/ManualImport/Summary/ManualImportSummaryView.js new file mode 100644 index 000000000..a4ab847c2 --- /dev/null +++ b/src/UI/ManualImport/Summary/ManualImportSummaryView.js @@ -0,0 +1,20 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'ManualImport/Summary/ManualImportSummaryViewTemplate', + + initialize : function (options) { + var episodes = _.map(options.episodes, function (episode) { + return episode.toJSON(); + }); + + this.templateHelpers = { + file : options.file, + series : options.series, + season : options.season, + episodes : episodes, + quality : options.quality + }; + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs new file mode 100644 index 000000000..d65ff52f1 --- /dev/null +++ b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs @@ -0,0 +1,19 @@ +
+ +
Path:
+
{{file}}
+ +
Series:
+
{{series.title}}
+ +
Season:
+
{{season.seasonNumber}}
+ + {{#each episodes}} +
Episode:
+
{{episodeNumber}} - {{title}}
+ {{/each}} + +
Quality:
+
{{quality.name}}
+
diff --git a/src/UI/ManualImport/manualimport.less b/src/UI/ManualImport/manualimport.less new file mode 100644 index 000000000..0d0b1b050 --- /dev/null +++ b/src/UI/ManualImport/manualimport.less @@ -0,0 +1,39 @@ +@import "../Shared/Styles/card.less"; +@import "../Shared/Styles/clickable.less"; +@import "../Content/Bootstrap/variables"; + +.manual-import-modal { + .path-cell { + word-break : break-all; + } + + .file-size-cell { + min-width : 80px; + } + + .editable { + .clickable(); + + .badge { + .clickable(); + } + } + + .select-row { + .clickable(); + } + + .select-folder { + .buttons { + margin-top: 20px; + + .row { + margin-top: 10px; + } + } + } + + .manual-import-error { + background-color : #fdefef; + } +} diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js index 2b7b92048..3c108c10f 100644 --- a/src/UI/Mixins/AsFilteredCollection.js +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -8,6 +8,7 @@ module.exports = function() { this.state.filterKey = filter[0]; this.state.filterValue = filter[1]; + this.state.filterType = filter[2] || 'equal'; if (options.reset) { if (this.mode !== 'server') { @@ -32,7 +33,11 @@ module.exports = function() { var filterModel = function(model) { if (!self.state.filterKey || !self.state.filterValue) { return true; - } else { + } + else if (self.state.filterType === 'contains') { + return model.get(self.state.filterKey).toLowerCase().indexOf(self.state.filterValue.toLowerCase()) > -1; + } + else { return model.get(self.state.filterKey) === self.state.filterValue; } }; diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayout.js b/src/UI/Shared/FileBrowser/FileBrowserLayout.js index 950c5512b..82ae8b32b 100644 --- a/src/UI/Shared/FileBrowser/FileBrowserLayout.js +++ b/src/UI/Shared/FileBrowser/FileBrowserLayout.js @@ -37,8 +37,9 @@ module.exports = Marionette.Layout.extend({ this.collection.showLastModified = options.showLastModified || false; this.input = options.input; this._setColumns(); - this.listenTo(this.collection, "sync", this._showGrid); - this.listenTo(this.collection, "filebrowser:folderselected", this._rowSelected); + this.listenTo(this.collection, 'sync', this._showGrid); + this.listenTo(this.collection, 'filebrowser:row:folderselected', this._rowSelected); + this.listenTo(this.collection, 'filebrowser:row:fileselected', this._fileSelected); }, onRender : function() { @@ -51,30 +52,30 @@ module.exports = Marionette.Layout.extend({ _setColumns : function() { this.columns = [ { - name : "type", - label : "", + name : 'type', + label : '', sortable : false, cell : FileBrowserTypeCell }, { - name : "name", - label : "Name", + name : 'name', + label : 'Name', sortable : false, cell : FileBrowserNameCell } ]; if (this.collection.showLastModified) { this.columns.push({ - name : "lastModified", - label : "Last Modified", + name : 'lastModified', + label : 'Last Modified', sortable : false, cell : RelativeDateCell }); } if (this.collection.showFiles) { this.columns.push({ - name : "size", - label : "Size", + name : 'size', + label : 'Size', sortable : false, cell : FileSizeCell }); @@ -100,17 +101,33 @@ module.exports = Marionette.Layout.extend({ row : FileBrowserRow, collection : this.collection, columns : this.columns, - className : "table table-hover" + className : 'table table-hover' }); this.browser.show(grid); }, _rowSelected : function(model) { - var path = model.get("path"); + var path = model.get('path'); + this._updatePath(path); this._fetchCollection(path); }, + _fileSelected : function(model) { + var path = model.get('path'); + var type = model.get('type'); + + this.input.val(path); + this.input.trigger('change'); + + this.input.trigger('filebrowser:fileselected', { + type : type, + path : path + }); + + vent.trigger(vent.Commands.CloseFileBrowser); + }, + _pathChanged : function(e, path) { this._fetchCollection(path.value); this._updatePath(path.value); @@ -118,7 +135,7 @@ module.exports = Marionette.Layout.extend({ _inputChanged : function() { var path = this.ui.path.val(); - if (path === "" || path.endsWith("\\") || path.endsWith("/")) { + if (path === '' || path.endsWith('\\') || path.endsWith('/')) { this._fetchCollection(path); } }, @@ -130,8 +147,16 @@ module.exports = Marionette.Layout.extend({ }, _selectPath : function() { - this.input.val(this.ui.path.val()); - this.input.trigger("change"); + var path = this.ui.path.val(); + + this.input.val(path); + this.input.trigger('change'); + + this.input.trigger('filebrowser:folderselected', { + type: 'folder', + path: path + }); + vent.trigger(vent.Commands.CloseFileBrowser); } -}); \ No newline at end of file +}); diff --git a/src/UI/Shared/FileBrowser/FileBrowserRow.js b/src/UI/Shared/FileBrowser/FileBrowserRow.js index c891393d9..af982cf72 100644 --- a/src/UI/Shared/FileBrowser/FileBrowserRow.js +++ b/src/UI/Shared/FileBrowser/FileBrowserRow.js @@ -16,9 +16,9 @@ module.exports = Backgrid.Row.extend({ _selectRow : function() { if (this.model.get('type') === 'file') { - this.model.collection.trigger('filebrowser:fileselected', this.model); + this.model.collection.trigger('filebrowser:row:fileselected', this.model); } else { - this.model.collection.trigger('filebrowser:folderselected', this.model); + this.model.collection.trigger('filebrowser:row:folderselected', this.model); } } }); \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 624c255b2..ae5c1ec8c 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -7,18 +7,22 @@ var EpisodeDetailsLayout = require('../../Episode/EpisodeDetailsLayout'); var HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout'); var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView'); var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout'); +var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); module.exports = Marionette.AppRouter.extend({ initialize : function() { vent.on(vent.Commands.OpenModalCommand, this._openModal, this); vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); + vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); + vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); + vent.on(vent.Commands.ShowManualImport, this._showManualImport, this); vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); }, @@ -31,6 +35,14 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.modalRegion.closeModal(); }, + _openModal2 : function(view) { + AppLayout.modalRegion2.show(view); + }, + + _closeModal2 : function() { + AppLayout.modalRegion2.closeModal(); + }, + _editSeries : function(options) { var view = new EditSeriesView({ model : options.series }); AppLayout.modalRegion.show(view); @@ -65,12 +77,17 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.modalRegion.show(view); }, - _showFileBrowser : function(options) { - var view = new FileBrowserLayout(options); - AppLayout.fileBrowserModalRegion.show(view); + _showManualImport : function(options) { + var view = new ManualImportLayout(options); + AppLayout.modalRegion.show(view); }, - _closeFileBrowser : function(options) { - AppLayout.fileBrowserModalRegion.closeModal(); + _showFileBrowser : function(options) { + var view = new FileBrowserLayout(options); + AppLayout.modalRegion2.show(view); + }, + + _closeFileBrowser : function() { + AppLayout.modalRegion2.closeModal(); } }); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserModalRegion.js b/src/UI/Shared/Modal/ModalRegion2.js similarity index 97% rename from src/UI/Shared/FileBrowser/FileBrowserModalRegion.js rename to src/UI/Shared/Modal/ModalRegion2.js index 5ca1a446b..a6cc0b8f7 100644 --- a/src/UI/Shared/FileBrowser/FileBrowserModalRegion.js +++ b/src/UI/Shared/Modal/ModalRegion2.js @@ -4,7 +4,7 @@ var Marionette = require('marionette'); require('bootstrap'); var region = Marionette.Region.extend({ - el : '#file-browser-modal-region', + el : '#modal-region2', constructor : function() { Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); diff --git a/src/UI/Wanted/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js index 9ed0d1b84..e9318b7e2 100644 --- a/src/UI/Wanted/Missing/MissingLayout.js +++ b/src/UI/Wanted/Missing/MissingLayout.js @@ -1,5 +1,6 @@ var $ = require('jquery'); var _ = require('underscore'); +var vent = require('../../vent'); var Marionette = require('marionette'); var Backgrid = require('backgrid'); var MissingCollection = require('./MissingCollection'); @@ -13,6 +14,7 @@ var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); var LoadingView = require('../../Shared/LoadingView'); var Messenger = require('../../Shared/Messenger'); var CommandController = require('../../Commands/CommandController'); + require('backgrid.selectall'); require('../../Mixins/backbone.signalr.mixin'); @@ -128,11 +130,16 @@ module.exports = Marionette.Layout.extend({ route : 'seasonpass' }, { - title : 'Rescan Drone Factory Folder', - icon : 'icon-sonarr-refresh', - command : 'downloadedepisodesscan', - + title : 'Rescan Drone Factory Folder', + icon : 'icon-sonarr-refresh', + command : 'downloadedepisodesscan', properties : { sendUpdates : true } + }, + { + title : 'Manual Import', + icon : 'icon-sonarr-search-manual', + callback : this._manualImport, + ownerContext : this } ] }; @@ -172,6 +179,7 @@ module.exports = Marionette.Layout.extend({ command : { name : 'missingEpisodeSearch' } }); }, + _setFilter : function(buttonContext) { var mode = buttonContext.model.get('key'); this.collection.state.currentPage = 1; @@ -180,6 +188,7 @@ module.exports = Marionette.Layout.extend({ buttonContext.ui.icon.spinForPromise(promise); } }, + _searchSelected : function() { var selected = this.missingGrid.getSelectedModels(); if (selected.length === 0) { @@ -223,5 +232,8 @@ module.exports = Marionette.Layout.extend({ $.when(promises).done(function () { self.collection.fetch(); }); + }, + _manualImport : function () { + vent.trigger(vent.Commands.ShowManualImport); } }); \ No newline at end of file diff --git a/src/UI/index.html b/src/UI/index.html index aae175128..dd61058f1 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -52,7 +52,7 @@
-
+