You've already forked Sonarr
							
							
				mirror of
				https://github.com/Sonarr/Sonarr.git
				synced 2025-10-31 00:07:55 +02:00 
			
		
		
		
	New: Manual Import episodes
This commit is contained in:
		| @@ -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', | ||||
|   | ||||
| @@ -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 @@ namespace NzbDrone.Api.FileSystem | ||||
| 
 | ||||
|             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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/NzbDrone.Api/ManualImport/ManualImportModule.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/NzbDrone.Api/ManualImport/ManualImportModule.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ManualImportResource> | ||||
|     { | ||||
|         private readonly IManualImportService _manualImportService; | ||||
| 
 | ||||
|         public ManualImportModule(IManualImportService manualImportService) | ||||
|             : base("/manualimport") | ||||
|         { | ||||
|             _manualImportService = manualImportService; | ||||
| 
 | ||||
|             GetResourceAll = GetMediaFiles; | ||||
|         } | ||||
| 
 | ||||
|         private List<ManualImportResource> 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/NzbDrone.Api/ManualImport/ManualImportResource.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/NzbDrone.Api/ManualImport/ManualImportResource.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<EpisodeResource> Episodes { get; set; } | ||||
|         public QualityModel Quality { get; set; } | ||||
|         public int QualityWeight { get; set; } | ||||
|         public string DownloadId { get; set; } | ||||
|         public IEnumerable<Rejection> Rejections { get; set; } | ||||
| 
 | ||||
|         public int Id | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 return Path.GetHashCode(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -102,6 +102,8 @@ | ||||
|     <Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" /> | ||||
|     <Compile Include="Parse\ParseModule.cs" /> | ||||
|     <Compile Include="Parse\ParseResource.cs" /> | ||||
|     <Compile Include="ManualImport\ManualImportModule.cs" /> | ||||
|     <Compile Include="ManualImport\ManualImportResource.cs" /> | ||||
|     <Compile Include="Profiles\Delay\DelayProfileModule.cs" /> | ||||
|     <Compile Include="Profiles\Delay\DelayProfileResource.cs" /> | ||||
|     <Compile Include="Profiles\Delay\DelayProfileValidator.cs" /> | ||||
|   | ||||
							
								
								
									
										14
									
								
								src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/NzbDrone.Common/Disk/RelativeFileSystemModel.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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; } | ||||
|     } | ||||
| } | ||||
| @@ -72,6 +72,7 @@ | ||||
|     <Compile Include="ConvertBase32.cs" /> | ||||
|     <Compile Include="Crypto\HashProvider.cs" /> | ||||
|     <Compile Include="Disk\FileSystemLookupService.cs" /> | ||||
|     <Compile Include="Disk\RelativeFileSystemModel.cs" /> | ||||
|     <Compile Include="Disk\FileSystemModel.cs" /> | ||||
|     <Compile Include="Disk\FileSystemResult.cs" /> | ||||
|     <Compile Include="Extensions\DictionaryExtensions.cs" /> | ||||
|   | ||||
| @@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests | ||||
|         [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)]
 | ||||
|   | ||||
| @@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles | ||||
|     { | ||||
|         List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo); | ||||
|         List<ImportResult> ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null); | ||||
|         bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series); | ||||
|     } | ||||
| 
 | ||||
|     public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService | ||||
| @@ -98,6 +99,41 @@ namespace NzbDrone.Core.MediaFiles | ||||
|             return new List<ImportResult>(); | ||||
|         } | ||||
| 
 | ||||
|         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<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null) | ||||
|         { | ||||
|             var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); | ||||
| @@ -206,41 +242,6 @@ namespace NzbDrone.Core.MediaFiles | ||||
|             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); | ||||
|   | ||||
| @@ -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<ManualImportFile> Files { get; set; } | ||||
|     } | ||||
| } | ||||
| @@ -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<int> EpisodeIds { get; set; } | ||||
|         public QualityModel Quality { get; set; } | ||||
|         public string DownloadId { get; set; } | ||||
|     } | ||||
| } | ||||
| @@ -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<Episode> Episodes { get; set; } | ||||
|         public QualityModel Quality { get; set; } | ||||
|         public string DownloadId { get; set; } | ||||
|         public IEnumerable<Rejection> Rejections { get; set; } | ||||
|     } | ||||
| } | ||||
| @@ -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<ManualImportItem> GetMediaFiles(string path, string downloadId); | ||||
|     } | ||||
| 
 | ||||
