mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-04 10:02:40 +01:00
New: Manual Import episodes
This commit is contained in:
parent
29ca1bc9da
commit
6dd22e7dcb
@ -15,6 +15,7 @@ gulp.task('less', function () {
|
|||||||
paths.src.root + 'AddSeries/addSeries.less',
|
paths.src.root + 'AddSeries/addSeries.less',
|
||||||
paths.src.root + 'Calendar/calendar.less',
|
paths.src.root + 'Calendar/calendar.less',
|
||||||
paths.src.root + 'Cells/cells.less',
|
paths.src.root + 'Cells/cells.less',
|
||||||
|
paths.src.root + 'ManualImport/manualimport.less',
|
||||||
paths.src.root + 'Settings/settings.less',
|
paths.src.root + 'Settings/settings.less',
|
||||||
paths.src.root + 'System/Logs/logs.less',
|
paths.src.root + 'System/Logs/logs.less',
|
||||||
paths.src.root + 'System/Update/update.less',
|
paths.src.root + 'System/Update/update.less',
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using NzbDrone.Api.Extensions;
|
using NzbDrone.Api.Extensions;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
|
||||||
namespace NzbDrone.Api.FileSystem
|
namespace NzbDrone.Api.FileSystem
|
||||||
{
|
{
|
||||||
public class FileSystemModule : NzbDroneApiModule
|
public class FileSystemModule : NzbDroneApiModule
|
||||||
{
|
{
|
||||||
private readonly IFileSystemLookupService _fileSystemLookupService;
|
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")
|
: base("/filesystem")
|
||||||
{
|
{
|
||||||
_fileSystemLookupService = fileSystemLookupService;
|
_fileSystemLookupService = fileSystemLookupService;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_diskScanService = diskScanService;
|
||||||
Get["/"] = x => GetContents();
|
Get["/"] = x => GetContents();
|
||||||
|
Get["/type"] = x => GetEntityType();
|
||||||
|
Get["/mediafiles"] = x => GetMediaFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response GetContents()
|
private Response GetContents()
|
||||||
@ -29,5 +41,36 @@ private Response GetContents()
|
|||||||
|
|
||||||
return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse();
|
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="Frontend\Mappers\LoginHtmlMapper.cs" />
|
||||||
<Compile Include="Parse\ParseModule.cs" />
|
<Compile Include="Parse\ParseModule.cs" />
|
||||||
<Compile Include="Parse\ParseResource.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\DelayProfileModule.cs" />
|
||||||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||||
<Compile Include="Profiles\Delay\DelayProfileValidator.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="ConvertBase32.cs" />
|
||||||
<Compile Include="Crypto\HashProvider.cs" />
|
<Compile Include="Crypto\HashProvider.cs" />
|
||||||
<Compile Include="Disk\FileSystemLookupService.cs" />
|
<Compile Include="Disk\FileSystemLookupService.cs" />
|
||||||
|
<Compile Include="Disk\RelativeFileSystemModel.cs" />
|
||||||
<Compile Include="Disk\FileSystemModel.cs" />
|
<Compile Include="Disk\FileSystemModel.cs" />
|
||||||
<Compile Include="Disk\FileSystemResult.cs" />
|
<Compile Include="Disk\FileSystemResult.cs" />
|
||||||
<Compile Include="Extensions\DictionaryExtensions.cs" />
|
<Compile Include="Extensions\DictionaryExtensions.cs" />
|
||||||
|
@ -77,6 +77,7 @@ public class AbsoluteEpisodeNumberParserFixture : CoreTest
|
|||||||
[TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep08_(8246e542).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 8, 0, 0)]
|
[TestCase("[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("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("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) - 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("[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)]
|
[TestCase("Dragon Ball Kai (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Dragon Ball Kai 2014", 39, 0, 0)]
|
||||||
|
@ -17,6 +17,7 @@ public interface IDownloadedEpisodesImportService
|
|||||||
{
|
{
|
||||||
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
|
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
|
||||||
List<ImportResult> ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null);
|
List<ImportResult> ProcessPath(string path, Series series = null, DownloadClientItem downloadClientItem = null);
|
||||||
|
bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService
|
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService
|
||||||
@ -98,6 +99,41 @@ public List<ImportResult> ProcessPath(string path, Series series = null, Downloa
|
|||||||
return new List<ImportResult>();
|
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)
|
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null)
|
||||||
{
|
{
|
||||||
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
|
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
|
||||||
@ -206,41 +242,6 @@ private string GetCleanedUpFolderName(string folder)
|
|||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series)
|
|
||||||
{
|
|
||||||
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
|
|
||||||
var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar");
|
|
||||||
|
|
||||||
foreach (var videoFile in videoFiles)
|
|
||||||
{
|
|
||||||
var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile));
|
|
||||||
|
|
||||||
if (episodeParseResult == null)
|
|
||||||
{
|
|
||||||
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var size = _diskProvider.GetFileSize(videoFile);
|
|
||||||
var quality = QualityParser.ParseQuality(videoFile);
|
|
||||||
|
|
||||||
if (!_detectSample.IsSample(series, quality, videoFile, size,
|
|
||||||
episodeParseResult.SeasonNumber))
|
|
||||||
{
|
|
||||||
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes()))
|
|
||||||
{
|
|
||||||
_logger.Warn("RAR file detected, will require manual cleanup");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImportResult FileIsLockedResult(string videoFile)
|
private ImportResult FileIsLockedResult(string videoFile)
|
||||||
{
|
{
|
||||||
_logger.Debug("[{0}] is currently locked by another process, skipping", 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\ImportDecision.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\ImportResultType.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\DetectSample.cs" />
|
||||||
|
<Compile Include="MediaFiles\EpisodeImport\Manual\ManuallyImportedFile.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecification.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecification.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
|
||||||
|
@ -9,6 +9,11 @@ namespace NzbDrone.Core.Parser.Model
|
|||||||
{
|
{
|
||||||
public class LocalEpisode
|
public class LocalEpisode
|
||||||
{
|
{
|
||||||
|
public LocalEpisode()
|
||||||
|
{
|
||||||
|
Episodes = new List<Episode>();
|
||||||
|
}
|
||||||
|
|
||||||
public String Path { get; set; }
|
public String Path { get; set; }
|
||||||
public Int64 Size { get; set; }
|
public Int64 Size { get; set; }
|
||||||
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
||||||
|
@ -11,9 +11,9 @@ module.exports = TemplatedCell.extend({
|
|||||||
className : 'queue-actions-cell',
|
className : 'queue-actions-cell',
|
||||||
|
|
||||||
events : {
|
events : {
|
||||||
'click .x-remove' : '_remove',
|
'click .x-remove' : '_remove',
|
||||||
'click .x-import' : '_import',
|
'click .x-manual-import' : '_manualImport',
|
||||||
'click .x-grab' : '_grab'
|
'click .x-grab' : '_grab'
|
||||||
},
|
},
|
||||||
|
|
||||||
ui : {
|
ui : {
|
||||||
@ -30,21 +30,12 @@ module.exports = TemplatedCell.extend({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
_import : function() {
|
_manualImport : function () {
|
||||||
var self = this;
|
vent.trigger(vent.Commands.ShowManualImport,
|
||||||
|
{
|
||||||
var promise = $.ajax({
|
downloadId: this.model.get('downloadId'),
|
||||||
url : window.NzbDrone.ApiRoot + '/queue/import',
|
title: this.model.get('title')
|
||||||
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);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_grab : function() {
|
_grab : function() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{#if_eq status compare="Completed"}}
|
{{#if_eq status compare="Completed"}}
|
||||||
{{#if_eq trackedDownloadStatus compare="Warning"}}
|
{{#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}}
|
||||||
{{/if_eq}}
|
{{/if_eq}}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
var Marionette = require('marionette');
|
var Marionette = require('marionette');
|
||||||
var ModalRegion = require('./Shared/Modal/ModalRegion');
|
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 ControlPanelRegion = require('./Shared/ControlPanel/ControlPanelRegion');
|
||||||
|
|
||||||
var Layout = Marionette.Layout.extend({
|
var Layout = Marionette.Layout.extend({
|
||||||
@ -11,9 +11,9 @@ var Layout = Marionette.Layout.extend({
|
|||||||
|
|
||||||
initialize : function() {
|
initialize : function() {
|
||||||
this.addRegions({
|
this.addRegions({
|
||||||
modalRegion : ModalRegion,
|
modalRegion : ModalRegion,
|
||||||
fileBrowserModalRegion : FileBrowserModalRegion,
|
modalRegion2 : ModalRegion2,
|
||||||
controlPanelRegion : ControlPanelRegion
|
controlPanelRegion : ControlPanelRegion
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{{#each this}}
|
{{#each this}}
|
||||||
<li>{{this}}</li>
|
<li>
|
||||||
|
{{#if reason}}
|
||||||
|
{{reason}}
|
||||||
|
{{else}}
|
||||||
|
{{this}}
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
var _ = require('underscore');
|
||||||
var Backgrid = require('backgrid');
|
var Backgrid = require('backgrid');
|
||||||
var Marionette = require('marionette');
|
var Marionette = require('marionette');
|
||||||
var _ = require('underscore');
|
|
||||||
var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection');
|
var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection');
|
||||||
|
|
||||||
module.exports = Backgrid.CellEditor.extend({
|
module.exports = Backgrid.CellEditor.extend({
|
||||||
@ -59,7 +59,11 @@ module.exports = Backgrid.CellEditor.extend({
|
|||||||
};
|
};
|
||||||
|
|
||||||
model.set(column.get('name'), newQuality);
|
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));
|
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;
|
var templateName = this.column.get('template') || this.template;
|
||||||
|
|
||||||
this.templateFunction = Marionette.TemplateCache.get(templateName);
|
this.templateFunction = Marionette.TemplateCache.get(templateName);
|
||||||
var data = this.cellValue.toJSON();
|
this.$el.empty();
|
||||||
var html = this.templateFunction(data);
|
|
||||||
this.$el.html(html);
|
if (this.cellValue) {
|
||||||
|
var data = this.cellValue.toJSON();
|
||||||
|
var html = this.templateFunction(data);
|
||||||
|
this.$el.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
this.delegateEvents();
|
this.delegateEvents();
|
||||||
return this;
|
return this;
|
||||||
|
@ -156,11 +156,12 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queue-actions-cell {
|
.queue-actions-cell {
|
||||||
min-width : 55px;
|
min-width : 65px;
|
||||||
width : 55px;
|
width : 65px;
|
||||||
text-align : right !important;
|
text-align : right !important;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
.clickable();
|
||||||
margin-left : 1px;
|
margin-left : 1px;
|
||||||
margin-right : 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 "input-groups.less";
|
||||||
@import "navs.less";
|
@import "navs.less";
|
||||||
@import "navbar.less";
|
@import "navbar.less";
|
||||||
//@import "breadcrumbs.less";
|
@import "breadcrumbs.less";
|
||||||
@import "pagination.less";
|
@import "pagination.less";
|
||||||
@import "pager.less";
|
@import "pager.less";
|
||||||
@import "labels.less";
|
@import "labels.less";
|
||||||
|
@ -156,6 +156,10 @@
|
|||||||
.fa-icon-content(@fa-var-inbox);
|
.fa-icon-content(@fa-var-inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-sonarr-import-manual {
|
||||||
|
.fa-icon-content(@fa-var-user);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-sonarr-imported {
|
.icon-sonarr-imported {
|
||||||
.fa-icon-content(@fa-var-download);
|
.fa-icon-content(@fa-var-download);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
@import "../Hotkeys/hotkeys";
|
@import "../Hotkeys/hotkeys";
|
||||||
@import "../Shared/FileBrowser/filebrowser";
|
@import "../Shared/FileBrowser/filebrowser";
|
||||||
@import "badges";
|
@import "badges";
|
||||||
|
@import "../ManualImport/manualimport";
|
||||||
|
|
||||||
.main-region {
|
.main-region {
|
||||||
@media (min-width : @screen-lg-min) {
|
@media (min-width : @screen-lg-min) {
|
||||||
|
@ -3,7 +3,6 @@ var Marionette = require('marionette');
|
|||||||
var ButtonsView = require('./ButtonsView');
|
var ButtonsView = require('./ButtonsView');
|
||||||
var ManualSearchLayout = require('./ManualLayout');
|
var ManualSearchLayout = require('./ManualLayout');
|
||||||
var ReleaseCollection = require('../../Release/ReleaseCollection');
|
var ReleaseCollection = require('../../Release/ReleaseCollection');
|
||||||
var SeriesCollection = require('../../Series/SeriesCollection');
|
|
||||||
var CommandController = require('../../Commands/CommandController');
|
var CommandController = require('../../Commands/CommandController');
|
||||||
var LoadingView = require('../../Shared/LoadingView');
|
var LoadingView = require('../../Shared/LoadingView');
|
||||||
var NoResultsView = require('./NoResultsView');
|
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.filterKey = filter[0];
|
||||||
this.state.filterValue = filter[1];
|
this.state.filterValue = filter[1];
|
||||||
|
this.state.filterType = filter[2] || 'equal';
|
||||||
|
|
||||||
if (options.reset) {
|
if (options.reset) {
|
||||||
if (this.mode !== 'server') {
|
if (this.mode !== 'server') {
|
||||||
@ -32,7 +33,11 @@ module.exports = function() {
|
|||||||
var filterModel = function(model) {
|
var filterModel = function(model) {
|
||||||
if (!self.state.filterKey || !self.state.filterValue) {
|
if (!self.state.filterKey || !self.state.filterValue) {
|
||||||
return true;
|
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;
|
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.collection.showLastModified = options.showLastModified || false;
|
||||||
this.input = options.input;
|
this.input = options.input;
|
||||||
this._setColumns();
|
this._setColumns();
|
||||||
this.listenTo(this.collection, "sync", this._showGrid);
|
this.listenTo(this.collection, 'sync', this._showGrid);
|
||||||
this.listenTo(this.collection, "filebrowser:folderselected", this._rowSelected);
|
this.listenTo(this.collection, 'filebrowser:row:folderselected', this._rowSelected);
|
||||||
|
this.listenTo(this.collection, 'filebrowser:row:fileselected', this._fileSelected);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRender : function() {
|
onRender : function() {
|
||||||
@ -51,30 +52,30 @@ module.exports = Marionette.Layout.extend({
|
|||||||
_setColumns : function() {
|
_setColumns : function() {
|
||||||
this.columns = [
|
this.columns = [
|
||||||
{
|
{
|
||||||
name : "type",
|
name : 'type',
|
||||||
label : "",
|
label : '',
|
||||||
sortable : false,
|
sortable : false,
|
||||||
cell : FileBrowserTypeCell
|
cell : FileBrowserTypeCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : "name",
|
name : 'name',
|
||||||
label : "Name",
|
label : 'Name',
|
||||||
sortable : false,
|
sortable : false,
|
||||||
cell : FileBrowserNameCell
|
cell : FileBrowserNameCell
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
if (this.collection.showLastModified) {
|
if (this.collection.showLastModified) {
|
||||||
this.columns.push({
|
this.columns.push({
|
||||||
name : "lastModified",
|
name : 'lastModified',
|
||||||
label : "Last Modified",
|
label : 'Last Modified',
|
||||||
sortable : false,
|
sortable : false,
|
||||||
cell : RelativeDateCell
|
cell : RelativeDateCell
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.collection.showFiles) {
|
if (this.collection.showFiles) {
|
||||||
this.columns.push({
|
this.columns.push({
|
||||||
name : "size",
|
name : 'size',
|
||||||
label : "Size",
|
label : 'Size',
|
||||||
sortable : false,
|
sortable : false,
|
||||||
cell : FileSizeCell
|
cell : FileSizeCell
|
||||||
});
|
});
|
||||||
@ -100,17 +101,33 @@ module.exports = Marionette.Layout.extend({
|
|||||||
row : FileBrowserRow,
|
row : FileBrowserRow,
|
||||||
collection : this.collection,
|
collection : this.collection,
|
||||||
columns : this.columns,
|
columns : this.columns,
|
||||||
className : "table table-hover"
|
className : 'table table-hover'
|
||||||
});
|
});
|
||||||
this.browser.show(grid);
|
this.browser.show(grid);
|
||||||
},
|
},
|
||||||
|
|
||||||
_rowSelected : function(model) {
|
_rowSelected : function(model) {
|
||||||
var path = model.get("path");
|
var path = model.get('path');
|
||||||
|
|
||||||
this._updatePath(path);
|
this._updatePath(path);
|
||||||
this._fetchCollection(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) {
|
_pathChanged : function(e, path) {
|
||||||
this._fetchCollection(path.value);
|
this._fetchCollection(path.value);
|
||||||
this._updatePath(path.value);
|
this._updatePath(path.value);
|
||||||
@ -118,7 +135,7 @@ module.exports = Marionette.Layout.extend({
|
|||||||
|
|
||||||
_inputChanged : function() {
|
_inputChanged : function() {
|
||||||
var path = this.ui.path.val();
|
var path = this.ui.path.val();
|
||||||
if (path === "" || path.endsWith("\\") || path.endsWith("/")) {
|
if (path === '' || path.endsWith('\\') || path.endsWith('/')) {
|
||||||
this._fetchCollection(path);
|
this._fetchCollection(path);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -130,8 +147,16 @@ module.exports = Marionette.Layout.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_selectPath : function() {
|
_selectPath : function() {
|
||||||
this.input.val(this.ui.path.val());
|
var path = this.ui.path.val();
|
||||||
this.input.trigger("change");
|
|
||||||
|
this.input.val(path);
|
||||||
|
this.input.trigger('change');
|
||||||
|
|
||||||
|
this.input.trigger('filebrowser:folderselected', {
|
||||||
|
type: 'folder',
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
|
||||||
vent.trigger(vent.Commands.CloseFileBrowser);
|
vent.trigger(vent.Commands.CloseFileBrowser);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -16,9 +16,9 @@ module.exports = Backgrid.Row.extend({
|
|||||||
|
|
||||||
_selectRow : function() {
|
_selectRow : function() {
|
||||||
if (this.model.get('type') === 'file') {
|
if (this.model.get('type') === 'file') {
|
||||||
this.model.collection.trigger('filebrowser:fileselected', this.model);
|
this.model.collection.trigger('filebrowser:row:fileselected', this.model);
|
||||||
} else {
|
} 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 HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout');
|
||||||
var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView');
|
var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView');
|
||||||
var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout');
|
var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout');
|
||||||
|
var ManualImportLayout = require('../../ManualImport/ManualImportLayout');
|
||||||
var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout');
|
var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout');
|
||||||
|
|
||||||
module.exports = Marionette.AppRouter.extend({
|
module.exports = Marionette.AppRouter.extend({
|
||||||
initialize : function() {
|
initialize : function() {
|
||||||
vent.on(vent.Commands.OpenModalCommand, this._openModal, this);
|
vent.on(vent.Commands.OpenModalCommand, this._openModal, this);
|
||||||
vent.on(vent.Commands.CloseModalCommand, this._closeModal, 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.EditSeriesCommand, this._editSeries, this);
|
||||||
vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this);
|
vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this);
|
||||||
vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this);
|
vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this);
|
||||||
vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this);
|
vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this);
|
||||||
vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this);
|
vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this);
|
||||||
vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, 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.ShowFileBrowser, this._showFileBrowser, this);
|
||||||
vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this);
|
vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this);
|
||||||
},
|
},
|
||||||
@ -31,6 +35,14 @@ module.exports = Marionette.AppRouter.extend({
|
|||||||
AppLayout.modalRegion.closeModal();
|
AppLayout.modalRegion.closeModal();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_openModal2 : function(view) {
|
||||||
|
AppLayout.modalRegion2.show(view);
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeModal2 : function() {
|
||||||
|
AppLayout.modalRegion2.closeModal();
|
||||||
|
},
|
||||||
|
|
||||||
_editSeries : function(options) {
|
_editSeries : function(options) {
|
||||||
var view = new EditSeriesView({ model : options.series });
|
var view = new EditSeriesView({ model : options.series });
|
||||||
AppLayout.modalRegion.show(view);
|
AppLayout.modalRegion.show(view);
|
||||||
@ -65,12 +77,17 @@ module.exports = Marionette.AppRouter.extend({
|
|||||||
AppLayout.modalRegion.show(view);
|
AppLayout.modalRegion.show(view);
|
||||||
},
|
},
|
||||||
|
|
||||||
_showFileBrowser : function(options) {
|
_showManualImport : function(options) {
|
||||||
var view = new FileBrowserLayout(options);
|
var view = new ManualImportLayout(options);
|
||||||
AppLayout.fileBrowserModalRegion.show(view);
|
AppLayout.modalRegion.show(view);
|
||||||
},
|
},
|
||||||
|
|
||||||
_closeFileBrowser : function(options) {
|
_showFileBrowser : function(options) {
|
||||||
AppLayout.fileBrowserModalRegion.closeModal();
|
var view = new FileBrowserLayout(options);
|
||||||
|
AppLayout.modalRegion2.show(view);
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeFileBrowser : function() {
|
||||||
|
AppLayout.modalRegion2.closeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -4,7 +4,7 @@ var Marionette = require('marionette');
|
|||||||
require('bootstrap');
|
require('bootstrap');
|
||||||
|
|
||||||
var region = Marionette.Region.extend({
|
var region = Marionette.Region.extend({
|
||||||
el : '#file-browser-modal-region',
|
el : '#modal-region2',
|
||||||
|
|
||||||
constructor : function() {
|
constructor : function() {
|
||||||
Backbone.Marionette.Region.prototype.constructor.apply(this, arguments);
|
Backbone.Marionette.Region.prototype.constructor.apply(this, arguments);
|
@ -1,5 +1,6 @@
|
|||||||
var $ = require('jquery');
|
var $ = require('jquery');
|
||||||
var _ = require('underscore');
|
var _ = require('underscore');
|
||||||
|
var vent = require('../../vent');
|
||||||
var Marionette = require('marionette');
|
var Marionette = require('marionette');
|
||||||
var Backgrid = require('backgrid');
|
var Backgrid = require('backgrid');
|
||||||
var MissingCollection = require('./MissingCollection');
|
var MissingCollection = require('./MissingCollection');
|
||||||
@ -13,6 +14,7 @@ var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout');
|
|||||||
var LoadingView = require('../../Shared/LoadingView');
|
var LoadingView = require('../../Shared/LoadingView');
|
||||||
var Messenger = require('../../Shared/Messenger');
|
var Messenger = require('../../Shared/Messenger');
|
||||||
var CommandController = require('../../Commands/CommandController');
|
var CommandController = require('../../Commands/CommandController');
|
||||||
|
|
||||||
require('backgrid.selectall');
|
require('backgrid.selectall');
|
||||||
require('../../Mixins/backbone.signalr.mixin');
|
require('../../Mixins/backbone.signalr.mixin');
|
||||||
|
|
||||||
@ -128,11 +130,16 @@ module.exports = Marionette.Layout.extend({
|
|||||||
route : 'seasonpass'
|
route : 'seasonpass'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title : 'Rescan Drone Factory Folder',
|
title : 'Rescan Drone Factory Folder',
|
||||||
icon : 'icon-sonarr-refresh',
|
icon : 'icon-sonarr-refresh',
|
||||||
command : 'downloadedepisodesscan',
|
command : 'downloadedepisodesscan',
|
||||||
|
|
||||||
properties : { sendUpdates : true }
|
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' }
|
command : { name : 'missingEpisodeSearch' }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_setFilter : function(buttonContext) {
|
_setFilter : function(buttonContext) {
|
||||||
var mode = buttonContext.model.get('key');
|
var mode = buttonContext.model.get('key');
|
||||||
this.collection.state.currentPage = 1;
|
this.collection.state.currentPage = 1;
|
||||||
@ -180,6 +188,7 @@ module.exports = Marionette.Layout.extend({
|
|||||||
buttonContext.ui.icon.spinForPromise(promise);
|
buttonContext.ui.icon.spinForPromise(promise);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_searchSelected : function() {
|
_searchSelected : function() {
|
||||||
var selected = this.missingGrid.getSelectedModels();
|
var selected = this.missingGrid.getSelectedModels();
|
||||||
if (selected.length === 0) {
|
if (selected.length === 0) {
|
||||||
@ -223,5 +232,8 @@ module.exports = Marionette.Layout.extend({
|
|||||||
$.when(promises).done(function () {
|
$.when(promises).done(function () {
|
||||||
self.collection.fetch();
|
self.collection.fetch();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
_manualImport : function () {
|
||||||
|
vent.trigger(vent.Commands.ShowManualImport);
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-region"></div>
|
<div id="modal-region"></div>
|
||||||
<div id="file-browser-modal-region"></div>
|
<div id="modal-region2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a id="scroll-up" title="Back to the top!">
|
<a id="scroll-up" title="Back to the top!">
|
||||||
|
@ -48,8 +48,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-region"></div>
|
|
||||||
<div id="file-browser-modal-region"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,12 +15,15 @@ vent.Commands = {
|
|||||||
DeleteSeriesCommand : 'DeleteSeriesCommand',
|
DeleteSeriesCommand : 'DeleteSeriesCommand',
|
||||||
OpenModalCommand : 'OpenModalCommand',
|
OpenModalCommand : 'OpenModalCommand',
|
||||||
CloseModalCommand : 'CloseModalCommand',
|
CloseModalCommand : 'CloseModalCommand',
|
||||||
|
OpenModal2Command : 'OpenModal2Command',
|
||||||
|
CloseModal2Command : 'CloseModal2Command',
|
||||||
ShowEpisodeDetails : 'ShowEpisodeDetails',
|
ShowEpisodeDetails : 'ShowEpisodeDetails',
|
||||||
ShowHistoryDetails : 'ShowHistoryDetails',
|
ShowHistoryDetails : 'ShowHistoryDetails',
|
||||||
ShowLogDetails : 'ShowLogDetails',
|
ShowLogDetails : 'ShowLogDetails',
|
||||||
SaveSettings : 'saveSettings',
|
SaveSettings : 'saveSettings',
|
||||||
ShowLogFile : 'showLogFile',
|
ShowLogFile : 'showLogFile',
|
||||||
ShowRenamePreview : 'showRenamePreview',
|
ShowRenamePreview : 'showRenamePreview',
|
||||||
|
ShowManualImport : 'showManualImport',
|
||||||
ShowFileBrowser : 'showFileBrowser',
|
ShowFileBrowser : 'showFileBrowser',
|
||||||
CloseFileBrowser : 'closeFileBrowser',
|
CloseFileBrowser : 'closeFileBrowser',
|
||||||
OpenControlPanelCommand : 'OpenControlPanelCommand',
|
OpenControlPanelCommand : 'OpenControlPanelCommand',
|
||||||
|
Loading…
Reference in New Issue
Block a user