diff --git a/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs b/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs new file mode 100644 index 000000000..d7678f72b --- /dev/null +++ b/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs @@ -0,0 +1,152 @@ +using System.Collections; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Api.REST; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Parser; +using System.Linq; +using System; +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.RootFolders; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Music; + +namespace NzbDrone.Api.Music +{ + + public class UnmappedComparer : IComparer + { + public int Compare(UnmappedFolder a, UnmappedFolder b) + { + return a.Name.CompareTo(b.Name); + } + } + + public class MusicBulkImportModule : NzbDroneRestModule + { + private readonly ISearchForNewArtist _searchProxy; + private readonly IRootFolderService _rootFolderService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly IDiskScanService _diskScanService; + private readonly ICached _mappedArtists; + private readonly IArtistService _artistService; + + public MusicBulkImportModule(ISearchForNewArtist searchProxy, + IRootFolderService rootFolderService, + IMakeImportDecision importDecisionMaker, + IDiskScanService diskScanService, + ICacheManager cacheManager, + IArtistService artistService + ) + : base("/artist/bulkimport") + { + _searchProxy = searchProxy; + _rootFolderService = rootFolderService; + _importDecisionMaker = importDecisionMaker; + _diskScanService = diskScanService; + _mappedArtists = cacheManager.GetCache(GetType(), "mappedArtistsCache"); + _artistService = artistService; + Get["/"] = x => Search(); + } + + + private Response Search() + { + if (Request.Query.Id == 0) + { + throw new BadRequestException("Invalid Query"); + } + + RootFolder rootFolder = _rootFolderService.Get(Request.Query.Id); + + var unmapped = rootFolder.UnmappedFolders.OrderBy(f => f.Name).ToList(); + + var paged = unmapped; + + var mapped = paged.Select(page => + { + Artist m = null; + + var mappedArtist = _mappedArtists.Find(page.Name); + + if (mappedArtist != null) + { + return mappedArtist; + } + + var files = _diskScanService.GetMusicFiles(page.Path); + + // Check for music files in directory + if (files.Count() == 0) + { + return null; + } + + var parsedTitle = Parser.ParseMusicPath(files.FirstOrDefault()); + if (parsedTitle == null || parsedTitle.ArtistTitle == null) + { + m = new Artist + { + Name = page.Name.Replace(".", " ").Replace("-", " "), + Path = page.Path, + }; + } + else + { + m = new Artist + { + Name = parsedTitle.ArtistTitle, + Path = page.Path + }; + } + + var searchResults = _searchProxy.SearchForNewArtist(m.Name); + + if (searchResults == null || searchResults.Count == 0) + { + return null; + }; + + mappedArtist = searchResults.First(); + + if (mappedArtist != null) + { + mappedArtist.Monitored = true; + mappedArtist.Path = page.Path; + + _mappedArtists.Set(page.Name, mappedArtist, TimeSpan.FromDays(2)); + + return mappedArtist; + } + + return null; + }); + + var mapping = MapToResource(mapped.Where(m => m != null)).ToList().AsResponse(); + + return mapping; + } + + + private static IEnumerable MapToResource(IEnumerable artists) + { + foreach (var currentArtist in artists) + { + var resource = currentArtist.ToResource(); + var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} diff --git a/src/NzbDrone.Api/Music/ArtistLookupModule.cs b/src/NzbDrone.Api/Music/ArtistLookupModule.cs index 4f6d5e030..faa0eca09 100644 --- a/src/NzbDrone.Api/Music/ArtistLookupModule.cs +++ b/src/NzbDrone.Api/Music/ArtistLookupModule.cs @@ -11,9 +11,9 @@ namespace NzbDrone.Api.Music { public class ArtistLookupModule : NzbDroneRestModule { - private readonly ISearchForNewSeries _searchProxy; //TODO: Switch out for Music varriant + private readonly ISearchForNewArtist _searchProxy; - public ArtistLookupModule(ISearchForNewSeries searchProxy) + public ArtistLookupModule(ISearchForNewArtist searchProxy) : base("/artist/lookup") { _searchProxy = searchProxy; diff --git a/src/NzbDrone.Api/Music/ListImport.cs b/src/NzbDrone.Api/Music/ListImport.cs new file mode 100644 index 000000000..456f95243 --- /dev/null +++ b/src/NzbDrone.Api/Music/ListImport.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using Nancy.Extensions; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.Music; + +namespace NzbDrone.Api.Music +{ + public class ListImportModule : NzbDroneApiModule + { + private readonly IAddArtistService _artistService; + + public ListImportModule(IAddArtistService artistService) + : base("/artist/import") + { + _artistService = artistService; + Put["/"] = Artist => SaveAll(); + } + + private Response SaveAll() + { + var resources = Request.Body.FromJson>(); + + var Artists = resources.Select(ArtistResource => (ArtistResource.ToModel())).Where(m => m != null).DistinctBy(m => m.ForeignArtistId).ToList(); + + return _artistService.AddArtists(Artists).ToResource().AsResponse(HttpStatusCode.Accepted); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 3c8875fcd..0cd539e45 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -108,7 +108,9 @@ + + @@ -244,7 +246,6 @@ - diff --git a/src/NzbDrone.Api/Series/SeriesLookupModule.cs b/src/NzbDrone.Api/Series/SeriesLookupModule.cs deleted file mode 100644 index 6506c1f82..000000000 --- a/src/NzbDrone.Api/Series/SeriesLookupModule.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MetadataSource; -using System.Linq; - -namespace NzbDrone.Api.Series -{ - public class SeriesLookupModule : NzbDroneRestModule - { - private readonly ISearchForNewSeries _searchProxy; - - public SeriesLookupModule(ISearchForNewSeries searchProxy) - : base("/series/lookup") - { - _searchProxy = searchProxy; - Get["/"] = x => Search(); - } - - - private Response Search() - { - var tvDbResults = _searchProxy.SearchForNewSeries((string)Request.Query.term); - return MapToResource(tvDbResults).AsResponse(); - } - - - private static IEnumerable MapToResource(IEnumerable series) - { - foreach (var currentSeries in series) - { - var resource = currentSeries.ToResource(); - var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); - if (poster != null) - { - resource.RemotePoster = poster.Url; - } - - yield return resource; - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/BulkImport/BulkImportFixture.cs b/src/NzbDrone.Core.Test/BulkImport/BulkImportFixture.cs new file mode 100644 index 000000000..579e5cdf2 --- /dev/null +++ b/src/NzbDrone.Core.Test/BulkImport/BulkImportFixture.cs @@ -0,0 +1,51 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using Moq; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Music; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music.Events; +using System.Collections.Generic; + +namespace NzbDrone.Core.Test.BulkImport +{ + [TestFixture] + public class BulkImportFixture : CoreTest + { + private List fakeArtists; + private FluentValidation.Results.ValidationResult fakeValidation; + + [SetUp] + public void Setup() + { + fakeArtists = Builder.CreateListOfSize(3).BuildList(); + fakeArtists.ForEach(m => + { + m.Path = null; + m.RootFolderPath = @"C:\Test\Music"; + }); + + fakeValidation = Builder.CreateNew().Build(); + } + [Test] + public void artist_added_event_should_have_proper_path() + { + Mocker.GetMock() + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns((Artist m, NamingConfig n) => m.Name); + + Mocker.GetMock() + .Setup(s => s.Validate(It.IsAny())) + .Returns(fakeValidation); + + var artists = Subject.AddArtists(fakeArtists); + + foreach (Artist artist in artists) + { + artist.Path.Should().NotBeNullOrEmpty(); + } + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs index 2ec2d8bc0..b9d84f0fa 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs @@ -17,37 +17,34 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook UseRealHttp(); } - [TestCase("The Simpsons", "The Simpsons")] - [TestCase("South Park", "South Park")] - [TestCase("Franklin & Bash", "Franklin & Bash")] - [TestCase("House", "House")] - [TestCase("Mr. D", "Mr. D")] - [TestCase("Rob & Big", "Rob & Big")] - [TestCase("M*A*S*H", "M*A*S*H")] - //[TestCase("imdb:tt0436992", "Doctor Who (2005)")] - [TestCase("tvdb:78804", "Doctor Who (2005)")] - [TestCase("tvdbid:78804", "Doctor Who (2005)")] - [TestCase("tvdbid: 78804 ", "Doctor Who (2005)")] + [TestCase("Coldplay", "Coldplay")] + [TestCase("Avenged Sevenfold", "Avenged Sevenfold")] + [TestCase("3OH!3", "3OH!3")] + [TestCase("Where's Kitty?", "Where's Kitty?")] + [TestCase("The Academy Is...", "The Academy Is...")] + [TestCase("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] + [TestCase("lidarrid:f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] + [TestCase("lidarrid: f59c5520-5f46-4d2c-b2c4-822eabf53419 ", "Linkin Park")] public void successful_search(string title, string expected) { - var result = Subject.SearchForNewSeries(title); + var result = Subject.SearchForNewArtist(title); result.Should().NotBeEmpty(); - result[0].Title.Should().Be(expected); + result[0].Name.Should().Be(expected); ExceptionVerification.IgnoreWarns(); } - [TestCase("tvdbid:")] - [TestCase("tvdbid: 99999999999999999999")] - [TestCase("tvdbid: 0")] - [TestCase("tvdbid: -12")] - [TestCase("tvdbid:289578")] + [TestCase("lidarrid:")] + [TestCase("lidarrid: 99999999999999999999")] + [TestCase("lidarrid: 0")] + [TestCase("lidarrid: -12")] + [TestCase("lidarrid:289578")] [TestCase("adjalkwdjkalwdjklawjdlKAJD;EF")] public void no_search_result(string term) { - var result = Subject.SearchForNewSeries(term); + var result = Subject.SearchForNewArtist(term); result.Should().BeEmpty(); ExceptionVerification.IgnoreWarns(); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 2928a35db..d168599c8 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -115,6 +115,7 @@ + diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 918440c59..b59a175f9 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles { void Scan(Artist artist); string[] GetVideoFiles(string path, bool allDirectories = true); + string[] GetMusicFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true); List FilterFiles(Series series, IEnumerable files); } @@ -166,7 +167,7 @@ namespace NzbDrone.Core.MediaFiles .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); - _logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path); + _logger.Debug("{0} audio files were found in {1}", mediaFileList.Count, path); return mediaFileList.ToArray(); } diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs similarity index 58% rename from src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs rename to src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs index 01b096254..65cdb28e8 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public interface ISearchForNewSeries + public interface ISearchForNewArtist { - List SearchForNewSeries(string title); List SearchForNewArtist(string title); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index e9774ed1e..8652e54d2 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -16,7 +16,7 @@ using Newtonsoft.Json; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideSeriesInfo, IProvideArtistInfo, ISearchForNewSeries + public class SkyHookProxy : IProvideSeriesInfo, IProvideArtistInfo, ISearchForNewArtist { private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -30,50 +30,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook _logger = logger; } - - public Tuple> GetSeriesInfo(int tvdbSeriesId) { - Console.WriteLine("[GetSeriesInfo] id:" + tvdbSeriesId); - var httpRequest = _requestBuilder.Create() - .SetSegment("route", "shows") - .Resource(tvdbSeriesId.ToString()) - .Build(); - - httpRequest.AllowAutoRedirect = true; - httpRequest.SuppressHttpError = true; - - var httpResponse = _httpClient.Get(httpRequest); - - if (httpResponse.HasHttpError) - { - if (httpResponse.StatusCode == HttpStatusCode.NotFound) - { - throw new SeriesNotFoundException(tvdbSeriesId); - } - else - { - throw new HttpException(httpRequest, httpResponse); - } - } - - var episodes = httpResponse.Resource.Episodes.Select(MapEpisode); - var series = MapSeries(httpResponse.Resource); - - return new Tuple>(series, episodes.ToList()); - } - - public List SearchForNewSeries(string title) - { - // TODO: Remove this API - var tempList = new List(); - var tempSeries = new Series(); - tempSeries.Title = "AFI"; - tempList.Add(tempSeries); - return tempList; + throw new NotImplementedException(); } - public Tuple> GetArtistInfo(string foreignArtistId) { @@ -203,65 +164,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return artist; } - - private static Series MapSeries(ShowResource show) - { - var series = new Series(); - series.TvdbId = show.TvdbId; - - if (show.TvRageId.HasValue) - { - series.TvRageId = show.TvRageId.Value; - } - - if (show.TvMazeId.HasValue) - { - series.TvMazeId = show.TvMazeId.Value; - } - - series.ImdbId = show.ImdbId; - series.Title = show.Title; - series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title); - series.SortTitle = SeriesTitleNormalizer.Normalize(show.Title, show.TvdbId); - - if (show.FirstAired != null) - { - series.FirstAired = DateTime.Parse(show.FirstAired).ToUniversalTime(); - series.Year = series.FirstAired.Value.Year; - } - - series.Overview = show.Overview; - - if (show.Runtime != null) - { - series.Runtime = show.Runtime.Value; - } - - series.Network = show.Network; - - if (show.TimeOfDay != null) - { - series.AirTime = string.Format("{0:00}:{1:00}", show.TimeOfDay.Hours, show.TimeOfDay.Minutes); - } - - series.TitleSlug = show.Slug; - series.Status = MapSeriesStatus(show.Status); - //series.Ratings = MapRatings(show.Rating); - series.Genres = show.Genres; - - if (show.ContentRating.IsNotNullOrWhiteSpace()) - { - series.Certification = show.ContentRating.ToUpper(); - } - - series.Actors = show.Actors.Select(MapActors).ToList(); - series.Seasons = show.Seasons.Select(MapSeason).ToList(); - series.Images = show.Images.Select(MapImage).ToList(); - series.Monitored = true; - - return series; - } - private static Actor MapActors(ActorResource arg) { var newActor = new Actor @@ -281,40 +183,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return newActor; } - private static Episode MapEpisode(EpisodeResource oracleEpisode) - { - var episode = new Episode(); - episode.Overview = oracleEpisode.Overview; - episode.SeasonNumber = oracleEpisode.SeasonNumber; - episode.EpisodeNumber = oracleEpisode.EpisodeNumber; - episode.AbsoluteEpisodeNumber = oracleEpisode.AbsoluteEpisodeNumber; - episode.Title = oracleEpisode.Title; - - episode.AirDate = oracleEpisode.AirDate; - episode.AirDateUtc = oracleEpisode.AirDateUtc; - - //episode.Ratings = MapRatings(oracleEpisode.Rating); - - //Don't include series fanart images as episode screenshot - if (oracleEpisode.Image != null) - { - episode.Images.Add(new MediaCover.MediaCover(MediaCoverTypes.Screenshot, oracleEpisode.Image)); - } - - return episode; - } - - private static Season MapSeason(SeasonResource seasonResource) - { - return new Season - { - SeasonNumber = seasonResource.SeasonNumber, - Images = seasonResource.Images.Select(MapImage).ToList(), - Monitored = seasonResource.SeasonNumber > 0 - }; - } - - private static SeriesStatusType MapSeriesStatus(string status) + private static SeriesStatusType MapArtistStatus(string status) { if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/NzbDrone.Core/Music/AddArtistService.cs b/src/NzbDrone.Core/Music/AddArtistService.cs index 41191b03c..1ed528c99 100644 --- a/src/NzbDrone.Core/Music/AddArtistService.cs +++ b/src/NzbDrone.Core/Music/AddArtistService.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Music public interface IAddArtistService { Artist AddArtist(Artist newArtist); + List AddArtists(List newArtists); } public class AddArtistService : IAddArtistService @@ -44,7 +45,7 @@ namespace NzbDrone.Core.Music { Ensure.That(newArtist, () => newArtist).IsNotNull(); - newArtist = AddSkyhookData(newArtist); + //newArtist = AddSkyhookData(newArtist); if (string.IsNullOrWhiteSpace(newArtist.Path)) { @@ -64,11 +65,21 @@ namespace NzbDrone.Core.Music } _logger.Info("Adding Artist {0} Path: [{1}]", newArtist, newArtist.Path); - _artistService.AddArtist(newArtist); + newArtist = _artistService.AddArtist(newArtist); return newArtist; } + public List AddArtists(List newArtists) + { + newArtists.ForEach(artist => + { + AddArtist(artist); + }); + + return newArtists; + } + private Artist AddSkyhookData(Artist newArtist) { Tuple> tuple; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 81ca11ed2..aac2ccb85 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -827,6 +827,7 @@ + @@ -860,8 +861,7 @@ - - + diff --git a/src/UI/AddArtist/AddArtistLayout.js b/src/UI/AddArtist/AddArtistLayout.js index 2b398335e..da539c101 100644 --- a/src/UI/AddArtist/AddArtistLayout.js +++ b/src/UI/AddArtist/AddArtistLayout.js @@ -6,6 +6,7 @@ var ExistingArtistCollectionView = require('./Existing/AddExistingArtistCollecti var AddArtistView = require('./AddArtistView'); var ProfileCollection = require('../Profile/ProfileCollection'); var RootFolderCollection = require('./RootFolders/RootFolderCollection'); +var BulkImportView = require('./BulkImport/BulkImportView'); require('../Artist/ArtistCollection'); module.exports = Marionette.Layout.extend({ @@ -17,6 +18,7 @@ module.exports = Marionette.Layout.extend({ events : { 'click .x-import' : '_importArtist', + 'click .x-bulk-import' : '_bulkImportArtist', 'click .x-add-new' : '_addArtist' }, @@ -41,6 +43,11 @@ module.exports = Marionette.Layout.extend({ this.workspace.show(new ExistingArtistCollectionView({ model : options.model })); }, + _bulkFolderSelected : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.workspace.show(new BulkImportView({ model : options.model})); + }, + _importArtist : function() { this.rootFolderLayout = new RootFolderLayout(); this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); @@ -49,5 +56,11 @@ module.exports = Marionette.Layout.extend({ _addArtist : function() { this.workspace.show(new AddArtistView()); + }, + + _bulkImportArtist : function() { + this.bulkRootFolderLayout = new RootFolderLayout(); + this.listenTo(this.bulkRootFolderLayout, 'folderSelected', this._bulkFolderSelected); + AppLayout.modalRegion.show(this.bulkRootFolderLayout); } }); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs b/src/UI/AddArtist/AddArtistLayoutTemplate.hbs index 53f225a1e..313cccef4 100644 --- a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs +++ b/src/UI/AddArtist/AddArtistLayoutTemplate.hbs @@ -1,10 +1,11 @@
- +
diff --git a/src/UI/AddArtist/BulkImport/ArtistPathCell.js b/src/UI/AddArtist/BulkImport/ArtistPathCell.js new file mode 100644 index 000000000..debe25ae1 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/ArtistPathCell.js @@ -0,0 +1,7 @@ +var TemplatedCell = require('../../Cells/TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'artist-title-cell', + template : 'AddArtist/BulkImport/ArtistPathTemplate', + +}); diff --git a/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs b/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs new file mode 100644 index 000000000..53fa29105 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs @@ -0,0 +1 @@ +{{path}}
diff --git a/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js b/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js new file mode 100644 index 000000000..5490439e3 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js @@ -0,0 +1,21 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var BulkImportCollection = require('./BulkImportCollection'); + +module.exports = NzbDroneCell.extend({ + className : 'artist-title-cell', + + render : function() { + var collection = this.model.collection; + this.listenTo(collection, 'sync', this._renderCell); + + this._renderCell(); + + return this; + }, + + _renderCell : function() { + this.$el.empty(); + + this.$el.html('' + this.cellValue.get('name') +'
' + this.cellValue.get('overview') + ''); + } +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportCollection.js b/src/UI/AddArtist/BulkImport/BulkImportCollection.js new file mode 100644 index 000000000..d6fef1faa --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportCollection.js @@ -0,0 +1,49 @@ +var _ = require('underscore'); +var PageableCollection = require('backbone.pageable'); +var ArtistModel = require('../../Artist/ArtistModel'); +var AsSortedCollection = require('../../Mixins/AsSortedCollection'); +var AsPageableCollection = require('../../Mixins/AsPageableCollection'); +var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); + +var BulkImportCollection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/artist/bulkimport', + model : ArtistModel, + tableName : 'bulkimport', + + state : { + pageSize : 100000, + sortKey: 'sortName', + firstPage: 1 + }, + + fetch : function(options) { + + options = options || {}; + + var data = options.data || {}; + + if (!data.id || !data.folder) { + data.id = this.folderId; + data.folder = this.folder; + } + + options.data = data; + return PageableCollection.prototype.fetch.call(this, options); + }, + + parseLinks : function(options) { + + return { + first : this.url, + next: this.url, + last : this.url + }; + } +}); + + +BulkImportCollection = AsSortedCollection.call(BulkImportCollection); +BulkImportCollection = AsPageableCollection.call(BulkImportCollection); +BulkImportCollection = AsPersistedStateCollection.call(BulkImportCollection); + +module.exports = BulkImportCollection; diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js b/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js new file mode 100644 index 000000000..db9860ffe --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js @@ -0,0 +1,65 @@ +var Backgrid = require('backgrid'); +var Config = require('../../Config'); +var _ = require('underscore'); +var vent = require('vent'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var Marionette = require('marionette'); + +module.exports = TemplatedCell.extend({ + className : 'monitor-cell', + template : 'AddArtist/BulkImport/BulkImportMonitorCell', + + _orig : TemplatedCell.prototype.initialize, + _origRender : TemplatedCell.prototype.initialize, + + ui : { + monitor : '.x-monitor', + }, + + events: { 'change .x-monitor' : '_monitorChanged' }, + + initialize : function () { + this._orig.apply(this, arguments); + + this.defaultMonitor = Config.getValue(Config.Keys.MonitorEpisodes, 'all'); + + this.model.set('monitored', this._convertMonitorToBool(this.defaultMonitor)); + + this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); + }, + + _convertMonitorToBool : function(monitorString) { + return monitorString === 'all' ? true : false; + }, + + _convertBooltoMonitor : function(monitorBool) { + return monitorBool === true ? 'all' : 'none'; + }, + + _monitorChanged : function() { + Config.setValue(Config.Keys.MonitorEpisodes, this.$el.find('.x-monitor').val()); + this.defaultMonitor = this.$el.find('.x-monitor').val(); + this.model.set('monitored', this._convertMonitorToBool(this.$el.find('.x-monitor').val())); + }, + + render : function() { + var templateName = this.column.get('template') || this.template; + + this.templateFunction = Marionette.TemplateCache.get(templateName); + this.$el.empty(); + + if (this.cellValue) { + var data = this.cellValue.toJSON(); + var html = this.templateFunction(data); + this.$el.html(html); + } + + this.delegateEvents(); + + this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); + + return this; + } + +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs new file mode 100644 index 000000000..5ef509ce1 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs @@ -0,0 +1,4 @@ + diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js new file mode 100644 index 000000000..682c475f9 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js @@ -0,0 +1,32 @@ +var Backgrid = require('backgrid'); +var ProfileCollection = require('../../Profile/ProfileCollection'); +var Config = require('../../Config'); +var _ = require('underscore'); + +module.exports = Backgrid.SelectCell.extend({ + className : 'profile-cell', + + _orig : Backgrid.SelectCell.prototype.initialize, + + initialize : function () { + this._orig.apply(this, arguments); + + this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + if(ProfileCollection.get(this.defaultProfile)) + { + this.profile = this.defaultProfile; + } else { + this.profile = ProfileCollection.get(1); + } + + this.render(); + + }, + + optionValues : function() { + return _.map(ProfileCollection.models, function(model){ + return [model.get('name'), model.get('id')+""]; + }); + } + +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js new file mode 100644 index 000000000..4b0bd168b --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js @@ -0,0 +1,77 @@ +var Backgrid = require('backgrid'); +var ProfileCollection = require('../../Profile/ProfileCollection'); +var Config = require('../../Config'); +var _ = require('underscore'); +var vent = require('vent'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var Marionette = require('marionette'); + +module.exports = TemplatedCell.extend({ + className : 'profile-cell', + template : 'AddArtist/BulkImport/BulkImportProfileCell', + + _orig : TemplatedCell.prototype.initialize, + _origRender : TemplatedCell.prototype.initialize, + + ui : { + profile : '.x-profile', + }, + + events: { 'change .x-profile' : '_profileChanged' }, + + initialize : function () { + this._orig.apply(this, arguments); + + this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); + + this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + + this.profile = this.defaultProfile; + + if(ProfileCollection.get(this.defaultProfile)) + { + this.profile = this.defaultProfile; + this.model.set('profileId', this.defaultProfile); + } else { + this.profile = 1; + this.model.set('profileId', 1); + } + + this.$('.x-profile').val(this.model.get('profileId')); + + this.cellValue = ProfileCollection; + + }, + + _profileChanged : function() { + Config.setValue(Config.Keys.DefaultProfileId, this.$('.x-profile').val()); + this.model.set('profileId', this.$('.x-profile').val()); + }, + + _onConfigUpdated : function(options) { + if (options.key === Config.Keys.DefaultProfileId) { + this.defaultProfile = options.value; + } + }, + + render : function() { + var templateName = this.column.get('template') || this.template; + + this.cellValue = ProfileCollection; + + this.templateFunction = Marionette.TemplateCache.get(templateName); + this.$el.empty(); + + if (this.cellValue) { + var data = this.cellValue.toJSON(); + var html = this.templateFunction(data); + this.$el.html(html); + } + + this.delegateEvents(); + this.$('.x-profile').val(this.model.get('profileId')); + return this; + } + +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs new file mode 100644 index 000000000..7124319eb --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs @@ -0,0 +1,5 @@ + diff --git a/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js b/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js new file mode 100644 index 000000000..d1435dd14 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js @@ -0,0 +1,54 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var Backgrid = require('backgrid'); +var FullArtistCollection = require('../../Artist/ArtistCollection'); + + +module.exports = SelectAllCell.extend({ + _originalRender : SelectAllCell.prototype.render, + + _originalInit : SelectAllCell.prototype.initialize, + + initialize : function() { + this._originalInit.apply(this, arguments); + + this._refreshIsDuplicate(); + + this.listenTo(this.model, 'change', this._refresh); + }, + + onChange : function(e) { + if(!this.isDuplicate) { + var checked = $(e.target).prop('checked'); + this.$el.parent().toggleClass('selected', checked); + this.model.trigger('backgrid:selected', this.model, checked); + } else { + $(e.target).prop('checked', false); + } + }, + + render : function() { + this._originalRender.apply(this, arguments); + + this.$el.children(':first').prop('disabled', this.isDuplicate); + + if (!this.isDuplicate) { + this.$el.children(':first').prop('checked', this.isChecked); + } + + return this; + }, + + _refresh: function() { + this.isChecked = this.$el.children(':first').prop('checked'); + this._refreshIsDuplicate(); + this.render(); + }, + + _refreshIsDuplicate: function() { + var foreignArtistId = this.model.get('foreignArtistId'); + var existingArtist = FullArtistCollection.where({ foreignArtistId: foreignArtistId }); + this.isDuplicate = existingArtist.length > 0 ? true : false; + } +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportView.js b/src/UI/AddArtist/BulkImport/BulkImportView.js new file mode 100644 index 000000000..83fcd41f6 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportView.js @@ -0,0 +1,191 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var ArtistNameCell = require('./BulkImportArtistNameCell'); +var BulkImportCollection = require('./BulkImportCollection'); +var ForeignIdCell = require('./ForeignIdCell'); +var GridPager = require('../../Shared/Grid/Pager'); +var SelectAllCell = require('./BulkImportSelectAllCell'); +var ProfileCell = require('./BulkImportProfileCellT'); +var MonitorCell = require('./BulkImportMonitorCell'); +var ArtistPathCell = require('./ArtistPathCell'); +var LoadingView = require('../../Shared/LoadingView'); +var EmptyView = require('./EmptyView'); +var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); +var CommandController = require('../../Commands/CommandController'); +var Messenger = require('../../Shared/Messenger'); +var ArtistCollection = require('../../Artist/ArtistCollection'); +var ProfileCollection = require('../../Profile/ProfileCollection'); + +require('backgrid.selectall'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + template : 'AddArtist/BulkImport/BulkImportViewTemplate', + + regions : { + toolbar : '#x-toolbar', + table : '#x-artists-bulk', + }, + + ui : { + addSelectdBtn : '.x-add-selected' + }, + + initialize : function(options) { + ProfileCollection.fetch(); + this.bulkImportCollection = new BulkImportCollection().bindSignalR({ updateOnly : true }); + this.model = options.model; + this.folder = this.model.get('path'); + this.folderId = this.model.get('id'); + this.bulkImportCollection.folderId = this.folderId; + this.bulkImportCollection.folder = this.folder; + this.bulkImportCollection.fetch(); + this.listenTo(this.bulkImportCollection, {'sync' : this._showContent, 'error' : this._showContent, 'backgrid:selected' : this._select}); + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false, + cellValue : 'this' + }, + { + name : 'movie', + label : 'Artist', + cell : ArtistNameCell, + cellValue : 'this', + sortable : false + }, + { + name : 'path', + label : 'Path', + cell : ArtistPathCell, + cellValue : 'this', + sortable : false + }, + { + name : 'foreignArtistId', + label : 'MB Id', + cell : ForeignIdCell, + cellValue : 'this', + sortable: false + }, + { + name :'monitor', + label: 'Monitor', + cell : MonitorCell, + cellValue : 'this', + sortable: false + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell, + cellValue : 'this', + sortable: false + } + ], + + _showContent : function() { + this._showToolbar(); + this._showTable(); + }, + + onShow : function() { + this.table.show(new LoadingView()); + }, + + _showToolbar : function() { + var leftSideButtons = { + type : 'default', + storeState: false, + collapse : true, + items : [ + { + title : 'Add Selected', + icon : 'icon-lidarr-add', + callback : this._addSelected, + ownerContext : this, + className : 'x-add-selected' + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : [leftSideButtons], + right : [], + context : this + })); + + $('#x-toolbar').addClass('inline'); + }, + + _addSelected : function() { + var selected = _.filter(this.bulkImportCollection.models, function(elem){ + return elem.selected; + }); + + var promise = ArtistCollection.importFromList(selected); + this.ui.addSelectdBtn.spinForPromise(promise); + this.ui.addSelectdBtn.addClass('disabled'); + + if (selected.length === 0) { + Messenger.show({ + type : 'error', + message : 'No artists selected' + }); + return; + } + + Messenger.show({ + message : 'Importing {0} artists. This can take multiple minutes depending on how many artists should be imported. Don\'t close this browser window until it is finished!'.format(selected.length), + hideOnNavigate : false, + hideAfter : 30, + type : 'error' + }); + + var _this = this; + + promise.done(function() { + Messenger.show({ + message : 'Imported artists from folder.', + hideAfter : 8, + hideOnNavigate : true + }); + + + _.forEach(selected, function(artist) { + artist.destroy(); //update the collection without the added movies + }); + }); + }, + + _handleEvent : function(eventName, data) { + if (eventName === 'sync' || eventName === 'content') { + this._showContent(); + } + }, + + _select : function(model, selected) { + model.selected = selected; + }, + + _showTable : function() { + if (this.bulkImportCollection.length === 0) { + this.table.show(new EmptyView({ folder : this.folder })); + return; + } + + this.importGrid = new Backgrid.Grid({ + columns : this.columns, + collection : this.bulkImportCollection, + className : 'table table-hover' + }); + + this.table.show(this.importGrid); + } +}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs new file mode 100644 index 000000000..e07b37e8e --- /dev/null +++ b/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs @@ -0,0 +1,13 @@ +
+ +
+
+ Disabled artists are possible duplicates. If the match is incorrect, update the MB Id cell to import the proper artist. +
+
+ +
+
+
+
+
diff --git a/src/UI/AddArtist/BulkImport/EmptyView.js b/src/UI/AddArtist/BulkImport/EmptyView.js new file mode 100644 index 000000000..a3c635533 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/EmptyView.js @@ -0,0 +1,10 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddArtist/BulkImport/EmptyViewTemplate', + + initialize : function (options) { + this.templateHelpers = {}; + this.templateHelpers.folder = options.folder; + } +}); diff --git a/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs b/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs new file mode 100644 index 000000000..b2d80a15c --- /dev/null +++ b/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs @@ -0,0 +1,3 @@ +
+ No artists found in folder {{folder}}. Have you already added all of them? +
diff --git a/src/UI/AddArtist/BulkImport/ForeignIdCell.js b/src/UI/AddArtist/BulkImport/ForeignIdCell.js new file mode 100644 index 000000000..4b22dcd69 --- /dev/null +++ b/src/UI/AddArtist/BulkImport/ForeignIdCell.js @@ -0,0 +1,57 @@ +var vent = require('vent'); +var _ = require('underscore'); +var $ = require('jquery'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var CommandController = require('../../Commands/CommandController'); + +module.exports = NzbDroneCell.extend({ + className : 'foreignId-cell', + + events : { + 'blur input.foreignId-input' : '_updateId' + }, + + render : function() { + this.$el.empty(); + + this.$el.html(''); + + return this; + }, + + _updateId : function() { + var field = this.$el.find('.x-foreignId'); + var data = field.val(); + + var promise = $.ajax({ + url : window.NzbDrone.ApiRoot + '/artist/lookup?term=lidarrid:' + data, + type : 'GET', + }); + + field.prop('disabled', true); + + var icon = this.$('.icon-lidarr-info'); + + icon.removeClass('hidden'); + + icon.spinForPromise(promise); + var _self = this; + var cacheMonitored = this.model.get('monitored'); + var cacheProfile = this.model.get('profileId'); + var cachePath = this.model.get('path'); + var cacheRoot = this.model.get('rootFolderPath'); + + promise.success(function(response) { + _self.model.set(response[0]); + _self.model.set('monitored', cacheMonitored); + _self.model.set('profileId', cacheProfile); + _self.model.set('path', cachePath); + field.prop('disabled', false); + }); + + promise.error(function(request, status, error) { + console.error('Status: ' + status, 'Error: ' + error); + field.prop('disabled', false); + }); + } +}); diff --git a/src/UI/Artist/ArtistCollection.js b/src/UI/Artist/ArtistCollection.js index 8f3822e9c..fe4b7d52b 100644 --- a/src/UI/Artist/ArtistCollection.js +++ b/src/UI/Artist/ArtistCollection.js @@ -24,6 +24,27 @@ var Collection = PageableCollection.extend({ mode : 'client', + importFromList : function(models) { + var self = this; + + var proxy = _.extend(new Backbone.Model(), { + id : '', + + url : self.url + '/import', + + toJSON : function() { + return models; + } + }); + + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true}); + this.trigger('save', this); + }); + + return proxy.save(); + }, + save : function() { var self = this;