|     public class ManualImportService : IExecute<ManualImportCommand>, 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<ManualImportItem> GetMediaFiles(string path, string downloadId) | ||||
|         { | ||||
|             if (downloadId.IsNotNullOrWhiteSpace()) | ||||
|             { | ||||
|                 var trackedDownload = _trackedDownloadService.Find(downloadId); | ||||
| 
 | ||||
|                 if (trackedDownload == null) | ||||
|                 { | ||||
|                     return new List<ManualImportItem>(); | ||||
|                 } | ||||
| 
 | ||||
|                 path = trackedDownload.DownloadItem.OutputPath.FullPath; | ||||
|             } | ||||
| 
 | ||||
|             if (!_diskProvider.FolderExists(path)) | ||||
|             { | ||||
|                 if (!_diskProvider.FileExists(path)) | ||||
|                 { | ||||
|                     return new List<ManualImportItem>(); | ||||
|                 } | ||||
| 
 | ||||
|                 return new List<ManualImportItem> { ProcessFile(path, downloadId) }; | ||||
|             } | ||||
| 
 | ||||
|             return ProcessFolder(path, downloadId); | ||||
|         } | ||||
| 
 | ||||
|         private List<ManualImportItem> 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<string> {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<ImportResult>(); | ||||
|             var importedTrackedDownload = new List<ManuallyImportedFile>(); | ||||
| 
 | ||||
|             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> { importDecision }, !existingFile));                     | ||||
|                 } | ||||
| 
 | ||||
|                 else | ||||
|                 { | ||||
|                     var trackedDownload = _trackedDownloadService.Find(file.DownloadId); | ||||
|                     var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { 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)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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; } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -578,8 +578,12 @@ | ||||
|     <Compile Include="MediaFiles\EpisodeImport\ImportDecision.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\ManualImportService.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Manual\ManualImportFile.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Manual\ManualImportCommand.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Manual\ManualImportItem.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Manual\ManualImportService.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\DetectSample.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Manual\ManuallyImportedFile.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecification.cs" /> | ||||
|     <Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" /> | ||||
|   | ||||
| @@ -9,6 +9,11 @@ namespace NzbDrone.Core.Parser.Model | ||||
| { | ||||
|     public class LocalEpisode | ||||
|     { | ||||
|         public LocalEpisode() | ||||
|         { | ||||
|             Episodes = new List<Episode>(); | ||||
|         } | ||||
| 
 | ||||
|         public String Path { get; set; } | ||||
|         public Int64 Size { get; set; } | ||||
|         public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {{#if_eq status compare="Completed"}} | ||||
|     {{#if_eq trackedDownloadStatus compare="Warning"}} | ||||
|         <i class="icon-sonarr-import x-import" title="Force import"></i> | ||||
|         <i class="icon-sonarr-import-manual x-manual-import" title="Manual import"></i> | ||||
|     {{/if_eq}} | ||||
| {{/if_eq}} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| <ul> | ||||
|     {{#each this}} | ||||
|     <li>{{this}}</li> | ||||
|     <li> | ||||
|         {{#if reason}} | ||||
|             {{reason}} | ||||
|         {{else}} | ||||
|             {{this}} | ||||
|         {{/if}} | ||||
|     </li> | ||||
|     {{/each}} | ||||
| </ul> | ||||
|   | ||||
| @@ -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)); | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/UI/Content/Bootstrap/bootstrap.less
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/UI/Content/Bootstrap/bootstrap.less
									
									
									
									
										vendored
									
									
								
							| @@ -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"; | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
| @import "../Hotkeys/hotkeys"; | ||||
| @import "../Shared/FileBrowser/filebrowser"; | ||||
| @import "badges"; | ||||
| @import "../ManualImport/manualimport"; | ||||
|  | ||||
| .main-region { | ||||
|   @media (min-width : @screen-lg-min) { | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/UI/ManualImport/Cells/EpisodesCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/UI/ManualImport/Cells/EpisodesCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										16
									
								
								src/UI/ManualImport/Cells/PathCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/UI/ManualImport/Cells/PathCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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('<div title="{0}">{1}</div>'.format(path, relativePath)); | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										23
									
								
								src/UI/ManualImport/Cells/QualityCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/UI/ManualImport/Cells/QualityCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										47
									
								
								src/UI/ManualImport/Cells/SeasonCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/UI/ManualImport/Cells/SeasonCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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     : [] | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										45
									
								
								src/UI/ManualImport/Cells/SeriesCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/UI/ManualImport/Cells/SeriesCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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     : [] | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										5
									
								
								src/UI/ManualImport/EmptyView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/UI/ManualImport/EmptyView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| var Marionette = require('marionette'); | ||||
|  | ||||
| module.exports = Marionette.CompositeView.extend({ | ||||
|     template : 'ManualImport/EmptyViewTemplate' | ||||
| }); | ||||
							
								
								
									
										1
									
								
								src/UI/ManualImport/EmptyViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/UI/ManualImport/EmptyViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| No video files were found in the selected folder. | ||||
							
								
								
									
										81
									
								
								src/UI/ManualImport/Episode/SelectEpisodeLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/UI/ManualImport/Episode/SelectEpisodeLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										21
									
								
								src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="manual-import-modal"> | ||||
|         <div class="modal-header"> | ||||
|             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||||
|  | ||||
|             <h3> | ||||
|                 Manual Import - Select Episode(s) | ||||
|             </h3> | ||||
|  | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-12 x-episodes"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button class="btn btn-default" data-dismiss="modal">cancel</button> | ||||
|             <button class="btn btn-success x-select" data-dismiss="modal">select episodes</button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										20
									
								
								src/UI/ManualImport/Episode/SelectEpisodeRow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/UI/ManualImport/Episode/SelectEpisodeRow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										49
									
								
								src/UI/ManualImport/Folder/SelectFolderView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/UI/ManualImport/Folder/SelectFolderView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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'); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										21
									
								
								src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <div class="select-folder"> | ||||
|     <div class="row"> | ||||
|         <div class="form-group"> | ||||
|             <div class="col-md-12"> | ||||
|                 <input type="text" class="form-control x-path" placeholder="Select a folder to import" name="path"> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="buttons"> | ||||
|         <div class="row"> | ||||
|             <div class="col-md-4 col-md-offset-4"> | ||||
|                 <button class="btn btn-primary btn-lg btn-block x-automatic-import x-button"><i class="icon-sonarr-search-automatic"></i> Import File(s) Automatically</button> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="row"> | ||||
|             <div class="col-md-4 col-md-offset-4"> | ||||
|                 <button class="btn btn-primary btn-lg btn-block x-manual-import x-button"><i class="icon-sonarr-search-manual"></i> Manual Import</button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										74
									
								
								src/UI/ManualImport/ManualImportCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/UI/ManualImport/ManualImportCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
							
								
								
									
										222
									
								
								src/UI/ManualImport/ManualImportLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/UI/ManualImport/ManualImportLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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      : '<i class="icon-sonarr-header-rejections" />', | ||||
|             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'); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										20
									
								
								src/UI/ManualImport/ManualImportLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/UI/ManualImport/ManualImportLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="manual-import-modal"> | ||||
|         <div class="modal-header"> | ||||
|             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||||
|  | ||||
|             <h3> | ||||
|                 Manual Import - {{#if title}}{{title}}{{else}}Select Folder{{/if}} | ||||
|             </h3> | ||||
|  | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="x-workspace"></div> | ||||
|             <div class="x-footer"></div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button class="btn btn-default" data-dismiss="modal">cancel</button> | ||||
|             <button class="btn btn-success x-import" disabled="disabled">import</button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										4
									
								
								src/UI/ManualImport/ManualImportModel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/UI/ManualImport/ManualImportModel.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| var Backbone = require('backbone'); | ||||
|  | ||||
| module.exports = Backbone.Model.extend({ | ||||
| }); | ||||
							
								
								
									
										34
									
								
								src/UI/ManualImport/ManualImportRow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/UI/ManualImport/ManualImportRow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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'); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										43
									
								
								src/UI/ManualImport/Quality/SelectQualityLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/UI/ManualImport/Quality/SelectQualityLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										19
									
								
								src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="manual-import-modal"> | ||||
|         <div class="modal-header"> | ||||
|             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||||
|  | ||||
|             <h3> | ||||
|                 Manual Import - Select Quality | ||||
|             </h3> | ||||
|  | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="x-quality"></div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button class="btn btn-default" data-dismiss="modal">cancel</button> | ||||
|             <button class="btn btn-success x-select" data-dismiss="modal">select quality</button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										37
									
								
								src/UI/ManualImport/Quality/SelectQualityView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/UI/ManualImport/Quality/SelectQualityView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										33
									
								
								src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| <div class="form-horizontal"> | ||||
|     <div class="form-group"> | ||||
|         <label class="col-sm-4 control-label">Quality</label> | ||||
|  | ||||
|         <div class="col-sm-4"> | ||||
|             <select class="form-control x-select-quality"> | ||||
|                 <option value="-1">Select Quality</option> | ||||
|                 {{#each qualities}} | ||||
|                     <option value="{{id}}">{{name}}</option> | ||||
|                 {{/each}} | ||||
|             </select> | ||||
|  | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="form-group"> | ||||
|         <label class="col-sm-4 control-label">Proper</label> | ||||
|  | ||||
|         <div class="col-sm-8"> | ||||
|             <div class="input-group"> | ||||
|                 <label class="checkbox toggle well"> | ||||
|                     <input type="checkbox" class="x-proper"/> | ||||
|                     <p> | ||||
|                         <span>Yes</span> | ||||
|                         <span>No</span> | ||||
|                     </p> | ||||
|  | ||||
|                     <div class="btn btn-primary slide-button"/> | ||||
|                 </label> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										28
									
								
								src/UI/ManualImport/Season/SelectSeasonLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/UI/ManualImport/Season/SelectSeasonLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										29
									
								
								src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="manual-import-modal"> | ||||
|         <div class="modal-header"> | ||||
|             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||||
|  | ||||
|             <h3> | ||||
|                 Manual Import - Select Season | ||||
|             </h3> | ||||
|  | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="row"> | ||||
|                 <div class="form-group col-md-4 col-md-offset-4"> | ||||
|                     <select class="form-control x-select-season"> | ||||
|                             <option value="-1">Select Season</option> | ||||
|                         {{#each seasons}} | ||||
|                             <option value="{{seasonNumber}}">Season {{seasonNumber}}</option> | ||||
|                         {{/each}} | ||||
|                     </select> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button class="btn btn-default" data-dismiss="modal">cancel</button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
							
								
								
									
										92
									
								
								src/UI/ManualImport/Series/SelectSeriesLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/UI/ManualImport/Series/SelectSeriesLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										30
									
								
								src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <div class="modal-content"> | ||||
|     <div class="manual-import-modal"> | ||||
|         <div class="modal-header"> | ||||
|             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> | ||||
|  | ||||
|             <h3> | ||||
|                 Manual Import - Select Series | ||||
|             </h3> | ||||
|  | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-12"> | ||||
|                     <div class="form-group"> | ||||
|                         <input type="text" class="form-control x-filter" placeholder="Filter series" /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-12 x-series"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="modal-footer"> | ||||
|             <button class="btn btn-default" data-dismiss="modal">cancel</button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
							
								
								
									
										13
									
								
								src/UI/ManualImport/Series/SelectSeriesRow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/UI/ManualImport/Series/SelectSeriesRow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										20
									
								
								src/UI/ManualImport/Summary/ManualImportSummaryView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/UI/ManualImport/Summary/ManualImportSummaryView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
| @@ -0,0 +1,19 @@ | ||||
| <dl class="dl-horizontal"> | ||||
|  | ||||
|     <dt>Path:</dt> | ||||
|     <dd>{{file}}</dd> | ||||
|  | ||||
|     <dt>Series:</dt> | ||||
|     <dd>{{series.title}}</dd> | ||||
|  | ||||
|     <dt>Season:</dt> | ||||
|     <dd>{{season.seasonNumber}}</dd> | ||||
|  | ||||
|     {{#each episodes}} | ||||
|         <dt>Episode:</dt> | ||||
|         <dd>{{episodeNumber}} - {{title}}</dd> | ||||
|     {{/each}} | ||||
|  | ||||
|     <dt>Quality:</dt> | ||||
|     <dd>{{quality.name}}</dd> | ||||
| </dl> | ||||
							
								
								
									
										39
									
								
								src/UI/ManualImport/manualimport.less
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/UI/ManualImport/manualimport.less
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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; | ||||
|   } | ||||
| } | ||||
| @@ -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; | ||||
|             } | ||||
|         }; | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| }); | ||||
| }); | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| @@ -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(); | ||||
|     } | ||||
| }); | ||||
| @@ -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); | ||||
| @@ -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); | ||||
|     } | ||||
| }); | ||||
| @@ -52,7 +52,7 @@ | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div id="modal-region"></div> | ||||
|             <div id="file-browser-modal-region"></div> | ||||
|             <div id="modal-region2"></div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <a id="scroll-up" title="Back to the top!"> | ||||
|   | ||||
| @@ -48,8 +48,6 @@ | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div id="modal-region"></div> | ||||
|             <div id="file-browser-modal-region"></div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
| @@ -15,12 +15,15 @@ vent.Commands = { | ||||
|     DeleteSeriesCommand      : 'DeleteSeriesCommand', | ||||
|     OpenModalCommand         : 'OpenModalCommand', | ||||
|     CloseModalCommand        : 'CloseModalCommand', | ||||
|     OpenModal2Command        : 'OpenModal2Command', | ||||
|     CloseModal2Command       : 'CloseModal2Command', | ||||
|     ShowEpisodeDetails       : 'ShowEpisodeDetails', | ||||
|     ShowHistoryDetails       : 'ShowHistoryDetails', | ||||
|     ShowLogDetails           : 'ShowLogDetails', | ||||
|     SaveSettings             : 'saveSettings', | ||||
|     ShowLogFile              : 'showLogFile', | ||||
|     ShowRenamePreview        : 'showRenamePreview', | ||||
|     ShowManualImport         : 'showManualImport', | ||||
|     ShowFileBrowser          : 'showFileBrowser', | ||||
|     CloseFileBrowser         : 'closeFileBrowser', | ||||
|     OpenControlPanelCommand  : 'OpenControlPanelCommand', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user