From 426448ed98e0f909ab148aab1d0aa6dbf8e9792b Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sun, 25 Dec 2016 12:43:05 +0100 Subject: [PATCH 01/40] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 495dd4155..ce1a925b3 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # Sonarr # - -Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available. +This fork of Sonarr aims to turn it into something like Couchpotato. +At the moment almost nothing is implemented. ## Major Features Include: ## From 20dbdfb344a2e0371a4625a6f572387088972eb7 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 27 Dec 2016 18:31:38 +0100 Subject: [PATCH 02/40] Added first iteration of adding movies. Currently working: - Searching for new Movies on IMDb (very hacky) - Adding movie as a series with one season and episode (very hacky) - Rarbg.to indexer. (somewhat hacky) TODO: - Tweak release specifications so that they do not cause exceptions. - Add Movie struct so that searching for ones is not so hacky. - rework movies UI. --- .../DecisionEngine/DownloadDecisionMaker.cs | 8 +- .../Specifications/FullSeasonSpecification.cs | 2 +- .../Search/SeasonMatchSpecification.cs | 3 +- .../SingleEpisodeSearchMatchSpecification.cs | 6 +- .../Indexers/Rarbg/RarbgRequestGenerator.cs | 7 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 106 +++++++++++++++++- src/UI/Navbar/NavbarLayoutTemplate.hbs | 4 +- 7 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index d86653478..5de2ae56d 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -80,7 +80,8 @@ namespace NzbDrone.Core.DecisionEngine if (remoteEpisode.Series == null) { - decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown Series")); + remoteEpisode.DownloadAllowed = true; //Fuck you :) + decision = GetDecisionForReport(remoteEpisode, searchCriteria); } else if (remoteEpisode.Episodes.Empty()) { @@ -143,8 +144,9 @@ namespace NzbDrone.Core.DecisionEngine { e.Data.Add("report", remoteEpisode.Release.ToJson()); e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); - _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title); - return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message)); + _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title + ", with spec: " + spec.GetType().Name); + //return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));//TODO UPDATE SPECS! + //return null; } return null; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index 023b6be60..c2d86afe8 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (subject.Episodes.Any(e => !e.AirDateUtc.HasValue || e.AirDateUtc.Value.After(DateTime.UtcNow))) { _logger.Debug("Full season release {0} rejected. All episodes haven't aired yet.", subject.Release.Title); - return Decision.Reject("Full season release rejected. All episodes haven't aired yet."); + //return Decision.Reject("Full season release rejected. All episodes haven't aired yet."); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index b09d888ec..56e986e19 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -28,7 +28,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + //return Decision.Reject("Wrong season"); + //Unnecessary for Movies } return Decision.Accept(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs index fb056734f..dfaa711ff 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -29,19 +29,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + //return Decision.Reject("Wrong season"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Any()) { _logger.Debug("Full season result during single episode search, skipping."); - return Decision.Reject("Full season pack"); + //return Decision.Reject("Full season pack"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Contains(singleEpisodeSpec.EpisodeNumber)) { _logger.Debug("Episode number does not match searched episode number, skipping."); - return Decision.Reject("Wrong episode"); + //return Decision.Reject("Wrong episode"); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs index 3b43e0f35..798d8ec4e 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs @@ -88,12 +88,13 @@ namespace NzbDrone.Core.Indexers.Rarbg if (tvdbId.HasValue) { - requestBuilder.AddQueryParam("search_tvdb", tvdbId.Value); + string imdbId = string.Format("tt{0:D7}", tvdbId); + requestBuilder.AddQueryParam("search_imdb", imdbId); } if (query.IsNotNullOrWhiteSpace()) { - requestBuilder.AddQueryParam("search_string", string.Format(query, args)); + //requestBuilder.AddQueryParam("search_string", string.Format(query, args)); } if (!Settings.RankedOnly) @@ -101,7 +102,7 @@ namespace NzbDrone.Core.Indexers.Rarbg requestBuilder.AddQueryParam("ranked", "0"); } - requestBuilder.AddQueryParam("category", "18;41"); + requestBuilder.AddQueryParam("category", "movies"); requestBuilder.AddQueryParam("limit", "100"); requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); requestBuilder.AddQueryParam("format", "json_extended"); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 3c1ca6740..3aaaf41aa 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Tv; +using Newtonsoft.Json; namespace NzbDrone.Core.MetadataSource.SkyHook { @@ -37,7 +38,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; - var httpResponse = _httpClient.Get(httpRequest); + string imdbId = string.Format("tt{0:D7}", tvdbSeriesId); + + var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i="+ imdbId + "&plot=full&r=json"); + + var httpResponse = _httpClient.Get(imdbRequest); if (httpResponse.HasHttpError) { @@ -51,8 +56,43 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - var episodes = httpResponse.Resource.Episodes.Select(MapEpisode); - var series = MapSeries(httpResponse.Resource); + var response = httpResponse.Content; + + dynamic json = JsonConvert.DeserializeObject(response); + + var series = new Series(); + + series.Title = json.Title; + series.TitleSlug = series.Title.ToLower().Replace(" ", "-"); + series.Overview = json.Plot; + series.CleanTitle = series.Title; + series.TvdbId = tvdbSeriesId; + string airDateStr = json.Released; + DateTime airDate = DateTime.Parse(airDateStr); + series.FirstAired = airDate; + series.Year = airDate.Year; + series.ImdbId = imdbId; + series.Images = new List(); + string url = json.Poster; + var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); + series.Images.Add(imdbPoster); + + var season = new Season(); + season.SeasonNumber = 1; + season.Monitored = true; + series.Seasons.Add(season); + + + var episode = new Episode(); + + episode.AirDate = airDate.ToShortTimeString(); + episode.Title = json.Title; + episode.SeasonNumber = 1; + episode.EpisodeNumber = 1; + episode.Overview = series.Overview; + episode.AirDate = airDate.ToShortDateString(); + + var episodes = new List { episode }; return new Tuple>(series, episodes.ToList()); } @@ -89,6 +129,66 @@ namespace NzbDrone.Core.MetadataSource.SkyHook .AddQueryParam("term", title.ToLower().Trim()) .Build(); + var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_"); + + var firstChar = searchTerm.First(); + + var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/"+firstChar+"/" + searchTerm + ".json"); + + var response = _httpClient.Get(imdbRequest); + + var imdbCallback = "imdb$" + searchTerm + "("; + + var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")"); + + dynamic json = JsonConvert.DeserializeObject(responseCleaned); + + var imdbMovies = new List(); + + foreach (dynamic entry in json.d) + { + var imdbMovie = new Series(); + imdbMovie.ImdbId = entry.id; + string noTT = imdbMovie.ImdbId.Replace("tt", ""); + try + { + imdbMovie.TvdbId = (int)Double.Parse(noTT); + } + catch + { + imdbMovie.TvdbId = 0; + } + try + { + imdbMovie.SortTitle = entry.l; + imdbMovie.Title = entry.l; + string titleSlug = entry.l; + imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-"); + imdbMovie.Year = entry.y; + imdbMovie.Images = new List(); + try + { + string url = entry.i[0]; + var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); + imdbMovie.Images.Add(imdbPoster); + } + catch (Exception e) + { + _logger.Debug(entry); + continue; + } + + imdbMovies.Add(imdbMovie); + } + catch + { + + } + + } + + return imdbMovies; + var httpResponse = _httpClient.Get>(httpRequest); return httpResponse.Resource.SelectList(MapSeries); diff --git a/src/UI/Navbar/NavbarLayoutTemplate.hbs b/src/UI/Navbar/NavbarLayoutTemplate.hbs index 75cfc096f..c3c70d7f5 100644 --- a/src/UI/Navbar/NavbarLayoutTemplate.hbs +++ b/src/UI/Navbar/NavbarLayoutTemplate.hbs @@ -19,7 +19,7 @@ - \ No newline at end of file + From 0b765d10fed8efefd1b0afa95d4cb031f0b0c2b1 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 27 Dec 2016 18:48:59 +0100 Subject: [PATCH 03/40] Updated some text to say movies instead of series --- src/UI/AddSeries/AddSeriesLayoutTemplate.hbs | 3 +-- src/UI/AddSeries/AddSeriesViewTemplate.hbs | 2 +- src/UI/Navbar/NavbarLayoutTemplate.hbs | 2 +- src/UI/Series/Index/EmptyTemplate.hbs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs index ab6e5e6c0..097a7ed75 100644 --- a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs @@ -5,7 +5,7 @@ Import existing series on disk - + @@ -14,4 +14,3 @@
- diff --git a/src/UI/AddSeries/AddSeriesViewTemplate.hbs b/src/UI/AddSeries/AddSeriesViewTemplate.hbs index 18ed2ffb3..8048f48ae 100644 --- a/src/UI/AddSeries/AddSeriesViewTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesViewTemplate.hbs @@ -11,7 +11,7 @@ {{#if folder}} {{else}} - + {{/if}} diff --git a/src/UI/Navbar/NavbarLayoutTemplate.hbs b/src/UI/Navbar/NavbarLayoutTemplate.hbs index c3c70d7f5..6009587df 100644 --- a/src/UI/Navbar/NavbarLayoutTemplate.hbs +++ b/src/UI/Navbar/NavbarLayoutTemplate.hbs @@ -37,7 +37,7 @@
- +
diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs index abca7f764..696d4f9e5 100644 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ b/src/UI/Series/Index/EmptyTemplate.hbs @@ -9,7 +9,7 @@ From 0b278c7db858680f10578899311de5a7471cd724 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Wed, 28 Dec 2016 17:13:18 +0100 Subject: [PATCH 04/40] Searching for movie now works with downloading. They also get imported fine. Additionally, a whole series (or movie in this case) can now be downloaded manually. Note: It probably won't start downloading missed releases. Only manually clicking search for is working ATM. --- .../DecisionEngine/DownloadDecisionMaker.cs | 5 ++-- .../Specifications/FullSeasonSpecification.cs | 4 ++-- .../MetadataSource/SkyHook/SkyHookProxy.cs | 8 +++++-- src/NzbDrone.Core/Parser/Parser.cs | 13 +++++++++- src/NzbDrone.Core/Parser/ParsingService.cs | 10 ++++---- src/NzbDrone.Core/Qualities/Quality.cs | 2 +- src/UI/Cells/EpisodeActionsCell.js | 3 ++- src/UI/Controller.js | 4 ++-- src/UI/Handlebars/Helpers/Series.js | 4 ++-- src/UI/Series/Details/InfoViewTemplate.hbs | 4 ++-- src/UI/Series/Details/SeriesDetailsLayout.js | 24 +++++++++++++++---- .../Series/Details/SeriesDetailsTemplate.hbs | 13 ++++++---- src/UI/Series/Index/SeriesIndexLayout.js | 2 +- 13 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 5de2ae56d..c87a3bc16 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -80,8 +80,9 @@ namespace NzbDrone.Core.DecisionEngine if (remoteEpisode.Series == null) { - remoteEpisode.DownloadAllowed = true; //Fuck you :) - decision = GetDecisionForReport(remoteEpisode, searchCriteria); + //remoteEpisode.DownloadAllowed = true; //Fuck you :) + //decision = GetDecisionForReport(remoteEpisode, searchCriteria); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Movie not Found.")); } else if (remoteEpisode.Episodes.Empty()) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 7397c13e7..853f6b1b7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -17,8 +17,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { if (localEpisode.ParsedEpisodeInfo.FullSeason) { - _logger.Debug("Single episode file detected as containing all episodes in the season"); - return Decision.Reject("Single episode file contains all episodes in seasons"); + //_logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah + //return Decision.Reject("Single episode file contains all episodes in seasons"); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 3aaaf41aa..785257c94 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook series.Title = json.Title; series.TitleSlug = series.Title.ToLower().Replace(" ", "-"); series.Overview = json.Plot; - series.CleanTitle = series.Title; + series.CleanTitle = Parser.Parser.CleanSeriesTitle(series.Title); series.TvdbId = tvdbSeriesId; string airDateStr = json.Released; DateTime airDate = DateTime.Parse(airDateStr); @@ -76,6 +76,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook string url = json.Poster; var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); series.Images.Add(imdbPoster); + string runtime = json.Runtime; + int runtimeNum = 0; + int.TryParse(runtime.Replace("min", "").Trim(), out runtimeNum); + series.Runtime = runtimeNum; var season = new Season(); season.SeasonNumber = 1; @@ -85,7 +89,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var episode = new Episode(); - episode.AirDate = airDate.ToShortTimeString(); + episode.AirDate = airDate.ToBestDateString(); episode.Title = json.Title; episode.SeasonNumber = 1; episode.EpisodeNumber = 1; diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9b8759bd1..f482c2c9a 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -26,6 +26,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:\W*S?(?(?\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Matches Movie name with AirYear + new Regex(@"^(?.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -296,6 +300,8 @@ namespace NzbDrone.Core.Parser public static ParsedEpisodeInfo ParseTitle(string title) { + + ParsedEpisodeInfo realResult = null; try { if (!ValidateBeforeParsing(title)) return null; @@ -342,6 +348,8 @@ namespace NzbDrone.Core.Parser } } + + foreach (var regex in ReportTitleRegex) { var match = regex.Matches(simpleTitle); @@ -383,6 +391,8 @@ namespace NzbDrone.Core.Parser Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); } + realResult = result; + return result; } } @@ -401,7 +411,7 @@ namespace NzbDrone.Core.Parser } Logger.Debug("Unable to parse {0}", title); - return null; + return realResult; } public static string ParseSeriesName(string title) @@ -525,6 +535,7 @@ namespace NzbDrone.Core.Parser int airYear; int.TryParse(matchCollection[0].Groups["airyear"].Value, out airYear); + //int.TryParse(matchCollection[0].Groups["year"].Value, out airYear); ParsedEpisodeInfo result; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 4ab4fd4c7..777c5d0ed 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Parser if (parsedEpisodeInfo == null) { - return _seriesService.FindByTitle(title); + return _seriesService.FindByTitle(title); //Here we have a problem since it is not possible for movies to find a scene mapping, so these releases are always rejected :( } var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); @@ -252,10 +252,12 @@ namespace NzbDrone.Core.Parser { Series series = null; + /*var localEpisode = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + var sceneMappingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle); - if (sceneMappingTvdbId.HasValue) + if (localEpisode != null) { - if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMappingTvdbId.Value) + if (searchCriteria != null && searchCriteria.Series.TvdbId == localEpisode.TvdbId) { return searchCriteria.Series; } @@ -269,7 +271,7 @@ namespace NzbDrone.Core.Parser } return series; - } + }*/ //This is only to find scene mapping should not be necessary for movies. if (searchCriteria != null) { diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 6476e5766..2230b7320 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Qualities new QualityDefinition(Quality.WEBDL720p) { Weight = 8, MinSize = 0, MaxSize = 100 }, new QualityDefinition(Quality.Bluray720p) { Weight = 9, MinSize = 0, MaxSize = 100 }, new QualityDefinition(Quality.WEBDL1080p) { Weight = 10, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.Bluray1080p) { Weight = 11, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.Bluray1080p) { Weight = 11, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.HDTV2160p) { Weight = 12, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.WEBDL2160p) { Weight = 13, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.Bluray2160p) { Weight = 14, MinSize = 0, MaxSize = null }, diff --git a/src/UI/Cells/EpisodeActionsCell.js b/src/UI/Cells/EpisodeActionsCell.js index 383942d34..0a8d20211 100644 --- a/src/UI/Cells/EpisodeActionsCell.js +++ b/src/UI/Cells/EpisodeActionsCell.js @@ -35,10 +35,11 @@ module.exports = NzbDroneCell.extend({ }, _manualSearch : function() { + console.warn(this.cellValue); vent.trigger(vent.Commands.ShowEpisodeDetails, { episode : this.cellValue, hideSeriesLink : true, openingTab : 'search' }); } -}); \ No newline at end of file +}); diff --git a/src/UI/Controller.js b/src/UI/Controller.js index f1e4032ab..ef901c60a 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -13,7 +13,7 @@ var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout'); module.exports = NzbDroneController.extend({ addSeries : function(action) { - this.setTitle('Add Series'); + this.setTitle('Add Movie'); this.showMainRegion(new AddSeriesLayout({ action : action })); }, @@ -56,4 +56,4 @@ module.exports = NzbDroneController.extend({ this.setTitle('Series Editor'); this.showMainRegion(new SeriesEditorLayout()); } -}); \ No newline at end of file +}); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 2c8a96bed..4bac9e659 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -11,7 +11,7 @@ Handlebars.registerHelper('poster', function() { if (!poster[0].url.match(/^https?:\/\//)) { return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); } else { - var url = poster[0].url.replace(/^https?\:/, ''); + var url = poster[0].url.replace(/^https?\:/, 'https://'); //IMDb posters need https to work, k? return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); } } @@ -28,7 +28,7 @@ Handlebars.registerHelper('imdbUrl', function() { }); Handlebars.registerHelper('tvdbUrl', function() { - return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId; + return 'http://imdb.com/title/tt' + this.tvdbId; }); Handlebars.registerHelper('tvRageUrl', function() { diff --git a/src/UI/Series/Details/InfoViewTemplate.hbs b/src/UI/Series/Details/InfoViewTemplate.hbs index b52130246..666003f77 100644 --- a/src/UI/Series/Details/InfoViewTemplate.hbs +++ b/src/UI/Series/Details/InfoViewTemplate.hbs @@ -29,9 +29,9 @@ </div> <div class="col-md-3"> <span class="series-info-links"> - <a href="{{traktUrl}}" class="label label-info">Trakt</a> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a> - <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a> + <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a>--> {{#if imdbId}} <a href="{{imdbUrl}}" class="label label-info">IMDB</a> diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js index f33cb0414..d869b47c9 100644 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ b/src/UI/Series/Details/SeriesDetailsLayout.js @@ -32,7 +32,8 @@ module.exports = Marionette.Layout.extend({ refresh : '.x-refresh', rename : '.x-rename', search : '.x-search', - poster : '.x-series-poster' + poster : '.x-series-poster', + manualSearch : '.x-manual-search' }, events : { @@ -41,7 +42,8 @@ module.exports = Marionette.Layout.extend({ 'click .x-edit' : '_editSeries', 'click .x-refresh' : '_refreshSeries', 'click .x-rename' : '_renameSeries', - 'click .x-search' : '_seriesSearch' + 'click .x-search' : '_seriesSearch', + 'click .x-manual-search' : '_manualSearchM' }, initialize : function() { @@ -178,11 +180,11 @@ module.exports = Marionette.Layout.extend({ if (self.model.get('id') !== seriesId) { return []; } - + if (sceneSeasonNumber === undefined) { sceneSeasonNumber = seasonNumber; } - + return _.where(self.model.get('alternateTitles'), function(alt) { return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; @@ -254,5 +256,17 @@ module.exports = Marionette.Layout.extend({ } else { $('body').removeClass('backdrop'); } + }, + + _manualSearchM : function() { + console.warn("Manual Search started"); + console.warn(this.model.get("seriesId")); + console.warn(this.model) + console.warn(this.episodeCollection); + vent.trigger(vent.Commands.ShowEpisodeDetails, { + episode : this.episodeCollection.models[0], + hideSeriesLink : true, + openingTab : 'search' + }); } -}); \ No newline at end of file +}); diff --git a/src/UI/Series/Details/SeriesDetailsTemplate.hbs b/src/UI/Series/Details/SeriesDetailsTemplate.hbs index 818cee455..04978e3d4 100644 --- a/src/UI/Series/Details/SeriesDetailsTemplate.hbs +++ b/src/UI/Series/Details/SeriesDetailsTemplate.hbs @@ -5,23 +5,26 @@ <div class="col-md-12 col-lg-10"> <div> <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire series"/> + <i class="x-monitored" title="Toggle monitored state for movie"/> {{title}} <div class="series-actions pull-right"> <div class="x-episode-file-editor"> - <i class="icon-sonarr-episode-file" title="Modify episode files for series"/> + <i class="icon-sonarr-episode-file" title="Modify episode files for movie"/> </div> <div class="x-refresh"> - <i class="icon-sonarr-refresh icon-can-spin" title="Update series info and scan disk"/> + <i class="icon-sonarr-refresh icon-can-spin" title="Update movie info and scan disk"/> </div> <div class="x-rename"> <i class="icon-sonarr-rename" title="Preview rename for all episodes"/> </div> <div class="x-search"> - <i class="icon-sonarr-search" title="Search for monitored episodes in this series"/> + <i class="icon-sonarr-search" title="Search for movie"/> + </div> + <div class="x-manual-search"> + <i class="icon-sonarr-search-manual" title="Manual Search"/> </div> <div class="x-edit"> - <i class="icon-sonarr-edit" title="Edit series"/> + <i class="icon-sonarr-edit" title="Edit movie"/> </div> </div> </h1> diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index f5f47b983..f79d17a76 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -80,7 +80,7 @@ module.exports = Marionette.Layout.extend({ collapse : true, items : [ { - title : 'Add Series', + title : 'Add Movie', icon : 'icon-sonarr-add', route : 'addseries' }, From 837c2683df3e585888dbd0ce3a261d7967b7b2e1 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Wed, 28 Dec 2016 18:43:31 +0100 Subject: [PATCH 05/40] Update readme.md --- readme.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index ce1a925b3..26695bc2b 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,20 @@ # Sonarr # This fork of Sonarr aims to turn it into something like Couchpotato. -At the moment almost nothing is implemented. + +## Currently working: +* Adding new movies (Note: Movies are currently added as one series with one season and one episode. This will change in the future) +* Manually searching for releases of movies. +* Automatically searching for releases. +* Rarbg.to indexer (Other indexers are coming, I just need to find the right categories) +* Everything that has nothing to do with series from Sonarr should be working as well. + +## Planned Features: +* Scanning PreDB to know when a new release is available. +* Fixing the other Indexers. +* Fixing how movies are stored and displayed. +* Importing of Sonarr config. +* New TorrentPotato Indexer. ## Major Features Include: ## From 2cb27240dc69acaba2daf89bedff37b76f647ad6 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Wed, 28 Dec 2016 22:37:06 +0100 Subject: [PATCH 06/40] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 26695bc2b..2ae43c9fb 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# Sonarr # +# Radarr # This fork of Sonarr aims to turn it into something like Couchpotato. From 40d7590f80675fd4ab8a1cc9b63130e329cebb37 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Thu, 29 Dec 2016 14:06:51 +0100 Subject: [PATCH 07/40] First implementation of completely rewriting the way Radarr handles movies. Searching for new movies is now mostly feature complete. --- .DS_Store | Bin 0 -> 8196 bytes gulp/less.js | 4 +- src/.DS_Store | Bin 0 -> 8196 bytes src/NzbDrone.Api/NzbDrone.Api.csproj | 2 + src/NzbDrone.Api/Series/MovieLookupModule.cs | 44 +++ src/NzbDrone.Api/Series/MovieResource.cs | 178 ++++++++++++ .../Exceptions/MovieNotFoundExceptions.cs | 27 ++ .../MetadataSource/IProvideMovieInfo.cs | 11 + .../MetadataSource/ISearchForNewMovie.cs | 10 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 213 ++++++++------ src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + src/NzbDrone.Core/Tv/Movie.cs | 50 ++++ src/NzbDrone.Core/Tv/MovieStatusType.cs | 9 + src/UI/.DS_Store | Bin 0 -> 8196 bytes src/UI/AddMovies/AddMoviesCollection.js | 22 ++ src/UI/AddMovies/AddMoviesLayout.js | 53 ++++ src/UI/AddMovies/AddMoviesLayoutTemplate.hbs | 16 ++ src/UI/AddMovies/AddMoviesView.js | 183 ++++++++++++ src/UI/AddMovies/AddMoviesViewTemplate.hbs | 24 ++ src/UI/AddMovies/EmptyView.js | 5 + src/UI/AddMovies/EmptyViewTemplate.hbs | 3 + src/UI/AddMovies/ErrorView.js | 13 + src/UI/AddMovies/ErrorViewTemplate.hbs | 7 + .../AddExistingSeriesCollectionView.js | 51 ++++ ...ddExistingSeriesCollectionViewTemplate.hbs | 5 + .../Existing/UnmappedFolderCollection.js | 20 ++ .../AddMovies/Existing/UnmappedFolderModel.js | 3 + .../AddMovies/MonitoringTooltipTemplate.hbs | 18 ++ .../AddMovies/MoviesTypeSelectionPartial.hbs | 3 + src/UI/AddMovies/NotFoundView.js | 13 + src/UI/AddMovies/NotFoundViewTemplate.hbs | 7 + .../RootFolders/RootFolderCollection.js | 10 + .../RootFolders/RootFolderCollectionView.js | 8 + .../RootFolderCollectionViewTemplate.hbs | 13 + .../RootFolders/RootFolderItemView.js | 28 ++ .../RootFolderItemViewTemplate.hbs | 9 + .../AddMovies/RootFolders/RootFolderLayout.js | 77 +++++ .../RootFolders/RootFolderLayoutTemplate.hbs | 36 +++ .../AddMovies/RootFolders/RootFolderModel.js | 8 + .../RootFolderSelectionPartial.hbs | 11 + .../AddMovies/SearchResultCollectionView.js | 29 ++ src/UI/AddMovies/SearchResultView.js | 272 ++++++++++++++++++ src/UI/AddMovies/SearchResultViewTemplate.hbs | 101 +++++++ .../StartingSeasonSelectionPartial.hbs | 13 + src/UI/AddMovies/addMovies.less | 177 ++++++++++++ src/UI/Controller.js | 6 + src/UI/Handlebars/Helpers/Series.js | 2 +- src/UI/Movies/MovieModel.js | 13 + src/UI/Movies/MoviesCollection.js | 120 ++++++++ src/UI/Router.js | 4 +- src/UI/Series/Index/EmptyTemplate.hbs | 2 +- src/UI/Series/Index/SeriesIndexLayout.js | 2 +- src/UI/index.html | 1 + 53 files changed, 1845 insertions(+), 96 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/NzbDrone.Api/Series/MovieLookupModule.cs create mode 100644 src/NzbDrone.Api/Series/MovieResource.cs create mode 100644 src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs create mode 100644 src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs create mode 100644 src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs create mode 100644 src/NzbDrone.Core/Tv/Movie.cs create mode 100644 src/NzbDrone.Core/Tv/MovieStatusType.cs create mode 100644 src/UI/.DS_Store create mode 100644 src/UI/AddMovies/AddMoviesCollection.js create mode 100644 src/UI/AddMovies/AddMoviesLayout.js create mode 100644 src/UI/AddMovies/AddMoviesLayoutTemplate.hbs create mode 100644 src/UI/AddMovies/AddMoviesView.js create mode 100644 src/UI/AddMovies/AddMoviesViewTemplate.hbs create mode 100644 src/UI/AddMovies/EmptyView.js create mode 100644 src/UI/AddMovies/EmptyViewTemplate.hbs create mode 100644 src/UI/AddMovies/ErrorView.js create mode 100644 src/UI/AddMovies/ErrorViewTemplate.hbs create mode 100644 src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js create mode 100644 src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs create mode 100644 src/UI/AddMovies/Existing/UnmappedFolderCollection.js create mode 100644 src/UI/AddMovies/Existing/UnmappedFolderModel.js create mode 100644 src/UI/AddMovies/MonitoringTooltipTemplate.hbs create mode 100644 src/UI/AddMovies/MoviesTypeSelectionPartial.hbs create mode 100644 src/UI/AddMovies/NotFoundView.js create mode 100644 src/UI/AddMovies/NotFoundViewTemplate.hbs create mode 100644 src/UI/AddMovies/RootFolders/RootFolderCollection.js create mode 100644 src/UI/AddMovies/RootFolders/RootFolderCollectionView.js create mode 100644 src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs create mode 100644 src/UI/AddMovies/RootFolders/RootFolderItemView.js create mode 100644 src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs create mode 100644 src/UI/AddMovies/RootFolders/RootFolderLayout.js create mode 100644 src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs create mode 100644 src/UI/AddMovies/RootFolders/RootFolderModel.js create mode 100644 src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs create mode 100644 src/UI/AddMovies/SearchResultCollectionView.js create mode 100644 src/UI/AddMovies/SearchResultView.js create mode 100644 src/UI/AddMovies/SearchResultViewTemplate.hbs create mode 100644 src/UI/AddMovies/StartingSeasonSelectionPartial.hbs create mode 100644 src/UI/AddMovies/addMovies.less create mode 100644 src/UI/Movies/MovieModel.js create mode 100644 src/UI/Movies/MoviesCollection.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aea3758c75ea4196314b71002ff096f2512214bc GIT binary patch literal 8196 zcmeHMU2GIZ9G_oXV3&<Bg;L6Kgo{ONwbquSwrNDJ*Om$e+t6=XTla2Py0YE9+-_Sw ztoBjGkZABD8e^geO8kh4#ps(M8Z|x{jN*ea`r?Zw>Vq-9_@AA<(gHO;0SP;oncvL( z|NoikZ-2LQGi8h+J!#Z4R>2rkC=28o6<0~ZF7g$rDC{XA1jREim$BUpwf@fJuJDdJ zA_gJ`A_gJ`A_gJ`ZUhEs&lX8pV&9k6sEinh7`Q1J5buWsWr3*>XC(%&4l05p0HHVn z1c~ZgR0tCxrb3*RNES+vp@cFN;SmEeoca@?Un;~|i87oaJbWOW8Q}>9!R$1CB5-F& zNsP*ffrx=C84zEeV&<_t%Vwq*p5JZD^Xuv^qNuoJ+42>Nx>6G#Ub^zZo^f}&$JhNy zFRkZ?gO+8OnXuli`{RXruj|_Lbv@H>nbTvutkrdV-Ez$Qg3yrx%Q4*Pv^(h--bjxe zZ@@DA@iD%xJ8w==Xt(Z|{g!8CEZg#rD6063@rq-|lF5deL^4@FQ<FH>*jQJS*q*#+ zW`@77C>yIATJ{a)W}bTO^;2)0KJ(`JPbC8g-y&@7WsH1wi4NPF>Xz3TMJF;jNyqmK z=+JJ7rCZy|+jos|KIxhJO<VV^DYH%YO>c~^_K2n3S=R{{Kah3p$q9!nrCHt1PLi|P zV%t5|QR$t{ySCl!dX{gw&d{K59`=Pz47A8Jc))Y>erqt;phe?LuQh_8uAB(=`pvvY zsH$gIt&b;eyW{TdO*<~E=A~=2GOc`IN-$`feba*6A>GTGj$t_m4-n;!Z#`_8d9@TZ zYOkYDm@;ZZY@^EOlo5lLyw5d+j}cg68)BOkWmxpL=1nSPw{~l6i^_GS*P}l`4sp9y zt#VK8Jw#7ghSq4c3OCd~ThExb?5fw23hz`0rsVo%XPMThsDoK|VuF_E$-d^;y$b(a z85I-n)jhu}XF8Xblv0!-VQDc8(@-i?hvkk?nW~b{y0Y-dD^qo{%EjihiPf<*+rvg# zjvZmg+4JmG_5qt?=h!FgOZGMUj{U@bWxuiC*&pmLFaX6UK`CO`h&ZZHk8RkF1~j7; zZP<kl>_s2?F^D0IVH_3?!G@0+Jch^d1fIdOIEm+Q3h&@uyoZnQG0x*Fe1jk4s)`C4 z?I<i*B7P(czp|v!@XD>|<kdAA@3?>W>OJi@tVS=c63Sd#wyu1AVoP;xvS~+p{%Hpq zTqv(VQBpF2lnPS0ObLsXT0#1aZ_+j|rTjk9q<L5oqUS3v2ybnZR;j9-1&#RDc$Gr( z=S9+XXZ$W9ZeA>{b=o$eW4=sU8?;6tbY3E@%^E4SQ{~HLb7#Co2$w6;d275~RXF~a zM*mZxUywe(Wk0iDNS}YQf3OnkkswXhq6zn5C(@+Jc07PCbYnk;F@jOjq>e{OlM`@| z!x0?Cqohrt%qMXiPvZn$z>9bZFXI)Q#u>bYvv?bGIEN1}X;mt6Ua6h3T_^)3hf1C5 z$vd{|93)#o*9Er-uM-2sa^4DY{%>FU{{K3yE}AxCAY$NJFo2S_p0+g2wA7nfoV62_ zAEGRh@SBwwgix{QB7oZS|1hL_f?P#RD#Tfd)I;f-Uj$tK%QxEpqy0Zn;>}h33uH_W AFaQ7m literal 0 HcmV?d00001 diff --git a/gulp/less.js b/gulp/less.js index 76e04b8dc..fecb67cb0 100644 --- a/gulp/less.js +++ b/gulp/less.js @@ -19,13 +19,15 @@ gulp.task('less', function() { paths.src.root + 'Series/series.less', paths.src.root + 'Activity/activity.less', paths.src.root + 'AddSeries/addSeries.less', + paths.src.root + 'AddMovies/addMovies.less', paths.src.root + 'Calendar/calendar.less', paths.src.root + 'Cells/cells.less', paths.src.root + 'ManualImport/manualimport.less', paths.src.root + 'Settings/settings.less', paths.src.root + 'System/Logs/logs.less', paths.src.root + 'System/Update/update.less', - paths.src.root + 'System/Info/info.less' + paths.src.root + 'System/Info/info.less', + ]; return gulp.src(src) diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f133d93819ae6f9ba0e265340d8df1763e68e1d1 GIT binary patch literal 8196 zcmeHMU2GIZ9G~B{z%CnM3I)pb8ZH*GXss>R+NKe?UR!GUkcNIkrS9FXbY;7Hx!txv zt@csH5H$D^jWJOKC4R(2F#2YQMvYGfqxfKqzWAbv`e2MN{%2>el#d!;6cTnWGr!sS z|Nk>{zxnOX%^qV6nF*tTu?ohRLRlc!sJKcJc9E}0v9P6-5ERd{wwtB;pV_=^UEv)y zL<~d>L<~d>L<~d>Tn`M;o-LBHz`if7Q5i81F>pgNAf69N$^z3N&Pog(9aIEI077vD z2olw~s1PPYOouotkt~!TLkVRl!XpM`IJGB3zjTPR5@k3;c=$j#GQtxIg3+n}WZ=$_ zmKc>00}%sPG9YfB80%-<*(onS|NQRx_4OAJD=A&Hc!{De)x?LFExo^Y%$@4>b$`Ok z=!Lz(m1UUOu->ElW5s%(>)La5Jv(5TQ=`1R-F1B3a?HZK(2+sQG2E$)JK-4KNU!W~ z&@%k7QNF6DU`|qKkM5WQmS<%x+wu=7s`!lZis|W8s;M@aN;S;XCa0U5>uZyZsk>%o z_`8Zyxv{Bj=Wu@JiC11d@!H8#ub=x^et(3|5VrC%K0YauVSAH3l8o3<@iIe@@$Eb^ zw436Y_Kww^TSqyc@XXz&t^3xb*`fQUH_DfL#8G$7b;79+=3IMX+#yR@PPcOt<ZQOt zcCU3<dgltRZTGmI<y)>ZJmi}PePI(FU1A#A<GBUDJ?L!6qW-1V3PI1Vya@IN%z{U# zs;8H&NhEK%?as!Q&F7c%vK3mnwt8?<@MoL-Q-a!I-OHJdVLAKv5aEt*J!qK)wG6dt zpQDePGHPwSQsrlq5rd|@(=~*T5m@4D<LeY<uV`;Cm{iJU?dJG;mFr5MM_)j8ajUjb z<(}GifYw-s)@pSMH`IPx&ziPuYS2;&->wc$%K6L2a;;fWhjQ-tI8D!!ZLRUU75=HR zPYk?6_xxRX)44dMw4w|POPgVshEknAcq~}q>U51<>FVNwSEuV`m5Yt1iq*3W+s5{> zJUhgWuxHuJ>^*jdon;@f&)Jvk8}<YHnf=0kWxugMzyOq>6lI8`5((6x0h`c>CbXg* z9oUL4>_9&TFoa=@Vhk1zz=n?*Jc38@7@oq@IF4s<0&n4Myn_$$A<p3oe1-4itYSrl zb`>Wq5#JLAw<{?$yj^3*uddKU*S+1#w{>2(3cavQ=yGNGs?}?f>o?Y=S~h3q);dt& zd~pSOl7b0@R20f(I=ED)6{Oxom9}mn-S?9s&4Y@NJYRBNcxzQ!wW@L!TqHImY7`Pb zk4f9@i93X}d5N^vYnz0M`66j;(wc?Ld8xFvYNXTcDqk!ww<OwxY`G$xHzYb$h2vi- z^dI&48L9JY_9Od=)cHI63rn#ENm67TT5u1xAVZ4m#C_O>9_+?mj9?!rQpZE2$Z<Hx z;}8zxVN#~h<>NSlCvg<d;d#7(7x5BK;uPM%X}pOuIE(i$Dpl%nPN&=Db+HU|9O!hi zx8T^Wv!5(QO&6Uayv7Wa$nh)0`M-1F_y5;;meKGL0}%uNB?Bn!=<UeR2n#)-#aTN^ z`2orz3BOs1K?oI>ya=G{xjzi4o+MWhlMZoKBDGNZ-(Lh={M{Sv|Iz**Xz)fX{svcz B3f2Gs literal 0 HcmV?d00001 diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 4ade4bcdf..b87179adb 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -231,8 +231,10 @@ <Compile Include="Series\SeasonResource.cs" /> <Compile Include="SeasonPass\SeasonPassModule.cs" /> <Compile Include="Series\SeriesEditorModule.cs" /> + <Compile Include="Series\MovieLookupModule.cs" /> <Compile Include="Series\SeriesLookupModule.cs" /> <Compile Include="Series\SeriesModule.cs" /> + <Compile Include="Series\MovieResource.cs" /> <Compile Include="Series\SeriesResource.cs" /> <Compile Include="Series\SeasonStatisticsResource.cs" /> <Compile Include="System\Backup\BackupModule.cs" /> diff --git a/src/NzbDrone.Api/Series/MovieLookupModule.cs b/src/NzbDrone.Api/Series/MovieLookupModule.cs new file mode 100644 index 000000000..0c74df808 --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieLookupModule.cs @@ -0,0 +1,44 @@ +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 MovieLookupModule : NzbDroneRestModule<MovieResource> + { + private readonly ISearchForNewMovie _searchProxy; + + public MovieLookupModule(ISearchForNewMovie searchProxy) + : base("/movies/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var imdbResults = _searchProxy.SearchForNewMovie((string)Request.Query.term); + return MapToResource(imdbResults).AsResponse(); + } + + + private static IEnumerable<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> movies) + { + foreach (var currentSeries in movies) + { + 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.Api/Series/MovieResource.cs b/src/NzbDrone.Api/Series/MovieResource.cs new file mode 100644 index 000000000..1ce197751 --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieResource.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Series +{ + public class MovieResource : RestResource + { + public MovieResource() + { + Monitored = true; + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + //View Only + public string Title { get; set; } + public List<AlternateTitleResource> AlternateTitles { get; set; } + public string SortTitle { get; set; } + public long? SizeOnDisk { get; set; } + public MovieStatusType Status { get; set; } + public string Overview { get; set; } + public DateTime? InCinemas { get; set; } + public List<MediaCover> Images { get; set; } + + public string RemotePoster { get; set; } + public int Year { get; set; } + + //View & Edit + public string Path { get; set; } + public int ProfileId { get; set; } + + //Editing Only + public bool Monitored { get; set; } + public int Runtime { get; set; } + public DateTime? LastInfoSync { get; set; } + public string CleanTitle { get; set; } + public string ImdbId { get; set; } + public string TitleSlug { get; set; } + public string RootFolderPath { get; set; } + public string Certification { get; set; } + public List<string> Genres { get; set; } + public HashSet<int> Tags { get; set; } + public DateTime Added { get; set; } + public Ratings Ratings { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + + //Used to support legacy consumers + public int QualityProfileId + { + get + { + return ProfileId; + } + set + { + if (value > 0 && ProfileId == 0) + { + ProfileId = value; + } + } + } + } + + public static class MovieResourceMapper + { + public static MovieResource ToResource(this Core.Tv.Movie model) + { + if (model == null) return null; + + return new MovieResource + { + Id = model.Id, + + Title = model.Title, + //AlternateTitles + SortTitle = model.SortTitle, + InCinemas = model.InCinemas, + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = model.Status, + Overview = model.Overview, + //NextAiring + //PreviousAiring + Images = model.Images, + + Year = model.Year, + + Path = model.Path, + ProfileId = model.ProfileId, + + Monitored = model.Monitored, + + Runtime = model.Runtime, + LastInfoSync = model.LastInfoSync, + CleanTitle = model.CleanTitle, + ImdbId = model.ImdbId, + TitleSlug = model.TitleSlug, + RootFolderPath = model.RootFolderPath, + Certification = model.Certification, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + Ratings = model.Ratings + }; + } + + public static Core.Tv.Movie ToModel(this MovieResource resource) + { + if (resource == null) return null; + + return new Core.Tv.Movie + { + Id = resource.Id, + + Title = resource.Title, + //AlternateTitles + SortTitle = resource.SortTitle, + InCinemas = resource.InCinemas, + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Overview = resource.Overview, + //NextAiring + //PreviousAiring + Images = resource.Images, + + Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.ProfileId, + + Monitored = resource.Monitored, + + Runtime = resource.Runtime, + LastInfoSync = resource.LastInfoSync, + CleanTitle = resource.CleanTitle, + ImdbId = resource.ImdbId, + TitleSlug = resource.TitleSlug, + RootFolderPath = resource.RootFolderPath, + Certification = resource.Certification, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + Ratings = resource.Ratings + }; + } + + public static Core.Tv.Movie ToModel(this MovieResource resource, Core.Tv.Movie movie) + { + movie.ImdbId = resource.ImdbId; + + movie.Path = resource.Path; + movie.ProfileId = resource.ProfileId; + + movie.Monitored = resource.Monitored; + + movie.RootFolderPath = resource.RootFolderPath; + movie.Tags = resource.Tags; + + return movie; + } + + public static List<MovieResource> ToResource(this IEnumerable<Core.Tv.Movie> movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs new file mode 100644 index 000000000..c2345bd93 --- /dev/null +++ b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs @@ -0,0 +1,27 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Exceptions +{ + public class MovieNotFoundException : NzbDroneException + { + public string ImdbId { get; set; } + + public MovieNotFoundException(string imdbid) + : base(string.Format("Movie with imdbid {0} was not found, it may have been removed from IMDb.", imdbid)) + { + ImdbId = imdbid; + } + + public MovieNotFoundException(string imdbid, string message, params object[] args) + : base(message, args) + { + ImdbId = imdbid; + } + + public MovieNotFoundException(string imdbid, string message) + : base(message) + { + ImdbId = imdbid; + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs new file mode 100644 index 000000000..861564bc4 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MetadataSource +{ + public interface IProvideMovieInfo + { + Movie GetMovieInfo(string ImdbId); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs new file mode 100644 index 000000000..d895075f9 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MetadataSource +{ + public interface ISearchForNewMovie + { + List<Movie> SearchForNewMovie(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 785257c94..8b996fedd 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -14,7 +14,7 @@ using Newtonsoft.Json; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries + public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries, IProvideMovieInfo, ISearchForNewMovie { private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -38,11 +38,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; - string imdbId = string.Format("tt{0:D7}", tvdbSeriesId); - - var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i="+ imdbId + "&plot=full&r=json"); - - var httpResponse = _httpClient.Get(imdbRequest); + var httpResponse = _httpClient.Get<ShowResource>(httpRequest); if (httpResponse.HasHttpError) { @@ -56,49 +52,140 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + var episodes = httpResponse.Resource.Episodes.Select(MapEpisode); + var series = MapSeries(httpResponse.Resource); + + return new Tuple<Series, List<Episode>>(series, episodes.ToList()); + } + + public Movie GetMovieInfo(string ImdbId) + { + var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i=" + ImdbId + "&plot=full&r=json"); + + var httpResponse = _httpClient.Get(imdbRequest); + + if (httpResponse.HasHttpError) + { + if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + throw new MovieNotFoundException(ImdbId); + } + else + { + throw new HttpException(imdbRequest, httpResponse); + } + } + var response = httpResponse.Content; dynamic json = JsonConvert.DeserializeObject(response); - var series = new Series(); + var movie = new Movie(); - series.Title = json.Title; - series.TitleSlug = series.Title.ToLower().Replace(" ", "-"); - series.Overview = json.Plot; - series.CleanTitle = Parser.Parser.CleanSeriesTitle(series.Title); - series.TvdbId = tvdbSeriesId; + movie.Title = json.Title; + movie.TitleSlug = movie.Title.ToLower().Replace(" ", "-"); + movie.Overview = json.Plot; + movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title); string airDateStr = json.Released; DateTime airDate = DateTime.Parse(airDateStr); - series.FirstAired = airDate; - series.Year = airDate.Year; - series.ImdbId = imdbId; - series.Images = new List<MediaCover.MediaCover>(); + movie.InCinemas = airDate; + movie.Year = airDate.Year; + movie.ImdbId = ImdbId; + string imdbRating = json.imdbVotes; + if (imdbRating == "N/A") + { + movie.Status = MovieStatusType.Announced; + } + else + { + movie.Status = MovieStatusType.Released; + } string url = json.Poster; var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); - series.Images.Add(imdbPoster); + movie.Images.Add(imdbPoster); string runtime = json.Runtime; int runtimeNum = 0; int.TryParse(runtime.Replace("min", "").Trim(), out runtimeNum); - series.Runtime = runtimeNum; + movie.Runtime = runtimeNum; - var season = new Season(); - season.SeasonNumber = 1; - season.Monitored = true; - series.Seasons.Add(season); - + return movie; + } - var episode = new Episode(); + public List<Movie> SearchForNewMovie(string title) + { + var lowerTitle = title.ToLower(); - episode.AirDate = airDate.ToBestDateString(); - episode.Title = json.Title; - episode.SeasonNumber = 1; - episode.EpisodeNumber = 1; - episode.Overview = series.Overview; - episode.AirDate = airDate.ToShortDateString(); + if (lowerTitle.StartsWith("imdb:") || lowerTitle.StartsWith("imdbid:")) + { + var slug = lowerTitle.Split(':')[1].Trim(); - var episodes = new List<Episode> { episode }; + string imdbid = slug; - return new Tuple<Series, List<Episode>>(series, episodes.ToList()); + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) + { + return new List<Movie>(); + } + + try + { + return new List<Movie> { GetMovieInfo(imdbid) }; + } + catch (SeriesNotFoundException) + { + return new List<Movie>(); + } + } + + var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_"); + + var firstChar = searchTerm.First(); + + var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/" + firstChar + "/" + searchTerm + ".json"); + + var response = _httpClient.Get(imdbRequest); + + var imdbCallback = "imdb$" + searchTerm + "("; + + var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")"); + + dynamic json = JsonConvert.DeserializeObject(responseCleaned); + + var imdbMovies = new List<Movie>(); + + foreach (dynamic entry in json.d) + { + var imdbMovie = new Movie(); + imdbMovie.ImdbId = entry.id; + try + { + imdbMovie.SortTitle = entry.l; + imdbMovie.Title = entry.l; + string titleSlug = entry.l; + imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-"); + imdbMovie.Year = entry.y; + imdbMovie.Images = new List<MediaCover.MediaCover>(); + try + { + string url = entry.i[0]; + var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); + imdbMovie.Images.Add(imdbPoster); + } + catch (Exception e) + { + _logger.Debug(entry); + continue; + } + + imdbMovies.Add(imdbMovie); + } + catch + { + + } + + } + + return imdbMovies; } public List<Series> SearchForNewSeries(string title) @@ -128,70 +215,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + + var httpRequest = _requestBuilder.Create() .SetSegment("route", "search") .AddQueryParam("term", title.ToLower().Trim()) .Build(); - var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_"); - - var firstChar = searchTerm.First(); - - var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/"+firstChar+"/" + searchTerm + ".json"); - - var response = _httpClient.Get(imdbRequest); - - var imdbCallback = "imdb$" + searchTerm + "("; - - var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")"); - - dynamic json = JsonConvert.DeserializeObject(responseCleaned); - - var imdbMovies = new List<Series>(); - - foreach (dynamic entry in json.d) - { - var imdbMovie = new Series(); - imdbMovie.ImdbId = entry.id; - string noTT = imdbMovie.ImdbId.Replace("tt", ""); - try - { - imdbMovie.TvdbId = (int)Double.Parse(noTT); - } - catch - { - imdbMovie.TvdbId = 0; - } - try - { - imdbMovie.SortTitle = entry.l; - imdbMovie.Title = entry.l; - string titleSlug = entry.l; - imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-"); - imdbMovie.Year = entry.y; - imdbMovie.Images = new List<MediaCover.MediaCover>(); - try - { - string url = entry.i[0]; - var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url); - imdbMovie.Images.Add(imdbPoster); - } - catch (Exception e) - { - _logger.Debug(entry); - continue; - } - - imdbMovies.Add(imdbMovie); - } - catch - { - - } - - } - - return imdbMovies; + var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5d43f5d5f..2d9ac2a47 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -488,6 +488,7 @@ <Compile Include="Exceptions\BadRequestException.cs" /> <Compile Include="Exceptions\DownstreamException.cs" /> <Compile Include="Exceptions\NzbDroneClientException.cs" /> + <Compile Include="Exceptions\MovieNotFoundExceptions.cs" /> <Compile Include="Exceptions\SeriesNotFoundException.cs" /> <Compile Include="Exceptions\ReleaseDownloadException.cs" /> <Compile Include="Exceptions\StatusCodeToExceptions.cs" /> @@ -778,6 +779,8 @@ <Compile Include="Messaging\Events\IEventAggregator.cs" /> <Compile Include="Messaging\Events\IHandle.cs" /> <Compile Include="Messaging\IProcessMessage.cs" /> + <Compile Include="MetadataSource\IProvideMovieInfo.cs" /> + <Compile Include="MetadataSource\ISearchForNewMovie.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ActorResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\EpisodeResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" /> @@ -1067,6 +1070,7 @@ <Compile Include="Tv\RefreshEpisodeService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\Season.cs" /> + <Compile Include="Tv\Movie.cs" /> <Compile Include="Tv\Series.cs" /> <Compile Include="Tv\SeriesAddedHandler.cs" /> <Compile Include="Tv\SeriesScannedHandler.cs" /> @@ -1075,6 +1079,7 @@ <Compile Include="Tv\SeriesService.cs"> <SubType>Code</SubType> </Compile> + <Compile Include="Tv\MovieStatusType.cs" /> <Compile Include="Tv\SeriesStatusType.cs" /> <Compile Include="Tv\SeriesTitleNormalizer.cs" /> <Compile Include="Tv\SeriesTypes.cs" /> diff --git a/src/NzbDrone.Core/Tv/Movie.cs b/src/NzbDrone.Core/Tv/Movie.cs new file mode 100644 index 000000000..ccbe93510 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Movie.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; + +namespace NzbDrone.Core.Tv +{ + public class Movie : ModelBase + { + public Movie() + { + Images = new List<MediaCover.MediaCover>(); + Genres = new List<string>(); + Actors = new List<Actor>(); + Tags = new HashSet<int>(); + } + + public string ImdbId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + public MovieStatusType Status { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + public int ProfileId { get; set; } + public DateTime? LastInfoSync { get; set; } + public int Runtime { get; set; } + public List<MediaCover.MediaCover> Images { get; set; } + public string TitleSlug { get; set; } + public string Path { get; set; } + public int Year { get; set; } + public Ratings Ratings { get; set; } + public List<string> Genres { get; set; } + public List<Actor> Actors { get; set; } + public string Certification { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public DateTime? InCinemas { get; set; } + public LazyLoaded<Profile> Profile { get; set; } + public HashSet<int> Tags { get; set; } +// public AddMovieOptions AddOptions { get; set; } + + public override string ToString() + { + return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe()); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieStatusType.cs b/src/NzbDrone.Core/Tv/MovieStatusType.cs new file mode 100644 index 000000000..9c0bdbed9 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieStatusType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Tv +{ + public enum MovieStatusType + { + TBA = 0, //Nothing yet announced, only rumors, but still IMDb page + Announced = 1, //AirDate is announced + Released = 2 //Has at least one PreDB release + } +} diff --git a/src/UI/.DS_Store b/src/UI/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..df70b98af2a4a591eadb11f72de6def1744bbbb7 GIT binary patch literal 8196 zcmeHMTWl0n7(QPqu-geRh1QmZrVGRtWfR+!mWvU#H)(;&hV7M0*V&zsPMprHJF{D` zX{;|^(1^a67^5hl64VEy^_KWx2u6*GO5g!`GSNq3V&d(=#Q&T#OWG~vi5L>*Oy+#& zod5i1&VKWqbMntJ#?X=1n;ENRjD_gts4A4*AaOgt=cO{grJNAt&zQm7^n{yDTTXgj z@1P-IAYdS1AYdS1AYkBjV1UkSo<a+p`_dZZ0RsU8cO(ORe~8k}G4A8EM6q>HCPDy0 z9s-C&Wr_oYPt?b_kJA!`D~&0m2l%e=LkviFvPXG$!nlvq5~VwXbZ79N3_n7Fcslt- zMR$g{#2^nC2pE{p0AD^!SRZp(fte-o`#<TdWx8Hd(-o90Dqp;0X((Kw@Jp?%IGh}F zCX$}!<=qY~cTm(!-AMc8gyxME%l(dHT`z0tLDQH>sa2hh?P;cM<Yu`Z8ZvF&ndorx zw(cHE%JGIw-5X1()rp)@AlHOu8-u26rcKNAj)lVfN~yJzlPxW+v1m(6^HeN4xn)aJ zEV{X+ZE8yWAQW12PiuSMNOtP@snciPdi$NT=PyVe;O`}DRS7R&m6Bq)g@pX5QTAq$ zl%HmiqAZJaKiIu$S5Hb+^RBVqur$vs7+soYxG8moOI-D49NYg1Lm9`)kK1Ia%xG37 zPo0f+%SxJ$%ifusV_6BuH9ga@M}|G)sK;%5pgE@D1Fn<vI>lhaCXFw9t>iTA%W`LT z(8#%j;qbZTHEW}H-+SNYtq)vUp;oS3Ri!8gh+x|@A2E&GzyxP`L~}ESt(*3dp#rDa zGWu<8+^9q>92Q=+t0T%Gou=F8=-f|NXF}3aw>lCz$Ss{YgK|9-5)SLuN0fe-Ca-FK z$A(5l>3@{ov+DYTZi*?90ZU68ma2!PTT`<V87j!Rt9$&$)+!}3oN>m-Y0|FWwrzVP zGRhyjS9866S;M9&ea)Y}V`sdbrnJ!S8sR?ey3QvW+E9G`8{!*faYMXG7F2#N)v+el z!FIDzmSxA-bL?eyl6}Nx*vITk_AR^2uCia*HTFCEgZ;_=0>d(_fQm{)(11p4L^Im3 z72B{Ko#?@C?7?0P;2;iT6e)}$iyT~dn8H(d8qeS*yn$0VjWc*3AL0vK!1wq8KjI>; z;ul<#Gb<~Kx3@TZiTI6h&Wa}S`d{SAp1rYn>v#3+s_yN)ZN+<Kxs+`6T{S|(+jewZ z7edL~pRGY5JgF?9xT2zjsOQM|5(#_F+B&6vfP`rrT}U<RQoa;IqW8M>_4R5<h>bfp zG*Y#UmV>lziq$LXA|ZNiB|X=xi-o*-wY5!A)N-)^cy;^zWL+Y3&Z|4?$jWGO@JeSR z4FuZcZX(i~O7t6ck^L;>`8WF)i&23ZL`j%266FrWNs?XY#zRP8Ka%JtQKq0FjSMW< zB+bW2oRcKYCoqjCaU9R%1)RW(cm=QGHN1|KIE!<57w=&PpWst`hR<;x-<61TcZo<Z zmx{EI%-NP>AEEZ5u;;o2wOh`>A~|9$kN>+D{{DZ<pBoGqFc2_s`!Im=u4GpS&1a#V zm&e*sx*w*SC*E&bqHv*1gyV!F94EZ~har`t)Rp_heVmp^EtLNE4*`xHod3c3FSK_j GXa50*yC}o} literal 0 HcmV?d00001 diff --git a/src/UI/AddMovies/AddMoviesCollection.js b/src/UI/AddMovies/AddMoviesCollection.js new file mode 100644 index 000000000..f133d4fbb --- /dev/null +++ b/src/UI/AddMovies/AddMoviesCollection.js @@ -0,0 +1,22 @@ +var Backbone = require('backbone'); +var MovieModel = require('../Movies/MovieModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/movies/lookup', + model : MovieModel, + + parse : function(response) { + var self = this; + + _.each(response, function(model) { + model.id = undefined; + + if (self.unmappedFolderModel) { + model.path = self.unmappedFolderModel.get('folder').path; + } + }); + + return response; + } +}); diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js new file mode 100644 index 000000000..904d3f179 --- /dev/null +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -0,0 +1,53 @@ +var vent = require('vent'); +var AppLayout = require('../AppLayout'); +var Marionette = require('marionette'); +var RootFolderLayout = require('./RootFolders/RootFolderLayout'); +//var ExistingMoviesCollectionView = require('./Existing/AddExistingSeriesCollectionView'); +var AddMoviesView = require('./AddMoviesView'); +var ProfileCollection = require('../Profile/ProfileCollection'); +var RootFolderCollection = require('./RootFolders/RootFolderCollection'); +require('../Movies/MoviesCollection'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/AddMoviesLayoutTemplate', + + regions : { + workspace : '#add-movies-workspace' + }, + + events : { + 'click .x-import' : '_importMovies', + 'click .x-add-new' : '_addMovies' + }, + + attributes : { + id : 'add-movies-screen' + }, + + initialize : function() { + ProfileCollection.fetch(); + RootFolderCollection.fetch().done(function() { + RootFolderCollection.synced = true; + }); + }, + + onShow : function() { + this.workspace.show(new AddMoviesView()); + }, + + _folderSelected : function(options) { + //vent.trigger(vent.Commands.CloseModalCommand); + //TODO: Fix this shit. + //this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); + }, + + _importMovies : function() { + this.rootFolderLayout = new RootFolderLayout(); + this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); + AppLayout.modalRegion.show(this.rootFolderLayout); + }, + + _addMovies : function() { + this.workspace.show(new AddMoviesView()); + } +}); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs new file mode 100644 index 000000000..9eccf4d91 --- /dev/null +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -0,0 +1,16 @@ +<div class="row"> + <div class="col-md-12"> + <div class="btn-group add-movies-btn-group btn-group-lg btn-block"> + <button type="button" class="btn btn-default col-md-10 col-xs-8 add-movies-import-btn x-import"> + <i class="icon-sonarr-hdd"/> + Import existing movies on disk + </button> + <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button> + </div> + </div> +</div> +<div class="row"> + <div class="col-md-12"> + <div id="add-movies-workspace"></div> + </div> +</div> diff --git a/src/UI/AddMovies/AddMoviesView.js b/src/UI/AddMovies/AddMoviesView.js new file mode 100644 index 000000000..1694a9ffc --- /dev/null +++ b/src/UI/AddMovies/AddMoviesView.js @@ -0,0 +1,183 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var AddMoviesCollection = require('./AddMoviesCollection'); +var SearchResultCollectionView = require('./SearchResultCollectionView'); +var EmptyView = require('./EmptyView'); +var NotFoundView = require('./NotFoundView'); +var ErrorView = require('./ErrorView'); +var LoadingView = require('../Shared/LoadingView'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/AddMoviesViewTemplate', + + regions : { + searchResult : '#search-result' + }, + + ui : { + moviesSearch : '.x-movies-search', + searchBar : '.x-search-bar', + loadMore : '.x-load-more' + }, + + events : { + 'click .x-load-more' : '_onLoadMore' + }, + + initialize : function(options) { + console.log(options) + this.isExisting = options.isExisting; + this.collection = new AddMoviesCollection(); + + if (this.isExisting) { + this.collection.unmappedFolderModel = this.model; + } + + if (this.isExisting) { + this.className = 'existing-movies'; + } else { + this.className = 'new-movies'; + } + + this.listenTo(vent, vent.Events.MoviesAdded, this._onMoviesAdded); + this.listenTo(this.collection, 'sync', this._showResults); + + this.resultCollectionView = new SearchResultCollectionView({ + collection : this.collection, + isExisting : this.isExisting + }); + + this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); + }, + + onRender : function() { + var self = this; + + this.$el.addClass(this.className); + + this.ui.moviesSearch.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._abortExistingSearch(); + self.throttledSearch({ + term : self.ui.moviesSearch.val() + }); + }); + + this._clearResults(); + + if (this.isExisting) { + this.ui.searchBar.hide(); + } + }, + + onShow : function() { + this.ui.moviesSearch.focus(); + }, + + search : function(options) { + var self = this; + + this.collection.reset(); + + if (!options.term || options.term === this.collection.term) { + return Marionette.$.Deferred().resolve(); + } + + this.searchResult.show(new LoadingView()); + this.collection.term = options.term; + this.currentSearchPromise = this.collection.fetch({ + data : { term : options.term } + }); + + this.currentSearchPromise.fail(function() { + self._showError(); + }); + + return this.currentSearchPromise; + }, + + _onMoviesAdded : function(options) { + if (this.isExisting && options.movies.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.collection.term = ''; + this.collection.reset(); + this._clearResults(); + this.ui.moviesSearch.val(''); + this.ui.moviesSearch.focus(); + } + }, + + _onLoadMore : function() { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + + _clearResults : function() { + if (!this.isExisting) { + this.searchResult.show(new EmptyView()); + } else { + this.searchResult.close(); + } + }, + + _showResults : function() { + if (!this.isClosed) { + if (this.collection.length === 0) { + this.ui.searchBar.show(); + this.searchResult.show(new NotFoundView({ term : this.collection.term })); + } else { + this.searchResult.show(this.resultCollectionView); + if (!this.showingAll && this.isExisting) { + this.ui.loadMore.show(); + } + } + } + }, + + _abortExistingSearch : function() { + if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { + console.log('aborting previous pending search request.'); + this.currentSearchPromise.abort(); + } else { + this._clearResults(); + } + }, + + _showError : function() { + if (!this.isClosed) { + this.ui.searchBar.show(); + this.searchResult.show(new ErrorView({ term : this.collection.term })); + this.collection.term = ''; + } + } +}); diff --git a/src/UI/AddMovies/AddMoviesViewTemplate.hbs b/src/UI/AddMovies/AddMoviesViewTemplate.hbs new file mode 100644 index 000000000..9f9e0660c --- /dev/null +++ b/src/UI/AddMovies/AddMoviesViewTemplate.hbs @@ -0,0 +1,24 @@ +{{#if folder.path}} +<div class="unmapped-folder-path"> + <div class="col-md-12"> + {{folder.path}} + </div> +</div>{{/if}} +<div class="x-search-bar"> + <div class="input-group input-group-lg add-movies-search"> + <span class="input-group-addon"><i class="icon-sonarr-search"/></span> + + {{#if folder}} + <input type="text" class="form-control x-movies-search" value="{{folder.name}}"> + {{else}} + <input type="text" class="form-control x-movies-search" placeholder="Start typing the name of the movie you want to add ..."> + {{/if}} + </div> +</div> +<div class="row"> + <div id="search-result" class="result-list col-md-12"/> +</div> +<div class="btn btn-block text-center new-movies-loadmore x-load-more" style="display: none;"> + <i class="icon-sonarr-load-more"/> + more +</div> diff --git a/src/UI/AddMovies/EmptyView.js b/src/UI/AddMovies/EmptyView.js new file mode 100644 index 000000000..19cdc7bff --- /dev/null +++ b/src/UI/AddMovies/EmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/EmptyViewTemplate' +}); diff --git a/src/UI/AddMovies/EmptyViewTemplate.hbs b/src/UI/AddMovies/EmptyViewTemplate.hbs new file mode 100644 index 000000000..681bd1933 --- /dev/null +++ b/src/UI/AddMovies/EmptyViewTemplate.hbs @@ -0,0 +1,3 @@ +<div class="text-center hint col-md-12"> + <span>You can also search by imdbid using the imdb: prefixes.</span> +</div> diff --git a/src/UI/AddMovies/ErrorView.js b/src/UI/AddMovies/ErrorView.js new file mode 100644 index 000000000..f953834db --- /dev/null +++ b/src/UI/AddMovies/ErrorView.js @@ -0,0 +1,13 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/ErrorViewTemplate', + + initialize : function(options) { + this.options = options; + }, + + templateHelpers : function() { + return this.options; + } +}); diff --git a/src/UI/AddMovies/ErrorViewTemplate.hbs b/src/UI/AddMovies/ErrorViewTemplate.hbs new file mode 100644 index 000000000..511d29952 --- /dev/null +++ b/src/UI/AddMovies/ErrorViewTemplate.hbs @@ -0,0 +1,7 @@ +<div class="text-center col-md-12"> + <h3> + There was an error searching for '{{term}}'. + </h3> + + If the movie title contains non-alphanumeric characters try removing them, otherwise try your search again later. +</div> diff --git a/src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js new file mode 100644 index 000000000..5c5eddc64 --- /dev/null +++ b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js @@ -0,0 +1,51 @@ +var Marionette = require('marionette'); +var AddSeriesView = require('../AddSeriesView'); +var UnmappedFolderCollection = require('./UnmappedFolderCollection'); + +module.exports = Marionette.CompositeView.extend({ + itemView : AddSeriesView, + itemViewContainer : '.x-loading-folders', + template : 'AddSeries/Existing/AddExistingSeriesCollectionViewTemplate', + + ui : { + loadingFolders : '.x-loading-folders' + }, + + initialize : function() { + this.collection = new UnmappedFolderCollection(); + this.collection.importItems(this.model); + }, + + showCollection : function() { + this._showAndSearch(0); + }, + + appendHtml : function(collectionView, itemView, index) { + collectionView.ui.loadingFolders.before(itemView.el); + }, + + _showAndSearch : function(index) { + var self = this; + var model = this.collection.at(index); + + if (model) { + var currentIndex = index; + var folderName = model.get('folder').name; + this.addItemView(model, this.getItemView(), index); + this.children.findByModel(model).search({ term : folderName }).always(function() { + if (!self.isClosed) { + self._showAndSearch(currentIndex + 1); + } + }); + } + + else { + this.ui.loadingFolders.hide(); + } + }, + + itemViewOptions : { + isExisting : true + } + +}); \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs new file mode 100644 index 000000000..d613a52d4 --- /dev/null +++ b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs @@ -0,0 +1,5 @@ +<div class="x-existing-folders"> + <div class="loading-folders x-loading-folders"> + Loading search results from TheTVDB for your series, this may take a few minutes. + </div> +</div> \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/UnmappedFolderCollection.js b/src/UI/AddMovies/Existing/UnmappedFolderCollection.js new file mode 100644 index 000000000..bd2a83f49 --- /dev/null +++ b/src/UI/AddMovies/Existing/UnmappedFolderCollection.js @@ -0,0 +1,20 @@ +var Backbone = require('backbone'); +var UnmappedFolderModel = require('./UnmappedFolderModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + model : UnmappedFolderModel, + + importItems : function(rootFolderModel) { + + this.reset(); + var rootFolder = rootFolderModel; + + _.each(rootFolderModel.get('unmappedFolders'), function(folder) { + this.push(new UnmappedFolderModel({ + rootFolder : rootFolder, + folder : folder + })); + }, this); + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/UnmappedFolderModel.js b/src/UI/AddMovies/Existing/UnmappedFolderModel.js new file mode 100644 index 000000000..3986a5948 --- /dev/null +++ b/src/UI/AddMovies/Existing/UnmappedFolderModel.js @@ -0,0 +1,3 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/AddMovies/MonitoringTooltipTemplate.hbs b/src/UI/AddMovies/MonitoringTooltipTemplate.hbs new file mode 100644 index 000000000..0cf813e98 --- /dev/null +++ b/src/UI/AddMovies/MonitoringTooltipTemplate.hbs @@ -0,0 +1,18 @@ +<dl class="monitor-tooltip-contents"> + <dt>All</dt> + <dd>Monitor all episodes except specials</dd> + <dt>Future</dt> + <dd>Monitor episodes that have not aired yet</dd> + <dt>Missing</dt> + <dd>Monitor episodes that do not have files or have not aired yet</dd> + <dt>Existing</dt> + <dd>Monitor episodes that have files or have not aired yet</dd> + <dt>First Season</dt> + <dd>Monitor all episodes of the first season. All other seasons will be ignored</dd> + <dt>Latest Season</dt> + <dd>Monitor all episodes of the latest season and future seasons</dd> + <dt>None</dt> + <dd>No episodes will be monitored.</dd> + <!--<dt>Latest Season</dt>--> + <!--<dd>Monitor all episodes the latest season only, previous seasons will be ignored</dd>--> +</dl> \ No newline at end of file diff --git a/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs b/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs new file mode 100644 index 000000000..d63e9f60b --- /dev/null +++ b/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs @@ -0,0 +1,3 @@ +<select class="form-control col-md-2 x-movie-type" name="movieType"> + <option value="standard">Standard</option> +</select> diff --git a/src/UI/AddMovies/NotFoundView.js b/src/UI/AddMovies/NotFoundView.js new file mode 100644 index 000000000..928a17392 --- /dev/null +++ b/src/UI/AddMovies/NotFoundView.js @@ -0,0 +1,13 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/NotFoundViewTemplate', + + initialize : function(options) { + this.options = options; + }, + + templateHelpers : function() { + return this.options; + } +}); diff --git a/src/UI/AddMovies/NotFoundViewTemplate.hbs b/src/UI/AddMovies/NotFoundViewTemplate.hbs new file mode 100644 index 000000000..e2d99bb63 --- /dev/null +++ b/src/UI/AddMovies/NotFoundViewTemplate.hbs @@ -0,0 +1,7 @@ +<div class="text-center col-md-12"> + <h3> + Sorry. We couldn't find any movies matching '{{term}}' + </h3> + <a href="https://github.com/NzbDrone/NzbDrone/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my show?</a> + +</div> diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollection.js b/src/UI/AddMovies/RootFolders/RootFolderCollection.js new file mode 100644 index 000000000..81050c19d --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollection.js @@ -0,0 +1,10 @@ +var Backbone = require('backbone'); +var RootFolderModel = require('./RootFolderModel'); +require('../../Mixins/backbone.signalr.mixin'); + +var RootFolderCollection = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/rootfolder', + model : RootFolderModel +}); + +module.exports = new RootFolderCollection(); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js new file mode 100644 index 000000000..f781f21d7 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var RootFolderItemView = require('./RootFolderItemView'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddSeries/RootFolders/RootFolderCollectionViewTemplate', + itemViewContainer : '.x-root-folders', + itemView : RootFolderItemView +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs new file mode 100644 index 000000000..70755bbca --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs @@ -0,0 +1,13 @@ +<table class="table table-hover"> + <thead> + <tr> + <th class="col-md-10 "> + Path + </th> + <th class="col-md-3"> + Free Space + </th> + </tr> + </thead> + <tbody class="x-root-folders"></tbody> +</table> \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderItemView.js b/src/UI/AddMovies/RootFolders/RootFolderItemView.js new file mode 100644 index 000000000..a0e98100b --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderItemView.js @@ -0,0 +1,28 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'AddSeries/RootFolders/RootFolderItemViewTemplate', + className : 'recent-folder', + tagName : 'tr', + + initialize : function() { + this.listenTo(this.model, 'change', this.render); + }, + + events : { + 'click .x-delete' : 'removeFolder', + 'click .x-folder' : 'folderSelected' + }, + + removeFolder : function() { + var self = this; + + this.model.destroy().success(function() { + self.close(); + }); + }, + + folderSelected : function() { + this.trigger('folderSelected', this.model); + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs new file mode 100644 index 000000000..2203e1efd --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs @@ -0,0 +1,9 @@ +<td class="col-md-10 x-folder folder-path"> + {{path}} +</td> +<td class="col-md-3 x-folder folder-free-space"> + <span>{{Bytes freeSpace}}</span> +</td> +<td class="col-md-1"> + <i class="icon-sonarr-delete x-delete"></i> +</td> diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayout.js b/src/UI/AddMovies/RootFolders/RootFolderLayout.js new file mode 100644 index 000000000..6dae383d7 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderLayout.js @@ -0,0 +1,77 @@ +var Marionette = require('marionette'); +var RootFolderCollectionView = require('./RootFolderCollectionView'); +var RootFolderCollection = require('./RootFolderCollection'); +var RootFolderModel = require('./RootFolderModel'); +var LoadingView = require('../../Shared/LoadingView'); +var AsValidatedView = require('../../Mixins/AsValidatedView'); +require('../../Mixins/FileBrowser'); + +var Layout = Marionette.Layout.extend({ + template : 'AddSeries/RootFolders/RootFolderLayoutTemplate', + + ui : { + pathInput : '.x-path' + }, + + regions : { + currentDirs : '#current-dirs' + }, + + events : { + 'click .x-add' : '_addFolder', + 'keydown .x-path input' : '_keydown' + }, + + initialize : function() { + this.collection = RootFolderCollection; + this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); + + this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); + }, + + onShow : function() { + this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs); + this.currentDirs.show(new LoadingView()); + + if (RootFolderCollection.synced) { + this._showCurrentDirs(); + } + + this.ui.pathInput.fileBrowser(); + }, + + _onFolderSelected : function(options) { + this.trigger('folderSelected', options); + }, + + _addFolder : function() { + var self = this; + + var newDir = new RootFolderModel({ + Path : this.ui.pathInput.val() + }); + + this.bindToModelValidation(newDir); + + newDir.save().done(function() { + RootFolderCollection.add(newDir); + self.trigger('folderSelected', { model : newDir }); + }); + }, + + _showCurrentDirs : function() { + this.currentDirs.show(this.rootfolderListView); + }, + + _keydown : function(e) { + if (e.keyCode !== 13) { + return; + } + + this._addFolder(); + } +}); + +var Layout = AsValidatedView.apply(Layout); + +module.exports = Layout; \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs new file mode 100644 index 000000000..83cb9535d --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs @@ -0,0 +1,36 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Select Folder</h3> + </div> + <div class="modal-body root-folders-modal"> + <div class="validation-errors"></div> + <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> + + <div class="row"> + <div class="form-group"> + + <div class="col-md-12"> + + <div class="input-group"> + <span class="input-group-addon"> <i class="icon-sonarr-folder-open"></i></span> + <input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> + <span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-sonarr-ok"/></button></span> + </div> + </div> + </div> + </div> + + <div class="row root-folders"> + <div class="col-md-12"> + {{#if items}} + <h4>Recent Folders</h4> + {{/if}} + <div id="current-dirs" class="root-folders-list"></div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">Close</button> + </div> +</div> diff --git a/src/UI/AddMovies/RootFolders/RootFolderModel.js b/src/UI/AddMovies/RootFolders/RootFolderModel.js new file mode 100644 index 000000000..28681768b --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderModel.js @@ -0,0 +1,8 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/rootfolder', + defaults : { + freeSpace : 0 + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs b/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs new file mode 100644 index 000000000..56729b0dd --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs @@ -0,0 +1,11 @@ +<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath"> + {{#if this}} + {{#each this}} + <option value="{{id}}">{{path}}</option> + {{/each}} + {{else}} + <option value="">Select Path</option> + {{/if}} + <option value="addNew">Add a different path</option> +</select> + diff --git a/src/UI/AddMovies/SearchResultCollectionView.js b/src/UI/AddMovies/SearchResultCollectionView.js new file mode 100644 index 000000000..e533085ac --- /dev/null +++ b/src/UI/AddMovies/SearchResultCollectionView.js @@ -0,0 +1,29 @@ +var Marionette = require('marionette'); +var SearchResultView = require('./SearchResultView'); + +module.exports = Marionette.CollectionView.extend({ + itemView : SearchResultView, + + initialize : function(options) { + this.isExisting = options.isExisting; + this.showing = 1; + }, + + showAll : function() { + this.showingAll = true; + this.render(); + }, + + showMore : function() { + this.showing += 5; + this.render(); + + return this.showing >= this.collection.length; + }, + + appendHtml : function(collectionView, itemView, index) { + if (!this.isExisting || index < this.showing || index === 0) { + collectionView.$el.append(itemView.el); + } + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/SearchResultView.js b/src/UI/AddMovies/SearchResultView.js new file mode 100644 index 000000000..ff697b7d9 --- /dev/null +++ b/src/UI/AddMovies/SearchResultView.js @@ -0,0 +1,272 @@ +var _ = require('underscore'); +var vent = require('vent'); +var AppLayout = require('../AppLayout'); +var Backbone = require('backbone'); +var Marionette = require('marionette'); +var Profiles = require('../Profile/ProfileCollection'); +var RootFolders = require('./RootFolders/RootFolderCollection'); +var RootFolderLayout = require('./RootFolders/RootFolderLayout'); +var MoviesCollection = require('../Movies/MoviesCollection'); +var Config = require('../Config'); +var Messenger = require('../Shared/Messenger'); +var AsValidatedView = require('../Mixins/AsValidatedView'); + +require('jquery.dotdotdot'); + +var view = Marionette.ItemView.extend({ + + template : 'AddMovies/SearchResultViewTemplate', + + ui : { + profile : '.x-profile', + rootFolder : '.x-root-folder', + seasonFolder : '.x-season-folder', + monitor : '.x-monitor', + monitorTooltip : '.x-monitor-tooltip', + addButton : '.x-add', + addSearchButton : '.x-add-search', + overview : '.x-overview' + }, + + events : { + 'click .x-add' : '_addWithoutSearch', + 'click .x-add-search' : '_addAndSearch', + 'change .x-profile' : '_profileChanged', + 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-season-folder' : '_seasonFolderChanged', + 'change .x-monitor' : '_monitorChanged' + }, + + initialize : function() { + + if (!this.model) { + throw 'model is required'; + } + + this.templateHelpers = {}; + this._configureTemplateHelpers(); + + this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); + this.listenTo(this.model, 'change', this.render); + this.listenTo(RootFolders, 'all', this._rootFoldersUpdated); + }, + + onRender : function() { + + var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); + var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true); + var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing'); + + if (Profiles.get(defaultProfile)) { + this.ui.profile.val(defaultProfile); + } + + if (RootFolders.get(defaultRoot)) { + this.ui.rootFolder.val(defaultRoot); + } + + this.ui.seasonFolder.prop('checked', useSeasonFolder); + this.ui.monitor.val(defaultMonitorEpisodes); + + //TODO: make this work via onRender, FM? + //works with onShow, but stops working after the first render + this.ui.overview.dotdotdot({ + height : 120 + }); + + this.templateFunction = Marionette.TemplateCache.get('AddMovies/MonitoringTooltipTemplate'); + var content = this.templateFunction(); + + this.ui.monitorTooltip.popover({ + content : content, + html : true, + trigger : 'hover', + title : 'Episode Monitoring Options', + placement : 'right', + container : this.$el + }); + }, + + _configureTemplateHelpers : function() { + var existingMovies = MoviesCollection.where({ imdbId : this.model.get('imdbId') }); + console.log(existingMovies) + if (existingMovies.length > 0) { + this.templateHelpers.existing = existingMovies[0].toJSON(); + } + + this.templateHelpers.profiles = Profiles.toJSON(); + console.log(this.model) + console.log(this.templateHelpers.existing) + if (!this.model.get('isExisting')) { + this.templateHelpers.rootFolders = RootFolders.toJSON(); + } + }, + + _onConfigUpdated : function(options) { + if (options.key === Config.Keys.DefaultProfileId) { + this.ui.profile.val(options.value); + } + + else if (options.key === Config.Keys.DefaultRootFolderId) { + this.ui.rootFolder.val(options.value); + } + + else if (options.key === Config.Keys.UseSeasonFolder) { + this.ui.seasonFolder.prop('checked', options.value); + } + + else if (options.key === Config.Keys.MonitorEpisodes) { + this.ui.monitor.val(options.value); + } + }, + + _profileChanged : function() { + Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val()); + }, + + _seasonFolderChanged : function() { + Config.setValue(Config.Keys.UseSeasonFolder, this.ui.seasonFolder.prop('checked')); + }, + + _rootFolderChanged : function() { + var rootFolderValue = this.ui.rootFolder.val(); + if (rootFolderValue === 'addNew') { + var rootFolderLayout = new RootFolderLayout(); + this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); + AppLayout.modalRegion.show(rootFolderLayout); + } else { + Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); + } + }, + + _monitorChanged : function() { + Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val()); + }, + + _setRootFolder : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.ui.rootFolder.val(options.model.id); + this._rootFolderChanged(); + }, + + _addWithoutSearch : function() { + this._addMovies(true); + }, + + _addAndSearch : function() { + this._addMovies(true); + }, + + _addMovies : function(searchForMissingEpisodes) { + var addButton = this.ui.addButton; + var addSearchButton = this.ui.addSearchButton; + + addButton.addClass('disabled'); + addSearchButton.addClass('disabled'); + + var profile = this.ui.profile.val(); + var rootFolderPath = this.ui.rootFolder.children(':selected').text(); + + var options = this._getAddMoviesOptions(); + options.searchForMissingEpisodes = searchForMissingEpisodes; + + this.model.set({ + profileId : profile, + rootFolderPath : rootFolderPath, + addOptions : options, + monitored : true + }, { silent : true }); + + var self = this; + var promise = this.model.save(); + + console.log(this.model.save); + console.log(promise); + + if (searchForMissingEpisodes) { + this.ui.addSearchButton.spinForPromise(promise); + } + + else { + this.ui.addButton.spinForPromise(promise); + } + + promise.always(function() { + addButton.removeClass('disabled'); + addSearchButton.removeClass('disabled'); + }); + + promise.done(function() { + MoviesCollection.add(self.model); + + self.close(); + + Messenger.show({ + message : 'Added: ' + self.model.get('title'), + actions : { + goToSeries : { + label : 'Go to Movie', + action : function() { + Backbone.history.navigate('/movies/' + self.model.get('titleSlug'), { trigger : true }); + } + } + }, + hideAfter : 8, + hideOnNavigate : true + }); + + vent.trigger(vent.Events.MoviesAdded, { movie : self.model }); + }); + }, + + _rootFoldersUpdated : function() { + this._configureTemplateHelpers(); + this.render(); + }, + + _getAddMoviesOptions : function() { + var monitor = this.ui.monitor.val(); + + var options = { + ignoreEpisodesWithFiles : false, + ignoreEpisodesWithoutFiles : false + }; + + if (monitor === 'all') { + return options; + } + + else if (monitor === 'future') { + options.ignoreEpisodesWithFiles = true; + options.ignoreEpisodesWithoutFiles = true; + } + + else if (monitor === 'latest') { + this.model.setSeasonPass(lastSeason.seasonNumber); + } + + else if (monitor === 'first') { + this.model.setSeasonPass(lastSeason.seasonNumber + 1); + this.model.setSeasonMonitored(firstSeason.seasonNumber); + } + + else if (monitor === 'missing') { + options.ignoreEpisodesWithFiles = true; + } + + else if (monitor === 'existing') { + options.ignoreEpisodesWithoutFiles = true; + } + + else if (monitor === 'none') { + this.model.setSeasonPass(lastSeason.seasonNumber + 1); + } + + return options; + } +}); + +AsValidatedView.apply(view); + +module.exports = view; diff --git a/src/UI/AddMovies/SearchResultViewTemplate.hbs b/src/UI/AddMovies/SearchResultViewTemplate.hbs new file mode 100644 index 000000000..41845cdee --- /dev/null +++ b/src/UI/AddMovies/SearchResultViewTemplate.hbs @@ -0,0 +1,101 @@ +<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}"> + <div class="row"> + <div class="col-md-2"> + <a href="{{imdbUrl}}" target="_blank"> + {{poster}} + </a> + </div> + <div class="col-md-10"> + <div class="row"> + <div class="col-md-12"> + <h2 class="movies-title"> + {{titleWithYear}} + + <span class="labels"> + <span class="label label-default">{{network}}</span> + {{#unless_eq status compare="announced"}} + <span class="label label-danger">Released</span> <!-- TODO: Better handling of cases here! --> + {{/unless_eq}} + </span> + </h2> + </div> + </div> + <div class="row new-movies-overview x-overview"> + <div class="col-md-12 overview-internal"> + {{overview}} + </div> + </div> + <div class="row"> + {{#unless existing}} + {{#unless path}} + <div class="form-group col-md-4"> + <label>Path</label> + {{> RootFolderSelectionPartial rootFolders}} + </div> + {{/unless}} + + <div class="form-group col-md-2"> + <label>Monitor <i class="icon-sonarr-form-info monitor-tooltip x-monitor-tooltip"></i></label> + <select class="form-control col-md-2 x-monitor"> + <option value="all">All</option> + <option value="missing">Missing</option> + <option value="none">None</option> + </select> + </div> + + <div class="form-group col-md-2"> + <label>Profile</label> + {{> ProfileSelectionPartial profiles}} + </div> + + <div class="form-group col-md-2"> + <label>Season Folders</label> + + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-season-folder"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + {{/unless}} + + {{#unless existing}} + {{#if title}} + <div class="form-group col-md-2"> + <!--Uncomment if we need to add even more controls to add Movies--> + <label style="visibility: hidden">Add</label> + <div class="btn-group"> + <button class="btn btn-success add x-add" title="Add"> + <i class="icon-sonarr-add"></i> + </button> + + <button class="btn btn-success add x-add-search" title="Add and Search for missing episodes"> + <i class="icon-sonarr-search"></i> + </button> + </div> + </div> + {{else}} + <label style="visibility: hidden">Add</label> + <div class="col-md-2" title="Movies require an English title"> + <button class="btn add-movies disabled"> + Add + </button> + </div> + {{/if}} + {{else}} + <label style="visibility: hidden">Add</label> + <div class="col-md-2 col-md-offset-10"> + <a class="btn btn-default" href="{{route}}"> + Already Exists + </a> + </div> + {{/unless}} + </div> + </div> + </div> +</div> diff --git a/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs b/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs new file mode 100644 index 000000000..e5623e33a --- /dev/null +++ b/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs @@ -0,0 +1,13 @@ +<select class="form-control col-md-2 starting-season x-starting-season"> + + + {{#each this}} + {{#if_eq seasonNumber compare="0"}} + <option value="{{seasonNumber}}">Specials</option> + {{else}} + <option value="{{seasonNumber}}">Season {{seasonNumber}}</option> + {{/if_eq}} + {{/each}} + + <option value="5000000">None</option> +</select> diff --git a/src/UI/AddMovies/addMovies.less b/src/UI/AddMovies/addMovies.less new file mode 100644 index 000000000..e1eaad2c8 --- /dev/null +++ b/src/UI/AddMovies/addMovies.less @@ -0,0 +1,177 @@ +@import "../Shared/Styles/card.less"; +@import "../Shared/Styles/clickable.less"; + +#add-movies-screen { + .existing-movies { + + .card(); + margin : 30px 0px; + + .unmapped-folder-path { + padding: 20px; + margin-left : 0px; + font-weight : 100; + font-size : 25px; + text-align : center; + } + + .new-movies-loadmore { + font-size : 30px; + font-weight : 300; + padding-top : 10px; + padding-bottom : 10px; + } + } + + .new-movies { + .search-item { + .card(); + margin : 40px 0px; + } + } + + .add-movies-search { + margin-top : 20px; + margin-bottom : 20px; + } + + .search-item { + + padding-bottom : 20px; + + .btn-group{ + display: table; + } + + .movies-title { + margin-top : 5px; + + .labels { + margin-left : 10px; + + .label { + font-size : 12px; + vertical-align : middle; + } + } + + .year { + font-style : italic; + color : #aaaaaa; + } + } + + .new-movies-overview { + overflow : hidden; + height : 103px; + + .overview-internal { + overflow : hidden; + height : 80px; + } + } + + .movies-poster { + min-width : 138px; + min-height : 203px; + max-width : 138px; + max-height : 203px; + margin : 10px; + } + + a { + color : #343434; + } + + a:hover { + text-decoration : none; + } + + select { + font-size : 14px; + } + + .checkbox { + margin-top : 0px; + } + + .add { + i { + &:before { + color : #ffffff; + } + } + } + + .monitor-tooltip { + margin-left : 5px; + } + } + + .loading-folders { + margin : 30px 0px; + text-align: center; + } + + .hint { + color : #999999; + font-style : italic; + } + + .monitor-tooltip-contents { + padding-bottom : 0px; + + dd { + padding-bottom : 8px; + } + } +} + +li.add-new { + .clickable; + + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: rgb(51, 51, 51); + white-space: nowrap; +} + +li.add-new:hover { + text-decoration: none; + color: rgb(255, 255, 255); + background-color: rgb(0, 129, 194); +} + +.root-folders-modal { + overflow : visible; + + .root-folders-list { + overflow-y : auto; + max-height : 300px; + + i { + .clickable(); + } + } + + .validation-errors { + display : none; + } + + .input-group { + .form-control { + background-color : white; + } + } + + .root-folders { + margin-top : 20px; + } + + .recent-folder { + .clickable(); + } +} diff --git a/src/UI/Controller.js b/src/UI/Controller.js index ef901c60a..eb5168daa 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -4,6 +4,7 @@ var Marionette = require('marionette'); var ActivityLayout = require('./Activity/ActivityLayout'); var SettingsLayout = require('./Settings/SettingsLayout'); var AddSeriesLayout = require('./AddSeries/AddSeriesLayout'); +var AddMoviesLayout = require('./AddMovies/AddMoviesLayout'); var WantedLayout = require('./Wanted/WantedLayout'); var CalendarLayout = require('./Calendar/CalendarLayout'); var ReleaseLayout = require('./Release/ReleaseLayout'); @@ -17,6 +18,11 @@ module.exports = NzbDroneController.extend({ this.showMainRegion(new AddSeriesLayout({ action : action })); }, + addMovies : function(action) { + this.setTitle("Add Movie"); + this.showMainRegion(new AddMoviesLayout({action : action})); + }, + calendar : function() { this.setTitle('Calendar'); this.showMainRegion(new CalendarLayout()); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 4bac9e659..f22ad5165 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -28,7 +28,7 @@ Handlebars.registerHelper('imdbUrl', function() { }); Handlebars.registerHelper('tvdbUrl', function() { - return 'http://imdb.com/title/tt' + this.tvdbId; + return 'http://imdb.com/title/tt' + this.imdbId; }); Handlebars.registerHelper('tvRageUrl', function() { diff --git a/src/UI/Movies/MovieModel.js b/src/UI/Movies/MovieModel.js new file mode 100644 index 000000000..49c64dea7 --- /dev/null +++ b/src/UI/Movies/MovieModel.js @@ -0,0 +1,13 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/movies', + + defaults : { + episodeFileCount : 0, + episodeCount : 0, + isExisting : false, + status : 0 + } +}); diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js new file mode 100644 index 000000000..b6f0e2edb --- /dev/null +++ b/src/UI/Movies/MoviesCollection.js @@ -0,0 +1,120 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var PageableCollection = require('backbone.pageable'); +var MovieModel = require('./MovieModel'); +var ApiData = require('../Shared/ApiData'); +var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); +var AsSortedCollection = require('../Mixins/AsSortedCollection'); +var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); +var moment = require('moment'); +require('../Mixins/backbone.signalr.mixin'); + +var Collection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/movies', + model : MovieModel, + tableName : 'movies', + + state : { + sortKey : 'sortTitle', + order : -1, + pageSize : 100000, + secondarySortKey : 'sortTitle', + secondarySortOrder : -1 + }, + + mode : 'client', + + save : function() { + var self = this; + + var proxy = _.extend(new Backbone.Model(), { + id : '', + + url : self.url + '/editor', + + toJSON : function() { + return self.filter(function(model) { + return model.edited; + }); + } + }); + + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true }); + this.trigger('save', this); + }); + + return proxy.save(); + }, + + filterModes : { + 'all' : [ + null, + null + ], + 'continuing' : [ + 'status', + 'continuing' + ], + 'ended' : [ + 'status', + 'ended' + ], + 'monitored' : [ + 'monitored', + true + ], + 'missing' : [ + null, + null, + function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); } + ] + }, + + sortMappings : { + title : { + sortKey : 'sortTitle' + }, + + nextAiring : { + sortValue : function(model, attr, order) { + var nextAiring = model.get(attr); + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (order === 1) { + return 0; + } + + return Number.MAX_VALUE; + } + }, + + percentOfEpisodes : { + sortValue : function(model, attr) { + var percentOfEpisodes = model.get(attr); + var episodeCount = model.get('episodeCount'); + + return percentOfEpisodes + episodeCount / 1000000; + } + }, + + path : { + sortValue : function(model) { + var path = model.get('path'); + + return path.toLowerCase(); + } + } + } +}); + +Collection = AsFilteredCollection.call(Collection); +Collection = AsSortedCollection.call(Collection); +Collection = AsPersistedStateCollection.call(Collection); + +var data = ApiData.get('series'); + +module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Router.js b/src/UI/Router.js index 91b42a074..ba41c0e61 100644 --- a/src/UI/Router.js +++ b/src/UI/Router.js @@ -6,6 +6,8 @@ module.exports = Marionette.AppRouter.extend({ appRoutes : { 'addseries' : 'addSeries', 'addseries/:action(/:query)' : 'addSeries', + 'addmovies' : 'addMovies', + 'addmovies/:action(/:query)' : 'addMovies', 'calendar' : 'calendar', 'settings' : 'settings', 'settings/:action(/:query)' : 'settings', @@ -22,4 +24,4 @@ module.exports = Marionette.AppRouter.extend({ 'serieseditor' : 'seriesEditor', ':whatever' : 'showNotFound' } -}); \ No newline at end of file +}); diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs index 696d4f9e5..06fb40fe5 100644 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ b/src/UI/Series/Index/EmptyTemplate.hbs @@ -7,7 +7,7 @@ </div> <div class="row"> <div class="col-md-4 col-md-offset-4"> - <a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'> + <a href="/addmovies" class='btn btn-lg btn-block btn-success x-add-series'> <i class='icon-sonarr-add'></i> Add Movie </a> diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index f79d17a76..77f31aac4 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -82,7 +82,7 @@ module.exports = Marionette.Layout.extend({ { title : 'Add Movie', icon : 'icon-sonarr-add', - route : 'addseries' + route : 'addmovies' }, { title : 'Season Pass', diff --git a/src/UI/index.html b/src/UI/index.html index 94ebba2af..e0c128e72 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -26,6 +26,7 @@ <link href="/Content/logs.css" rel='stylesheet' type='text/css'/> <link href="/Content/settings.css" rel='stylesheet' type='text/css'/> <link href="/Content/addSeries.css" rel='stylesheet' type='text/css'/> + <link href="/Content/addMovies.css" rel='stylesheet' type='text/css'/> <link href="/Content/calendar.css" rel='stylesheet' type='text/css'/> <link href="/Content/update.css" rel='stylesheet' type='text/css'/> <link href="/Content/overrides.css" rel='stylesheet' type='text/css'/> From 5ebfac6cc87552b41e1098e7bf46130961250eb0 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Thu, 29 Dec 2016 16:04:01 +0100 Subject: [PATCH 08/40] First implementation of custom database table for movies.Some things are not yet working quite well (e.g. search clears when movies are added.). Also movies cannot yet be looked up! --- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + src/NzbDrone.Api/Series/MovieLookupModule.cs | 2 +- src/NzbDrone.Api/Series/MovieModule.cs | 225 ++++++++++++++++++ src/NzbDrone.Api/Series/MovieResource.cs | 3 +- .../Datastore/Migration/001_initial_setup.cs | 28 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 5 + .../MediaFiles/Events/MovieRenamedEvent.cs | 15 ++ .../MovieStats/MovieStatistics.cs | 42 ++++ .../MovieStats/MovieStatisticsRepository.cs | 86 +++++++ .../MovieStats/MovieStatisticsService.cs | 63 +++++ .../MovieStats/SeasonStatistics.cs | 41 ++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 15 ++ .../Organizer/FileNameBuilder.cs | 6 + .../Tv/Events/MovieAddedEvent.cs | 14 ++ .../Tv/Events/MovieDeletedEvent.cs | 16 ++ .../Tv/Events/MovieEditedEvent.cs | 16 ++ .../Tv/Events/MovieUpdateEvent.cs | 14 ++ src/NzbDrone.Core/Tv/MovieRepository.cs | 50 ++++ src/NzbDrone.Core/Tv/MovieService.cs | 194 +++++++++++++++ src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs | 22 ++ .../Paths/MovieAncestorValidator.cs | 25 ++ .../Validation/Paths/MovieExistsValidator.cs | 26 ++ .../Validation/Paths/MoviePathValidation.cs | 27 +++ src/UI/Movies/MovieModel.js | 2 +- src/UI/Movies/MoviesCollection.js | 6 +- 25 files changed, 938 insertions(+), 6 deletions(-) create mode 100644 src/NzbDrone.Api/Series/MovieModule.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatistics.cs create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs create mode 100644 src/NzbDrone.Core/MovieStats/SeasonStatistics.cs create mode 100644 src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs create mode 100644 src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs create mode 100644 src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs create mode 100644 src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs create mode 100644 src/NzbDrone.Core/Tv/MovieRepository.cs create mode 100644 src/NzbDrone.Core/Tv/MovieService.cs create mode 100644 src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index b87179adb..455cc845a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -233,6 +233,7 @@ <Compile Include="Series\SeriesEditorModule.cs" /> <Compile Include="Series\MovieLookupModule.cs" /> <Compile Include="Series\SeriesLookupModule.cs" /> + <Compile Include="Series\MovieModule.cs" /> <Compile Include="Series\SeriesModule.cs" /> <Compile Include="Series\MovieResource.cs" /> <Compile Include="Series\SeriesResource.cs" /> diff --git a/src/NzbDrone.Api/Series/MovieLookupModule.cs b/src/NzbDrone.Api/Series/MovieLookupModule.cs index 0c74df808..1120b3046 100644 --- a/src/NzbDrone.Api/Series/MovieLookupModule.cs +++ b/src/NzbDrone.Api/Series/MovieLookupModule.cs @@ -5,7 +5,7 @@ using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using System.Linq; -namespace NzbDrone.Api.Series +namespace NzbDrone.Api.Movie { public class MovieLookupModule : NzbDroneRestModule<MovieResource> { diff --git a/src/NzbDrone.Api/Series/MovieModule.cs b/src/NzbDrone.Api/Series/MovieModule.cs new file mode 100644 index 000000000..5a8e5f52f --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieModule.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MovieStats; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Validation; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.Movie +{ + public class MovieModule : NzbDroneRestModuleWithSignalR<MovieResource, Core.Tv.Movie>, + IHandle<EpisodeImportedEvent>, + IHandle<EpisodeFileDeletedEvent>, + IHandle<MovieUpdatedEvent>, + IHandle<MovieEditedEvent>, + IHandle<MovieDeletedEvent>, + IHandle<MovieRenamedEvent>, + IHandle<MediaCoversUpdatedEvent> + + { + private readonly IMovieService _moviesService; + private readonly IMovieStatisticsService _moviesStatisticsService; + private readonly IMapCoversToLocal _coverMapper; + + public MovieModule(IBroadcastSignalRMessage signalRBroadcaster, + IMovieService moviesService, + IMovieStatisticsService moviesStatisticsService, + ISceneMappingService sceneMappingService, + IMapCoversToLocal coverMapper, + RootFolderValidator rootFolderValidator, + MoviePathValidator moviesPathValidator, + MovieExistsValidator moviesExistsValidator, + DroneFactoryValidator droneFactoryValidator, + MovieAncestorValidator moviesAncestorValidator, + ProfileExistsValidator profileExistsValidator + ) + : base(signalRBroadcaster) + { + _moviesService = moviesService; + _moviesStatisticsService = moviesStatisticsService; + + _coverMapper = coverMapper; + + GetResourceAll = AllMovie; + GetResourceById = GetMovie; + CreateResource = AddMovie; + UpdateResource = UpdateMovie; + DeleteResource = DeleteMovie; + + Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(moviesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(moviesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.Title).NotEmpty(); + PostValidator.RuleFor(s => s.ImdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private MovieResource GetMovie(int id) + { + var movies = _moviesService.GetMovie(id); + return MapToResource(movies); + } + + private MovieResource MapToResource(Core.Tv.Movie movies) + { + if (movies == null) return null; + + var resource = movies.ToResource(); + MapCoversToLocal(resource); + FetchAndLinkMovieStatistics(resource); + PopulateAlternateTitles(resource); + + return resource; + } + + private List<MovieResource> AllMovie() + { + var moviesStats = _moviesStatisticsService.MovieStatistics(); + var moviesResources = _moviesService.GetAllMovies().ToResource(); + + MapCoversToLocal(moviesResources.ToArray()); + LinkMovieStatistics(moviesResources, moviesStats); + PopulateAlternateTitles(moviesResources); + + return moviesResources; + } + + private int AddMovie(MovieResource moviesResource) + { + var model = moviesResource.ToModel(); + + return _moviesService.AddMovie(model).Id; + } + + private void UpdateMovie(MovieResource moviesResource) + { + var model = moviesResource.ToModel(_moviesService.GetMovie(moviesResource.Id)); + + _moviesService.UpdateMovie(model); + + BroadcastResourceChange(ModelAction.Updated, moviesResource); + } + + private void DeleteMovie(int id) + { + var deleteFiles = false; + var deleteFilesQuery = Request.Query.deleteFiles; + + if (deleteFilesQuery.HasValue) + { + deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); + } + + _moviesService.DeleteMovie(id, deleteFiles); + } + + private void MapCoversToLocal(params MovieResource[] movies) + { + foreach (var moviesResource in movies) + { + _coverMapper.ConvertToLocalUrls(moviesResource.Id, moviesResource.Images); + } + } + + private void FetchAndLinkMovieStatistics(MovieResource resource) + { + LinkMovieStatistics(resource, _moviesStatisticsService.MovieStatistics(resource.Id)); + } + + private void LinkMovieStatistics(List<MovieResource> resources, List<MovieStatistics> moviesStatistics) + { + var dictMovieStats = moviesStatistics.ToDictionary(v => v.MovieId); + + foreach (var movies in resources) + { + var stats = dictMovieStats.GetValueOrDefault(movies.Id); + if (stats == null) continue; + + LinkMovieStatistics(movies, stats); + } + } + + private void LinkMovieStatistics(MovieResource resource, MovieStatistics moviesStatistics) + { + resource.SizeOnDisk = moviesStatistics.SizeOnDisk; + } + + private void PopulateAlternateTitles(List<MovieResource> resources) + { + foreach (var resource in resources) + { + PopulateAlternateTitles(resource); + } + } + + private void PopulateAlternateTitles(MovieResource resource) + { + //var mappings = null;//_sceneMappingService.FindByTvdbId(resource.TvdbId); + + //if (mappings == null) return; + + //resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + } + + public void Handle(EpisodeImportedEvent message) + { + //BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.MovieId); + } + + public void Handle(EpisodeFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + //BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.MovieId); + } + + public void Handle(MovieUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MovieEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MovieDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Movie.ToResource()); + } + + public void Handle(MovieRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MediaCoversUpdatedEvent message) + { + //BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + } +} diff --git a/src/NzbDrone.Api/Series/MovieResource.cs b/src/NzbDrone.Api/Series/MovieResource.cs index 1ce197751..eed694e05 100644 --- a/src/NzbDrone.Api/Series/MovieResource.cs +++ b/src/NzbDrone.Api/Series/MovieResource.cs @@ -4,8 +4,9 @@ using System.Linq; using NzbDrone.Api.REST; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; +using NzbDrone.Api.Series; -namespace NzbDrone.Api.Series +namespace NzbDrone.Api.Movie { public class MovieResource : RestResource { diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index b2792fe56..53d7a9a17 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -41,6 +41,34 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("FirstAired").AsDateTime().Nullable() .WithColumn("NextAiring").AsDateTime().Nullable(); + Create.TableForModel("Movies") + .WithColumn("ImdbId").AsString().Unique() + .WithColumn("Title").AsString() + .WithColumn("TitleSlug").AsString().Unique() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("CleanTitle").AsString() + .WithColumn("Status").AsInt32() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString() + .WithColumn("Monitored").AsBoolean() + .WithColumn("QualityProfileId").AsInt32() + .WithColumn("SeasonFolder").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("Runtime").AsInt32() + .WithColumn("BacklogSetting").AsInt32() + .WithColumn("CustomStartDate").AsDateTime().Nullable() + .WithColumn("InCinemas").AsDateTime().Nullable() + .WithColumn("Year").AsInt32().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("Actors").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Certification").AsString().Nullable(); + + Create.TableForModel("Seasons") .WithColumn("SeriesId").AsInt32() .WithColumn("SeasonNumber").AsInt32() diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 62f6aeb8b..397703127 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -76,6 +76,11 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(s => s.Profile, s => s.ProfileId); + Mapper.Entity<Movie>().RegisterModel("Movies") + .Ignore(s => s.RootFolderPath) + .Relationship() + .HasOne(s => s.Profile, s => s.ProfileId); + Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles") .Ignore(f => f.Path) .Relationships.AutoMapICollectionOrComplexProperties() diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs new file mode 100644 index 000000000..d7e264fa3 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieRenamedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieRenamedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MovieStats/MovieStatistics.cs b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs new file mode 100644 index 000000000..7ea4dabdb --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public class MovieStatistics : ResultSet + { + public int MovieId { get; set; } + public string NextAiringString { get; set; } + public string PreviousAiringString { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + public List<SeasonStatistics> SeasonStatistics { get; set; } + + public DateTime? NextAiring + { + get + { + DateTime nextAiring; + + if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; + + return nextAiring; + } + } + + public DateTime? PreviousAiring + { + get + { + DateTime previousAiring; + + if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; + + return previousAiring; + } + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs new file mode 100644 index 000000000..32950944d --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsRepository + { + List<SeasonStatistics> MovieStatistics(); + List<SeasonStatistics> MovieStatistics(int movieId); + } + + public class MovieStatisticsRepository : IMovieStatisticsRepository + { + private readonly IMainDatabase _database; + + public MovieStatisticsRepository(IMainDatabase database) + { + _database = database; + } + + public List<SeasonStatistics> MovieStatistics() + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("currentDate", DateTime.UtcNow); + + var sb = new StringBuilder(); + sb.AppendLine(GetSelectClause()); + sb.AppendLine(GetEpisodeFilesJoin()); + sb.AppendLine(GetGroupByClause()); + var queryText = sb.ToString(); + + return new List<SeasonStatistics>(); + + return mapper.Query<SeasonStatistics>(queryText); + } + + public List<SeasonStatistics> MovieStatistics(int movieId) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("currentDate", DateTime.UtcNow); + mapper.AddParameter("movieId", movieId); + + var sb = new StringBuilder(); + sb.AppendLine(GetSelectClause()); + sb.AppendLine(GetEpisodeFilesJoin()); + sb.AppendLine("WHERE Episodes.MovieId = @movieId"); + sb.AppendLine(GetGroupByClause()); + var queryText = sb.ToString(); + + return new List<SeasonStatistics>(); + + return mapper.Query<SeasonStatistics>(queryText); + } + + private string GetSelectClause() + { + return @"SELECT Episodes.*, SUM(EpisodeFiles.Size) as SizeOnDisk FROM + (SELECT + Episodes.MovieId, + Episodes.SeasonNumber, + SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount, + SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, + SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, + MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString, + MAX(CASE WHEN AirDateUtc >= @currentDate OR EpisodeFileId = 0 AND Monitored = 0 THEN NULL ELSE AirDateUtc END) AS PreviousAiringString + FROM Episodes + GROUP BY Episodes.MovieId, Episodes.SeasonNumber) as Episodes"; + } + + private string GetGroupByClause() + { + return "GROUP BY Episodes.MovieId, Episodes.SeasonNumber"; + } + + private string GetEpisodeFilesJoin() + { + return @"LEFT OUTER JOIN EpisodeFiles + ON EpisodeFiles.MovieId = Episodes.MovieId + AND EpisodeFiles.SeasonNumber = Episodes.SeasonNumber"; + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs new file mode 100644 index 000000000..68dabd609 --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsService + { + List<MovieStatistics> MovieStatistics(); + MovieStatistics MovieStatistics(int movieId); + } + + public class MovieStatisticsService : IMovieStatisticsService + { + private readonly IMovieStatisticsRepository _movieStatisticsRepository; + + public MovieStatisticsService(IMovieStatisticsRepository movieStatisticsRepository) + { + _movieStatisticsRepository = movieStatisticsRepository; + } + + public List<MovieStatistics> MovieStatistics() + { + var seasonStatistics = _movieStatisticsRepository.MovieStatistics(); + + return seasonStatistics.GroupBy(s => s.MovieId).Select(s => MapMovieStatistics(s.ToList())).ToList(); + } + + public MovieStatistics MovieStatistics(int movieId) + { + var stats = _movieStatisticsRepository.MovieStatistics(movieId); + + if (stats == null || stats.Count == 0) return new MovieStatistics(); + + return MapMovieStatistics(stats); + } + + private MovieStatistics MapMovieStatistics(List<SeasonStatistics> seasonStatistics) + { + var movieStatistics = new MovieStatistics + { + SeasonStatistics = seasonStatistics, + MovieId = seasonStatistics.First().MovieId, + EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount), + EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount), + TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), + SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk) + }; + + var nextAiring = seasonStatistics.Where(s => s.NextAiring != null) + .OrderBy(s => s.NextAiring) + .FirstOrDefault(); + + var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null) + .OrderBy(s => s.PreviousAiring) + .LastOrDefault(); + + movieStatistics.NextAiringString = nextAiring != null ? nextAiring.NextAiringString : null; + movieStatistics.PreviousAiringString = previousAiring != null ? previousAiring.PreviousAiringString : null; + + return movieStatistics; + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs b/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs new file mode 100644 index 000000000..05da073db --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs @@ -0,0 +1,41 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public class SeasonStatistics : ResultSet + { + public int MovieId { get; set; } + public int SeasonNumber { get; set; } + public string NextAiringString { get; set; } + public string PreviousAiringString { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + + public DateTime? NextAiring + { + get + { + DateTime nextAiring; + + if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; + + return nextAiring; + } + } + + public DateTime? PreviousAiring + { + get + { + DateTime previousAiring; + + if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; + + return previousAiring; + } + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 2d9ac2a47..33fb52fd4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -735,6 +735,7 @@ <Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeFolderCreatedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieRenamedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" /> @@ -1024,6 +1025,10 @@ </Compile> <Compile Include="RootFolders\UnmappedFolder.cs" /> <Compile Include="Security.cs" /> + <Compile Include="MovieStats\SeasonStatistics.cs" /> + <Compile Include="MovieStats\MovieStatistics.cs" /> + <Compile Include="MovieStats\MovieStatisticsRepository.cs" /> + <Compile Include="MovieStats\MovieStatisticsService.cs" /> <Compile Include="SeriesStats\SeasonStatistics.cs" /> <Compile Include="SeriesStats\SeriesStatistics.cs" /> <Compile Include="SeriesStats\SeriesStatisticsRepository.cs" /> @@ -1058,11 +1063,15 @@ </Compile> <Compile Include="Tv\EpisodeService.cs" /> <Compile Include="Tv\Events\EpisodeInfoRefreshedEvent.cs" /> + <Compile Include="Tv\Events\MovieAddedEvent.cs" /> <Compile Include="Tv\Events\SeriesAddedEvent.cs" /> + <Compile Include="Tv\Events\MovieDeletedEvent.cs" /> <Compile Include="Tv\Events\SeriesDeletedEvent.cs" /> + <Compile Include="Tv\Events\MovieEditedEvent.cs" /> <Compile Include="Tv\Events\SeriesEditedEvent.cs" /> <Compile Include="Tv\Events\SeriesMovedEvent.cs" /> <Compile Include="Tv\Events\SeriesRefreshStartingEvent.cs" /> + <Compile Include="Tv\Events\MovieUpdateEvent.cs" /> <Compile Include="Tv\Events\SeriesUpdatedEvent.cs" /> <Compile Include="Tv\MonitoringOptions.cs" /> <Compile Include="Tv\MoveSeriesService.cs" /> @@ -1073,14 +1082,17 @@ <Compile Include="Tv\Movie.cs" /> <Compile Include="Tv\Series.cs" /> <Compile Include="Tv\SeriesAddedHandler.cs" /> + <Compile Include="Tv\MovieRepository.cs" /> <Compile Include="Tv\SeriesScannedHandler.cs" /> <Compile Include="Tv\SeriesEditedService.cs" /> <Compile Include="Tv\SeriesRepository.cs" /> + <Compile Include="Tv\MovieService.cs" /> <Compile Include="Tv\SeriesService.cs"> <SubType>Code</SubType> </Compile> <Compile Include="Tv\MovieStatusType.cs" /> <Compile Include="Tv\SeriesStatusType.cs" /> + <Compile Include="Tv\MovieTitleNormalizer.cs" /> <Compile Include="Tv\SeriesTitleNormalizer.cs" /> <Compile Include="Tv\SeriesTypes.cs" /> <Compile Include="Tv\ShouldRefreshSeries.cs" /> @@ -1109,6 +1121,9 @@ <Compile Include="Validation\Paths\FolderWritableValidator.cs" /> <Compile Include="Validation\Paths\PathExistsValidator.cs" /> <Compile Include="Validation\Paths\PathValidator.cs" /> + <Compile Include="Validation\Paths\MoviePathValidation.cs" /> + <Compile Include="Validation\Paths\MovieAncestorValidator.cs" /> + <Compile Include="Validation\Paths\MovieExistsValidator.cs" /> <Compile Include="Validation\Paths\StartupFolderValidator.cs" /> <Compile Include="Validation\Paths\RootFolderValidator.cs" /> <Compile Include="Validation\Paths\SeriesAncestorValidator.cs" /> diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 31cbd53ef..4d7773ad7 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Organizer BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + string GetMovieFolder(Movie movie); } public class FileNameBuilder : IBuildFileNames @@ -243,6 +244,11 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); } + public string GetMovieFolder(Movie movie) + { + return CleanFolderName(Parser.Parser.CleanSeriesTitle(movie.Title)); + } + public static string CleanTitle(string title) { title = title.Replace("&", "and"); diff --git a/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs new file mode 100644 index 000000000..1559d3716 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieAddedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieAddedEvent(Movie movie) + { + Movie = movie; + } + } +} diff --git a/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs new file mode 100644 index 000000000..6c56ef1d2 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieDeletedEvent : IEvent + { + public Movie Movie { get; private set; } + public bool DeleteFiles { get; private set; } + + public MovieDeletedEvent(Movie movie, bool deleteFiles) + { + Movie = movie; + DeleteFiles = deleteFiles; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs new file mode 100644 index 000000000..8b4b5c5f3 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieEditedEvent : IEvent + { + public Movie Movie { get; private set; } + public Movie OldMovie { get; private set; } + + public MovieEditedEvent(Movie movie, Movie oldMovie) + { + Movie = movie; + OldMovie = oldMovie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs new file mode 100644 index 000000000..bae4d3e1d --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieUpdatedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieUpdatedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs new file mode 100644 index 000000000..281152b05 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -0,0 +1,50 @@ +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.Tv +{ + public interface IMovieRepository : IBasicRepository<Movie> + { + bool MoviePathExists(string path); + Movie FindByTitle(string cleanTitle); + Movie FindByTitle(string cleanTitle, int year); + Movie FindByImdbId(string imdbid); + } + + public class MovieRepository : BasicRepository<Movie>, IMovieRepository + { + public MovieRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public bool MoviePathExists(string path) + { + return Query.Where(c => c.Path == path).Any(); + } + + public Movie FindByTitle(string cleanTitle) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .SingleOrDefault(); + } + + public Movie FindByTitle(string cleanTitle, int year) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .AndWhere(s => s.Year == year) + .SingleOrDefault(); + } + + public Movie FindByImdbId(string imdbid) + { + return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs new file mode 100644 index 000000000..546442f48 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public interface IMovieService + { + Movie GetMovie(int movieId); + List<Movie> GetMovies(IEnumerable<int> movieIds); + Movie AddMovie(Movie newMovie); + Movie FindByImdbId(string imdbid); + Movie FindByTitle(string title); + Movie FindByTitle(string title, int year); + Movie FindByTitleInexact(string title); + void DeleteMovie(int movieId, bool deleteFiles); + List<Movie> GetAllMovies(); + Movie UpdateMovie(Movie movie); + List<Movie> UpdateMovie(List<Movie> movie); + bool MoviePathExists(string folder); + } + + public class MovieService : IMovieService + { + private readonly IMovieRepository _movieRepository; + private readonly IEventAggregator _eventAggregator; + private readonly IBuildFileNames _fileNameBuilder; + private readonly Logger _logger; + + public MovieService(IMovieRepository movieRepository, + IEventAggregator eventAggregator, + ISceneMappingService sceneMappingService, + IEpisodeService episodeService, + IBuildFileNames fileNameBuilder, + Logger logger) + { + _movieRepository = movieRepository; + _eventAggregator = eventAggregator; + _fileNameBuilder = fileNameBuilder; + _logger = logger; + } + + public Movie GetMovie(int movieId) + { + return _movieRepository.Get(movieId); + } + + public List<Movie> GetMovies(IEnumerable<int> movieIds) + { + return _movieRepository.Get(movieIds).ToList(); + } + + public Movie AddMovie(Movie newMovie) + { + Ensure.That(newMovie, () => newMovie).IsNotNull(); + + if (string.IsNullOrWhiteSpace(newMovie.Path)) + { + var folderName = _fileNameBuilder.GetMovieFolder(newMovie); + newMovie.Path = Path.Combine(newMovie.RootFolderPath, folderName); + } + + _logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path); + + newMovie.CleanTitle = newMovie.Title.CleanSeriesTitle(); + newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.ImdbId); + newMovie.Added = DateTime.UtcNow; + + _movieRepository.Insert(newMovie); + _eventAggregator.PublishEvent(new MovieAddedEvent(GetMovie(newMovie.Id))); + + return newMovie; + } + + public Movie FindByTitle(string title) + { + return _movieRepository.FindByTitle(title.CleanSeriesTitle()); + } + + public Movie FindByImdbId(string imdbid) + { + return _movieRepository.FindByImdbId(imdbid); + } + + public Movie FindByTitleInexact(string title) + { + // find any movie clean title within the provided release title + string cleanTitle = title.CleanSeriesTitle(); + var list = _movieRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList(); + if (!list.Any()) + { + // no movie matched + return null; + } + if (list.Count == 1) + { + // return the first movie if there is only one + return list.Single(); + } + // build ordered list of movie by position in the search string + var query = + list.Select(movie => new + { + position = cleanTitle.IndexOf(movie.CleanTitle), + length = movie.CleanTitle.Length, + movie = movie + }) + .Where(s => (s.position>=0)) + .ToList() + .OrderBy(s => s.position) + .ThenByDescending(s => s.length) + .ToList(); + + // get the leftmost movie that is the longest + // movie are usually the first thing in release title, so we select the leftmost and longest match + var match = query.First().movie; + + _logger.Debug("Multiple movie matched {0} from title {1}", match.Title, title); + foreach (var entry in list) + { + _logger.Debug("Multiple movie match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle); + } + + return match; + } + + public Movie FindByTitle(string title, int year) + { + return _movieRepository.FindByTitle(title.CleanSeriesTitle(), year); + } + + public void DeleteMovie(int movieId, bool deleteFiles) + { + var movie = _movieRepository.Get(movieId); + _movieRepository.Delete(movieId); + _eventAggregator.PublishEvent(new MovieDeletedEvent(movie, deleteFiles)); + } + + public List<Movie> GetAllMovies() + { + return _movieRepository.All().ToList(); + } + + public Movie UpdateMovie(Movie movie) + { + var storedMovie = GetMovie(movie.Id); + + var updatedMovie = _movieRepository.Update(movie); + _eventAggregator.PublishEvent(new MovieEditedEvent(updatedMovie, storedMovie)); + + return updatedMovie; + } + + public List<Movie> UpdateMovie(List<Movie> movie) + { + _logger.Debug("Updating {0} movie", movie.Count); + foreach (var s in movie) + { + _logger.Trace("Updating: {0}", s.Title); + if (!s.RootFolderPath.IsNullOrWhiteSpace()) + { + var folderName = new DirectoryInfo(s.Path).Name; + s.Path = Path.Combine(s.RootFolderPath, folderName); + _logger.Trace("Changing path for {0} to {1}", s.Title, s.Path); + } + + else + { + _logger.Trace("Not changing path for: {0}", s.Title); + } + } + + _movieRepository.UpdateMany(movie); + _logger.Debug("{0} movie updated", movie.Count); + + return movie; + } + + public bool MoviePathExists(string folder) + { + return _movieRepository.MoviePathExists(folder); + } + + } +} diff --git a/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs b/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs new file mode 100644 index 000000000..fd2f87cd1 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Tv +{ + public static class MovieTitleNormalizer + { + private readonly static Dictionary<string, string> PreComputedTitles = new Dictionary<string, string> + { + { "tt_109823457098", "a to z" }, + }; + + public static string Normalize(string title, string imdbid) + { + if (PreComputedTitles.ContainsKey(imdbid)) + { + return PreComputedTitles[imdbid]; + } + + return Parser.Parser.NormalizeTitle(title).ToLower(); + } + } +} diff --git a/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs new file mode 100644 index 000000000..d694d00b4 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs @@ -0,0 +1,25 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MovieAncestorValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MovieAncestorValidator(IMovieService seriesService) + : base("Path is an ancestor of an existing path") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return !_seriesService.GetAllMovies().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs new file mode 100644 index 000000000..88519e41f --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs @@ -0,0 +1,26 @@ +using System; +using FluentValidation.Validators; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MovieExistsValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MovieExistsValidator(IMovieService seriesService) + : base("This series has already been added") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + var imdbid = context.PropertyValue.ToString(); + + return (!_seriesService.GetAllMovies().Exists(s => s.ImdbId == imdbid)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs b/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs new file mode 100644 index 000000000..690bd59f2 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs @@ -0,0 +1,27 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MoviePathValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MoviePathValidator(IMovieService seriesService) + : base("Path is already configured for another series") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + dynamic instance = context.ParentContext.InstanceToValidate; + var instanceId = (int)instance.Id; + + return (!_seriesService.GetAllMovies().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId)); + } + } +} \ No newline at end of file diff --git a/src/UI/Movies/MovieModel.js b/src/UI/Movies/MovieModel.js index 49c64dea7..a3e0d5a35 100644 --- a/src/UI/Movies/MovieModel.js +++ b/src/UI/Movies/MovieModel.js @@ -2,7 +2,7 @@ var Backbone = require('backbone'); var _ = require('underscore'); module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/movies', + urlRoot : window.NzbDrone.ApiRoot + '/movie', defaults : { episodeFileCount : 0, diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js index b6f0e2edb..2df59e282 100644 --- a/src/UI/Movies/MoviesCollection.js +++ b/src/UI/Movies/MoviesCollection.js @@ -10,9 +10,9 @@ var moment = require('moment'); require('../Mixins/backbone.signalr.mixin'); var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/movies', + url : window.NzbDrone.ApiRoot + '/movie', model : MovieModel, - tableName : 'movies', + tableName : 'movie', state : { sortKey : 'sortTitle', @@ -115,6 +115,6 @@ Collection = AsFilteredCollection.call(Collection); Collection = AsSortedCollection.call(Collection); Collection = AsPersistedStateCollection.call(Collection); -var data = ApiData.get('series'); +var data = ApiData.get('movie'); module.exports = new Collection(data, { full : true }).bindSignalR(); From b7c70d750a2c1dfd33204f3602cac42dc5bc93c6 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Thu, 29 Dec 2016 17:38:54 +0100 Subject: [PATCH 09/40] Movies should now show on the main page. However, a lot has to be done to the detail controller before it is really going to work. --- .../MediaCover/MediaCoverService.cs | 96 +++++ .../MediaCover/MediaCoversUpdatedEvent.cs | 7 + src/UI/AddMovies/SearchResultView.js | 2 + src/UI/Handlebars/Helpers/Series.js | 2 +- src/UI/Movies/Details/EpisodeNumberCell.js | 47 +++ .../Details/EpisodeNumberCellTemplate.hbs | 39 ++ src/UI/Movies/Details/EpisodeWarningCell.js | 21 ++ src/UI/Movies/Details/InfoView.js | 18 + src/UI/Movies/Details/InfoViewTemplate.hbs | 73 ++++ src/UI/Movies/Details/MoviesDetailsLayout.js | 264 +++++++++++++ .../Movies/Details/MoviesDetailsTemplate.hbs | 38 ++ src/UI/Movies/Details/SeasonCollectionView.js | 44 +++ src/UI/Movies/Details/SeasonLayout.js | 301 +++++++++++++++ .../Movies/Details/SeasonLayoutTemplate.hbs | 50 +++ src/UI/Movies/Index/EmptyTemplate.hbs | 16 + src/UI/Movies/Index/EmptyView.js | 5 + .../Movies/Index/EpisodeProgressPartial.hbs | 4 + src/UI/Movies/Index/FooterModel.js | 4 + src/UI/Movies/Index/FooterView.js | 5 + src/UI/Movies/Index/FooterViewTemplate.hbs | 46 +++ src/UI/Movies/Index/MoviesIndexItemView.js | 35 ++ src/UI/Movies/Index/MoviesIndexLayout.js | 354 ++++++++++++++++++ .../Index/MoviesIndexLayoutTemplate.hbs | 12 + .../Overview/SeriesOverviewCollectionView.js | 8 + .../SeriesOverviewCollectionViewTemplate.hbs | 1 + .../Index/Overview/SeriesOverviewItemView.js | 7 + .../SeriesOverviewItemViewTemplate.hbs | 56 +++ .../Posters/SeriesPostersCollectionView.js | 8 + .../SeriesPostersCollectionViewTemplate.hbs | 1 + .../Index/Posters/SeriesPostersItemView.js | 19 + .../Posters/SeriesPostersItemViewTemplate.hbs | 30 ++ src/UI/Movies/MoviesController.js | 34 ++ src/UI/main.js | 2 +- 33 files changed, 1647 insertions(+), 2 deletions(-) create mode 100644 src/UI/Movies/Details/EpisodeNumberCell.js create mode 100644 src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs create mode 100644 src/UI/Movies/Details/EpisodeWarningCell.js create mode 100644 src/UI/Movies/Details/InfoView.js create mode 100644 src/UI/Movies/Details/InfoViewTemplate.hbs create mode 100644 src/UI/Movies/Details/MoviesDetailsLayout.js create mode 100644 src/UI/Movies/Details/MoviesDetailsTemplate.hbs create mode 100644 src/UI/Movies/Details/SeasonCollectionView.js create mode 100644 src/UI/Movies/Details/SeasonLayout.js create mode 100644 src/UI/Movies/Details/SeasonLayoutTemplate.hbs create mode 100644 src/UI/Movies/Index/EmptyTemplate.hbs create mode 100644 src/UI/Movies/Index/EmptyView.js create mode 100644 src/UI/Movies/Index/EpisodeProgressPartial.hbs create mode 100644 src/UI/Movies/Index/FooterModel.js create mode 100644 src/UI/Movies/Index/FooterView.js create mode 100644 src/UI/Movies/Index/FooterViewTemplate.hbs create mode 100644 src/UI/Movies/Index/MoviesIndexItemView.js create mode 100644 src/UI/Movies/Index/MoviesIndexLayout.js create mode 100644 src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs create mode 100644 src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js create mode 100644 src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs create mode 100644 src/UI/Movies/Index/Overview/SeriesOverviewItemView.js create mode 100644 src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs create mode 100644 src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js create mode 100644 src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs create mode 100644 src/UI/Movies/Index/Posters/SeriesPostersItemView.js create mode 100644 src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs create mode 100644 src/UI/Movies/MoviesController.js diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index deb2b35a5..048d04068 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -22,6 +22,8 @@ namespace NzbDrone.Core.MediaCover public class MediaCoverService : IHandleAsync<SeriesUpdatedEvent>, + IHandleAsync<MovieUpdatedEvent>, + IHandleAsync<MovieAddedEvent>, IHandleAsync<SeriesDeletedEvent>, IMapCoversToLocal { @@ -83,6 +85,8 @@ namespace NzbDrone.Core.MediaCover return Path.Combine(_coverRootFolder, seriesId.ToString()); } + + private void EnsureCovers(Series series) { foreach (var cover in series.Images) @@ -110,6 +114,33 @@ namespace NzbDrone.Core.MediaCover } } + private void EnsureCovers(Movie movie) + { + foreach (var cover in movie.Images) + { + var fileName = GetCoverPath(movie.Id, cover.CoverType); + var alreadyExists = false; + try + { + alreadyExists = _coverExistsSpecification.AlreadyExists(cover.Url, fileName); + if (!alreadyExists) + { + DownloadCover(movie, cover); + } + } + catch (WebException e) + { + _logger.Warn(string.Format("Couldn't download media cover for {0}. {1}", movie, e.Message)); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't download media cover for " + movie); + } + + EnsureResizedCovers(movie, cover, !alreadyExists); + } + } + private void DownloadCover(Series series, MediaCover cover) { var fileName = GetCoverPath(series.Id, cover.CoverType); @@ -118,6 +149,14 @@ namespace NzbDrone.Core.MediaCover _httpClient.DownloadFile(cover.Url, fileName); } + private void DownloadCover(Movie series, MediaCover cover) + { + var fileName = GetCoverPath(series.Id, cover.CoverType); + + _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, series, cover.Url); + _httpClient.DownloadFile(cover.Url, fileName); + } + private void EnsureResizedCovers(Series series, MediaCover cover, bool forceResize) { int[] heights; @@ -163,12 +202,69 @@ namespace NzbDrone.Core.MediaCover } } + private void EnsureResizedCovers(Movie series, MediaCover cover, bool forceResize) + { + int[] heights; + + switch (cover.CoverType) + { + default: + return; + + case MediaCoverTypes.Poster: + case MediaCoverTypes.Headshot: + heights = new[] { 500, 250 }; + break; + + case MediaCoverTypes.Banner: + heights = new[] { 70, 35 }; + break; + + case MediaCoverTypes.Fanart: + case MediaCoverTypes.Screenshot: + heights = new[] { 360, 180 }; + break; + } + + foreach (var height in heights) + { + var mainFileName = GetCoverPath(series.Id, cover.CoverType); + var resizeFileName = GetCoverPath(series.Id, cover.CoverType, height); + + if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0) + { + _logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, series); + + try + { + _resizer.Resize(mainFileName, resizeFileName, height); + } + catch + { + _logger.Debug("Couldn't resize media cover {0}-{1} for {2}, using full size image instead.", cover.CoverType, height, series); + } + } + } + } + public void HandleAsync(SeriesUpdatedEvent message) { EnsureCovers(message.Series); _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Series)); } + public void HandleAsync(MovieUpdatedEvent message) + { + EnsureCovers(message.Movie); + _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Movie)); + } + + public void HandleAsync(MovieAddedEvent message) + { + EnsureCovers(message.Movie); + _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Movie)); + } + public void HandleAsync(SeriesDeletedEvent message) { var path = GetSeriesCoverPath(message.Series.Id); diff --git a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs index 7335f7f9b..2f56e7cb0 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs @@ -7,9 +7,16 @@ namespace NzbDrone.Core.MediaCover { public Series Series { get; set; } + public Movie Movie { get; set; } + public MediaCoversUpdatedEvent(Series series) { Series = series; } + + public MediaCoversUpdatedEvent(Movie movie) + { + Movie = movie; + } } } diff --git a/src/UI/AddMovies/SearchResultView.js b/src/UI/AddMovies/SearchResultView.js index ff697b7d9..839b2d1ee 100644 --- a/src/UI/AddMovies/SearchResultView.js +++ b/src/UI/AddMovies/SearchResultView.js @@ -43,6 +43,8 @@ var view = Marionette.ItemView.extend({ throw 'model is required'; } + console.log(this.route); + this.templateHelpers = {}; this._configureTemplateHelpers(); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index f22ad5165..a95b2b10e 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -40,7 +40,7 @@ Handlebars.registerHelper('tvMazeUrl', function() { }); Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/series/' + this.titleSlug; + return StatusModel.get('urlBase') + '/movies/' + this.titleSlug; }); Handlebars.registerHelper('percentOfEpisodes', function() { diff --git a/src/UI/Movies/Details/EpisodeNumberCell.js b/src/UI/Movies/Details/EpisodeNumberCell.js new file mode 100644 index 000000000..9a84e644e --- /dev/null +++ b/src/UI/Movies/Details/EpisodeNumberCell.js @@ -0,0 +1,47 @@ +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var reqres = require('../../reqres'); +var SeriesCollection = require('../SeriesCollection'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-number-cell', + template : 'Series/Details/EpisodeNumberCellTemplate', + + render : function() { + this.$el.empty(); + this.$el.html(this.model.get('episodeNumber')); + + var series = SeriesCollection.get(this.model.get('seriesId')); + + if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) { + this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber'))); + } + + var alternateTitles = []; + + if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { + alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); + } + + if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { + this.templateFunction = Marionette.TemplateCache.get(this.template); + + var json = this.model.toJSON(); + json.alternateTitles = alternateTitles; + + var html = this.templateFunction(json); + + this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : 'Scene Information', + placement : 'right', + container : this.$el + }); + } + + this.delegateEvents(); + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs b/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs new file mode 100644 index 000000000..a9028a423 --- /dev/null +++ b/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs @@ -0,0 +1,39 @@ +<div class="scene-info"> + {{#if sceneSeasonNumber}} + <div class="row"> + <div class="key">Season</div> + <div class="value">{{sceneSeasonNumber}}</div> + </div> + {{/if}} + + {{#if sceneEpisodeNumber}} + <div class="row"> + <div class="key">Episode</div> + <div class="value">{{sceneEpisodeNumber}}</div> + </div> + {{/if}} + + {{#if sceneAbsoluteEpisodeNumber}} + <div class="row"> + <div class="key">Absolute</div> + <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> + </div> + {{/if}} + + {{#if alternateTitles}} + <div class="row"> + {{#if_gt alternateTitles.length compare="1"}} + <div class="key">Titles</div> + {{else}} + <div class="key">Title</div> + {{/if_gt}} + <div class="value"> + <ul> + {{#each alternateTitles}} + <li>{{title}}</li> + {{/each}} + </ul> + </div> + </div> + {{/if}} +</div> \ No newline at end of file diff --git a/src/UI/Movies/Details/EpisodeWarningCell.js b/src/UI/Movies/Details/EpisodeWarningCell.js new file mode 100644 index 000000000..c9befe7a1 --- /dev/null +++ b/src/UI/Movies/Details/EpisodeWarningCell.js @@ -0,0 +1,21 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SeriesCollection = require('../SeriesCollection'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-warning-cell', + + render : function() { + this.$el.empty(); + + if (this.model.get('unverifiedSceneNumbering')) { + this.$el.html('<i class="icon-sonarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); + } + + else if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { + this.$el.html('<i class="icon-sonarr-form-warning" title="Episode does not have an absolute episode number"></i>'); + } + + this.delegateEvents(); + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/InfoView.js b/src/UI/Movies/Details/InfoView.js new file mode 100644 index 000000000..c7fab9fc4 --- /dev/null +++ b/src/UI/Movies/Details/InfoView.js @@ -0,0 +1,18 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Series/Details/InfoViewTemplate', + + initialize : function(options) { + this.episodeFileCollection = options.episodeFileCollection; + + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.episodeFileCollection, 'sync', this.render); + }, + + templateHelpers : function() { + return { + fileCount : this.episodeFileCollection.length + }; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/InfoViewTemplate.hbs b/src/UI/Movies/Details/InfoViewTemplate.hbs new file mode 100644 index 000000000..666003f77 --- /dev/null +++ b/src/UI/Movies/Details/InfoViewTemplate.hbs @@ -0,0 +1,73 @@ +<div class="row"> + <div class="col-md-9"> + {{profile profileId}} + + {{#if network}} + <span class="label label-info">{{network}}</span> + {{/if}} + + <span class="label label-info">{{runtime}} minutes</span> + <span class="label label-info">{{path}}</span> + + {{#if ratings}} + <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> + {{/if}} + + <span class="label label-info">{{Bytes sizeOnDisk}}</span> + + {{#if_eq fileCount compare="1"}} + <span class="label label-info"> 1 file</span> + {{else}} + <span class="label label-info"> {{fileCount}} files</span> + {{/if_eq}} + + {{#if_eq status compare="continuing"}} + <span class="label label-info">Continuing</span> + {{else}} + <span class="label label-default">Ended</span> + {{/if_eq}} + </div> + <div class="col-md-3"> + <span class="series-info-links"> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a> + + <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a>--> + + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} + + {{#if tvRageId}} + <a href="{{tvRageUrl}}" class="label label-info">TV Rage</a> + {{/if}} + + {{#if tvMazeId}} + <a href="{{tvMazeUrl}}" class="label label-info">TV Maze</a> + {{/if}} + </span> + </div> +</div> + +{{#if alternateTitles}} +<div class="row"> + <div class="col-md-12"> + {{#each alternateTitles}} + {{#if_eq seasonNumber compare="-1"}} + <span class="label label-default">{{title}}</span> + {{/if_eq}} + + {{#if_eq sceneSeasonNumber compare="-1"}} + <span class="label label-default">{{title}}</span> + {{/if_eq}} + {{/each}} + </div> +</div> +{{/if}} + +{{#if tags}} +<div class="row"> + <div class="col-md-12"> + {{tagDisplay tags}} + </div> +</div> +{{/if}} diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js new file mode 100644 index 000000000..eb8a74e28 --- /dev/null +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -0,0 +1,264 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var vent = require('vent'); +var reqres = require('../../reqres'); +var Marionette = require('marionette'); +var Backbone = require('backbone'); +var MoviesCollection = require('../MoviesCollection'); +var InfoView = require('./InfoView'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); +require('backstrech'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + itemViewContainer : '.x-movie-seasons', + template : 'Movies/Details/MoviesDetailsTemplate', + + regions : { + seasons : '#seasons', + info : '#info' + }, + + ui : { + header : '.x-header', + monitored : '.x-monitored', + edit : '.x-edit', + refresh : '.x-refresh', + rename : '.x-rename', + search : '.x-search', + poster : '.x-movie-poster', + manualSearch : '.x-manual-search' + }, + + events : { + 'click .x-episode-file-editor' : '_openEpisodeFileEditor', + 'click .x-monitored' : '_toggleMonitored', + 'click .x-edit' : '_editMovies', + 'click .x-refresh' : '_refreshMovies', + 'click .x-rename' : '_renameMovies', + 'click .x-search' : '_moviesSearch', + 'click .x-manual-search' : '_manualSearchM' + }, + + initialize : function() { + this.moviesCollection = MoviesCollection.clone(); + this.moviesCollection.shadowCollection.bindSignalR(); + + this.listenTo(this.model, 'change:monitored', this._setMonitoredState); + this.listenTo(this.model, 'remove', this._moviesRemoved); + this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); + + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(); + } + }); + + this.listenTo(this.model, 'change:images', this._updateImages); + }, + + onShow : function() { + this._showBackdrop(); + this._showSeasons(); + this._setMonitoredState(); + this._showInfo(); + }, + + onRender : function() { + CommandController.bindToCommand({ + element : this.ui.refresh, + command : { + name : 'refreshMovies' + } + }); + CommandController.bindToCommand({ + element : this.ui.search, + command : { + name : 'moviesSearch' + } + }); + + CommandController.bindToCommand({ + element : this.ui.rename, + command : { + name : 'renameFiles', + movieId : this.model.id, + seasonNumber : -1 + } + }); + }, + + onClose : function() { + if (this._backstrech) { + this._backstrech.destroy(); + delete this._backstrech; + } + + $('body').removeClass('backdrop'); + reqres.removeHandler(reqres.Requests.GetEpisodeFileById); + }, + + _getImage : function(type) { + var image = _.where(this.model.get('images'), { coverType : type }); + + if (image && image[0]) { + return image[0].url; + } + + return undefined; + }, + + _toggleMonitored : function() { + var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); + + this.ui.monitored.spinForPromise(savePromise); + }, + + _setMonitoredState : function() { + var monitored = this.model.get('monitored'); + + this.ui.monitored.removeAttr('data-idle-icon'); + this.ui.monitored.removeClass('fa-spin icon-sonarr-spinner'); + + if (monitored) { + this.ui.monitored.addClass('icon-sonarr-monitored'); + this.ui.monitored.removeClass('icon-sonarr-unmonitored'); + this.$el.removeClass('movie-not-monitored'); + } else { + this.ui.monitored.addClass('icon-sonarr-unmonitored'); + this.ui.monitored.removeClass('icon-sonarr-monitored'); + this.$el.addClass('movie-not-monitored'); + } + }, + + _editMovies : function() { + vent.trigger(vent.Commands.EditMoviesCommand, { movie : this.model }); + }, + + _refreshMovies : function() { + CommandController.Execute('refreshMovies', { + name : 'refreshMovies', + movieId : this.model.id + }); + }, + + _moviesRemoved : function() { + Backbone.history.navigate('/', { trigger : true }); + }, + + _renameMovies : function() { + vent.trigger(vent.Commands.ShowRenamePreview, { movie : this.model }); + }, + + _moviesSearch : function() { + CommandController.Execute('moviesSearch', { + name : 'moviesSearch', + movieId : this.model.id + }); + }, + + _showSeasons : function() { + var self = this; + + return; + + reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { + return self.episodeFileCollection.get(episodeFileId); + }); + + reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(moviesId, seasonNumber, sceneSeasonNumber) { + if (self.model.get('id') !== moviesId) { + return []; + } + + if (sceneSeasonNumber === undefined) { + sceneSeasonNumber = seasonNumber; + } + + return _.where(self.model.get('alternateTitles'), + function(alt) { + return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; + }); + }); + + $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { + var seasonCollectionView = new SeasonCollectionView({ + collection : self.seasonCollection, + episodeCollection : self.episodeCollection, + movies : self.model + }); + + if (!self.isClosed) { + self.seasons.show(seasonCollectionView); + } + }); + }, + + _showInfo : function() { + this.info.show(new InfoView({ + model : this.model, + episodeFileCollection : this.episodeFileCollection + })); + }, + + _commandComplete : function(options) { + if (options.command.get('name') === 'renamefiles') { + if (options.command.get('moviesId') === this.model.get('id')) { + this._refresh(); + } + } + }, + + _refresh : function() { + this.seasonCollection.add(this.model.get('seasons'), { merge : true }); + this.episodeCollection.fetch(); + this.episodeFileCollection.fetch(); + + this._setMonitoredState(); + this._showInfo(); + }, + + _openEpisodeFileEditor : function() { + var view = new EpisodeFileEditorLayout({ + movies : this.model, + episodeCollection : this.episodeCollection + }); + + vent.trigger(vent.Commands.OpenModalCommand, view); + }, + + _updateImages : function () { + var poster = this._getImage('poster'); + + if (poster) { + this.ui.poster.attr('src', poster); + } + + this._showBackdrop(); + }, + + _showBackdrop : function () { + $('body').addClass('backdrop'); + var fanArt = this._getImage('fanart'); + + if (fanArt) { + this._backstrech = $.backstretch(fanArt); + } else { + $('body').removeClass('backdrop'); + } + }, + + _manualSearchM : function() { + console.warn("Manual Search started"); + console.warn(this.model.get("moviesId")); + console.warn(this.model) + console.warn(this.episodeCollection); + vent.trigger(vent.Commands.ShowEpisodeDetails, { + episode : this.episodeCollection.models[0], + hideMoviesLink : true, + openingTab : 'search' + }); + } +}); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs new file mode 100644 index 000000000..8bacf94b0 --- /dev/null +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -0,0 +1,38 @@ +<div class="row movie-page-header"> + <div class="visible-lg col-lg-2 poster"> + {{poster}} + </div> + <div class="col-md-12 col-lg-10"> + <div> + <h1 class="header-text"> + <i class="x-monitored" title="Toggle monitored state for movie"/> + {{title}} + <div class="movie-actions pull-right"> + <div class="x-episode-file-editor"> + <i class="icon-sonarr-episode-file" title="Modify episode files for movie"/> + </div> + <div class="x-refresh"> + <i class="icon-sonarr-refresh icon-can-spin" title="Update movie info and scan disk"/> + </div> + <div class="x-rename"> + <i class="icon-sonarr-rename" title="Preview rename for all episodes"/> + </div> + <div class="x-search"> + <i class="icon-sonarr-search" title="Search for movie"/> + </div> + <div class="x-manual-search"> + <i class="icon-sonarr-search-manual" title="Manual Search"/> + </div> + <div class="x-edit"> + <i class="icon-sonarr-edit" title="Edit movie"/> + </div> + </div> + </h1> + </div> + <div class="movie-detail-overview"> + {{overview}} + </div> + <div id="info" class="movie-info"></div> + </div> +</div> +<div id="seasons"></div> diff --git a/src/UI/Movies/Details/SeasonCollectionView.js b/src/UI/Movies/Details/SeasonCollectionView.js new file mode 100644 index 000000000..24da6171c --- /dev/null +++ b/src/UI/Movies/Details/SeasonCollectionView.js @@ -0,0 +1,44 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); +var SeasonLayout = require('./SeasonLayout'); +var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); + +var view = Marionette.CollectionView.extend({ + + itemView : SeasonLayout, + + initialize : function(options) { + if (!options.episodeCollection) { + throw 'episodeCollection is needed'; + } + + this.episodeCollection = options.episodeCollection; + this.series = options.series; + }, + + itemViewOptions : function() { + return { + episodeCollection : this.episodeCollection, + series : this.series + }; + }, + + onEpisodeGrabbed : function(message) { + if (message.episode.series.id !== this.episodeCollection.seriesId) { + return; + } + + var self = this; + + _.each(message.episode.episodes, function(episode) { + var ep = self.episodeCollection.get(episode.id); + ep.set('downloading', true); + }); + + this.render(); + } +}); + +AsSortedCollectionView.call(view); + +module.exports = view; \ No newline at end of file diff --git a/src/UI/Movies/Details/SeasonLayout.js b/src/UI/Movies/Details/SeasonLayout.js new file mode 100644 index 000000000..cf10b6fa8 --- /dev/null +++ b/src/UI/Movies/Details/SeasonLayout.js @@ -0,0 +1,301 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var ToggleCell = require('../../Cells/EpisodeMonitoredCell'); +var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); +var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell'); +var EpisodeNumberCell = require('./EpisodeNumberCell'); +var EpisodeWarningCell = require('./EpisodeWarningCell'); +var CommandController = require('../../Commands/CommandController'); +var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); +var moment = require('moment'); +var _ = require('underscore'); +var Messenger = require('../../Shared/Messenger'); + +module.exports = Marionette.Layout.extend({ + template : 'Series/Details/SeasonLayoutTemplate', + + ui : { + seasonSearch : '.x-season-search', + seasonMonitored : '.x-season-monitored', + seasonRename : '.x-season-rename' + }, + + events : { + 'click .x-season-episode-file-editor' : '_openEpisodeFileEditor', + 'click .x-season-monitored' : '_seasonMonitored', + 'click .x-season-search' : '_seasonSearch', + 'click .x-season-rename' : '_seasonRename', + 'click .x-show-hide-episodes' : '_showHideEpisodes', + 'dblclick .series-season h2' : '_showHideEpisodes' + }, + + regions : { + episodeGrid : '.x-episode-grid' + }, + + columns : [ + { + name : 'monitored', + label : '', + cell : ToggleCell, + trueClass : 'icon-sonarr-monitored', + falseClass : 'icon-sonarr-unmonitored', + tooltip : 'Toggle monitored status', + sortable : false + }, + { + name : 'episodeNumber', + label : '#', + cell : EpisodeNumberCell + }, + { + name : 'this', + label : '', + cell : EpisodeWarningCell, + sortable : false, + className : 'episode-warning-cell' + }, + { + name : 'this', + label : 'Title', + hideSeriesLink : true, + cell : EpisodeTitleCell, + sortable : false + }, + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell + }, + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable : false + }, + { + name : 'this', + label : '', + cell : EpisodeActionsCell, + sortable : false + } + ], + + templateHelpers : function() { + var episodeCount = this.episodeCollection.filter(function(episode) { + return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()); + }).length; + + var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length; + var percentOfEpisodes = 100; + + if (episodeCount > 0) { + percentOfEpisodes = episodeFileCount / episodeCount * 100; + } + + return { + showingEpisodes : this.showingEpisodes, + episodeCount : episodeCount, + episodeFileCount : episodeFileCount, + percentOfEpisodes : percentOfEpisodes + }; + }, + + initialize : function(options) { + if (!options.episodeCollection) { + throw 'episodeCollection is required'; + } + + this.series = options.series; + this.fullEpisodeCollection = options.episodeCollection; + this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')); + this._updateEpisodeCollection(); + + this.showingEpisodes = this._shouldShowEpisodes(); + + this.listenTo(this.model, 'sync', this._afterSeasonMonitored); + this.listenTo(this.episodeCollection, 'sync', this.render); + + this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes); + }, + + onRender : function() { + if (this.showingEpisodes) { + this._showEpisodes(); + } + + this._setSeasonMonitoredState(); + + CommandController.bindToCommand({ + element : this.ui.seasonSearch, + command : { + name : 'seasonSearch', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + } + }); + + CommandController.bindToCommand({ + element : this.ui.seasonRename, + command : { + name : 'renameFiles', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + } + }); + }, + + _seasonSearch : function() { + CommandController.Execute('seasonSearch', { + name : 'seasonSearch', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + }); + }, + + _seasonRename : function() { + vent.trigger(vent.Commands.ShowRenamePreview, { + series : this.series, + seasonNumber : this.model.get('seasonNumber') + }); + }, + + _seasonMonitored : function() { + if (!this.series.get('monitored')) { + + Messenger.show({ + message : 'Unable to change monitored state when series is not monitored', + type : 'error' + }); + + return; + } + + var name = 'monitored'; + this.model.set(name, !this.model.get(name)); + this.series.setSeasonMonitored(this.model.get('seasonNumber')); + + var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this)); + + this.ui.seasonMonitored.spinForPromise(savePromise); + }, + + _afterSeasonMonitored : function() { + var self = this; + + _.each(this.episodeCollection.models, function(episode) { + episode.set({ monitored : self.model.get('monitored') }); + }); + + this.render(); + }, + + _setSeasonMonitoredState : function() { + this.ui.seasonMonitored.removeClass('icon-sonarr-spinner fa-spin'); + + if (this.model.get('monitored')) { + this.ui.seasonMonitored.addClass('icon-sonarr-monitored'); + this.ui.seasonMonitored.removeClass('icon-sonarr-unmonitored'); + } else { + this.ui.seasonMonitored.addClass('icon-sonarr-unmonitored'); + this.ui.seasonMonitored.removeClass('icon-sonarr-monitored'); + } + }, + + _showEpisodes : function() { + this.episodeGrid.show(new Backgrid.Grid({ + columns : this.columns, + collection : this.episodeCollection, + className : 'table table-hover season-grid' + })); + }, + + _shouldShowEpisodes : function() { + var startDate = moment().add('month', -1); + var endDate = moment().add('year', 1); + + return this.episodeCollection.some(function(episode) { + var airDate = episode.get('airDateUtc'); + + if (airDate) { + var airDateMoment = moment(airDate); + + if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { + return true; + } + } + + return false; + }); + }, + + _showHideEpisodes : function() { + if (this.showingEpisodes) { + this.showingEpisodes = false; + this.episodeGrid.close(); + } else { + this.showingEpisodes = true; + this._showEpisodes(); + } + + this.templateHelpers.showingEpisodes = this.showingEpisodes; + this.render(); + }, + + _episodeMonitoredToggled : function(options) { + var model = options.model; + var shiftKey = options.shiftKey; + + if (!this.episodeCollection.get(model.get('id'))) { + return; + } + + if (!shiftKey) { + return; + } + + var lastToggled = this.episodeCollection.lastToggled; + + if (!lastToggled) { + return; + } + + var currentIndex = this.episodeCollection.indexOf(model); + var lastIndex = this.episodeCollection.indexOf(lastToggled); + + var low = Math.min(currentIndex, lastIndex); + var high = Math.max(currentIndex, lastIndex); + var range = _.range(low + 1, high); + + this.episodeCollection.lastToggled = model; + }, + + _updateEpisodeCollection : function() { + var self = this; + + this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true }); + + this.episodeCollection.each(function(model) { + model.episodeCollection = self.episodeCollection; + }); + }, + + _refreshEpisodes : function() { + this._updateEpisodeCollection(); + this.episodeCollection.fullCollection.sort(); + this.render(); + }, + + _openEpisodeFileEditor : function() { + var view = new EpisodeFileEditorLayout({ + model : this.model, + series : this.series, + episodeCollection : this.episodeCollection + }); + + vent.trigger(vent.Commands.OpenModalCommand, view); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/SeasonLayoutTemplate.hbs b/src/UI/Movies/Details/SeasonLayoutTemplate.hbs new file mode 100644 index 000000000..06034f19d --- /dev/null +++ b/src/UI/Movies/Details/SeasonLayoutTemplate.hbs @@ -0,0 +1,50 @@ +<div class="series-season" id="season-{{seasonNumber}}"> + <h2> + <i class="x-season-monitored season-monitored clickable" title="Toggle season monitored status"/> + + {{#if seasonNumber}} + Season {{seasonNumber}} + {{else}} + Specials + {{/if}} + + + {{#if_eq episodeCount compare=0}} + {{#if monitored}} + <span class="badge badge-primary season-status" title="No aired episodes"> </span> + {{else}} + <span class="badge badge-warning season-status" title="Season is not monitored"> </span> + {{/if}} + {{else}} + {{#if_eq percentOfEpisodes compare=100}} + <span class="badge badge-success season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> + {{else}} + <span class="badge badge-danger season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> + {{/if_eq}} + {{/if_eq}} + + <span class="season-actions pull-right"> + <div class="x-season-episode-file-editor"> + <i class="icon-sonarr-episode-file" title="Modify episode files for season"/> + </div> + <div class="x-season-rename"> + <i class="icon-sonarr-rename" title="Preview rename for season {{seasonNumber}}"/> + </div> + <div class="x-season-search"> + <i class="icon-sonarr-search" title="Search for monitored episodes in season {{seasonNumber}}"/> + </div> + </span> + </h2> + <div class="show-hide-episodes x-show-hide-episodes"> + <h4> + {{#if showingEpisodes}} + <i class="icon-sonarr-panel-hide"/> + Hide Episodes + {{else}} + <i class="icon-sonarr-panel-show"/> + Show Episodes + {{/if}} + </h4> + </div> + <div class="x-episode-grid table-responsive"></div> +</div> diff --git a/src/UI/Movies/Index/EmptyTemplate.hbs b/src/UI/Movies/Index/EmptyTemplate.hbs new file mode 100644 index 000000000..06fb40fe5 --- /dev/null +++ b/src/UI/Movies/Index/EmptyTemplate.hbs @@ -0,0 +1,16 @@ +<div class="no-series"> + <div class="row"> + <div class="well col-md-12"> + <i class="icon-sonarr-comment"/> + You must be new around here, You should add some series. + </div> + </div> + <div class="row"> + <div class="col-md-4 col-md-offset-4"> + <a href="/addmovies" class='btn btn-lg btn-block btn-success x-add-series'> + <i class='icon-sonarr-add'></i> + Add Movie + </a> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/EmptyView.js b/src/UI/Movies/Index/EmptyView.js new file mode 100644 index 000000000..01dcc07a4 --- /dev/null +++ b/src/UI/Movies/Index/EmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'Series/Index/EmptyTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/EpisodeProgressPartial.hbs b/src/UI/Movies/Index/EpisodeProgressPartial.hbs new file mode 100644 index 000000000..db5c49a2b --- /dev/null +++ b/src/UI/Movies/Index/EpisodeProgressPartial.hbs @@ -0,0 +1,4 @@ +<div class="progress episode-progress"> + <span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span> + <div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> +</div> \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterModel.js b/src/UI/Movies/Index/FooterModel.js new file mode 100644 index 000000000..235552061 --- /dev/null +++ b/src/UI/Movies/Index/FooterModel.js @@ -0,0 +1,4 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterView.js b/src/UI/Movies/Index/FooterView.js new file mode 100644 index 000000000..1d31cc404 --- /dev/null +++ b/src/UI/Movies/Index/FooterView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'Series/Index/FooterViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterViewTemplate.hbs b/src/UI/Movies/Index/FooterViewTemplate.hbs new file mode 100644 index 000000000..1b45fa747 --- /dev/null +++ b/src/UI/Movies/Index/FooterViewTemplate.hbs @@ -0,0 +1,46 @@ +<div class="row"> + <div class="series-legend legend col-xs-6 col-sm-4"> + <ul class='legend-labels'> + <li><span class="progress-bar"></span>Continuing (All episodes downloaded)</li> + <li><span class="progress-bar-success"></span>Ended (All episodes downloaded)</li> + <li><span class="progress-bar-danger"></span>Missing Episodes (Series monitored)</li> + <li><span class="progress-bar-warning"></span>Missing Episodes (Series not monitored)</li> + </ul> + </div> + <div class="col-xs-5 col-sm-7"> + <div class="row"> + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Series</dt> + <dd>{{series}}</dd> + + <dt>Ended</dt> + <dd>{{ended}}</dd> + + <dt>Continuing</dt> + <dd>{{continuing}}</dd> + </dl> + </div> + + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Monitored</dt> + <dd>{{monitored}}</dd> + + <dt>Unmonitored</dt> + <dd>{{unmonitored}}</dd> + </dl> + </div> + + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Episodes</dt> + <dd>{{episodes}}</dd> + + <dt>Files</dt> + <dd>{{episodeFiles}}</dd> + </dl> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/MoviesIndexItemView.js b/src/UI/Movies/Index/MoviesIndexItemView.js new file mode 100644 index 000000000..427fe489e --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexItemView.js @@ -0,0 +1,35 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var CommandController = require('../../Commands/CommandController'); + +module.exports = Marionette.ItemView.extend({ + ui : { + refresh : '.x-refresh' + }, + + events : { + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' + }, + + onRender : function() { + CommandController.bindToCommand({ + element : this.ui.refresh, + command : { + name : 'refreshSeries', + seriesId : this.model.get('id') + } + }); + }, + + _editSeries : function() { + vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); + }, + + _refreshSeries : function() { + CommandController.Execute('refreshSeries', { + name : 'refreshSeries', + seriesId : this.model.id + }); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/MoviesIndexLayout.js b/src/UI/Movies/Index/MoviesIndexLayout.js new file mode 100644 index 000000000..128575333 --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexLayout.js @@ -0,0 +1,354 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var PosterCollectionView = require('./Posters/SeriesPostersCollectionView'); +var ListCollectionView = require('./Overview/SeriesOverviewCollectionView'); +var EmptyView = require('./EmptyView'); +var MoviesCollection = require('../MoviesCollection'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var ProfileCell = require('../../Cells/ProfileCell'); +var EpisodeProgressCell = require('../../Cells/EpisodeProgressCell'); +var SeriesActionsCell = require('../../Cells/SeriesActionsCell'); +var SeriesStatusCell = require('../../Cells/SeriesStatusCell'); +var FooterView = require('./FooterView'); +var FooterModel = require('./FooterModel'); +var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Index/MoviesIndexLayoutTemplate', + + regions : { + seriesRegion : '#x-series', + toolbar : '#x-toolbar', + toolbar2 : '#x-toolbar2', + footer : '#x-series-footer' + }, + + columns : [ + { + name : 'statusWeight', + label : '', + cell : SeriesStatusCell + }, + { + name : 'title', + label : 'Title', + cell : SeriesTitleCell, + cellValue : 'this', + sortValue : 'sortTitle' + }, + { + name : 'seasonCount', + label : 'Seasons', + cell : 'integer' + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell + }, + { + name : 'network', + label : 'Network', + cell : 'string' + }, + { + name : 'nextAiring', + label : 'Next Airing', + cell : RelativeDateCell + }, + { + name : 'percentOfEpisodes', + label : 'Episodes', + cell : EpisodeProgressCell, + className : 'episode-progress-cell' + }, + { + name : 'this', + label : '', + sortable : false, + cell : SeriesActionsCell + } + ], + + leftSideButtons : { + type : 'default', + storeState : false, + collapse : true, + items : [ + { + title : 'Add Movie', + icon : 'icon-sonarr-add', + route : 'addmovies' + }, + { + title : 'Season Pass', + icon : 'icon-sonarr-monitored', + route : 'seasonpass' + }, + { + title : 'Series Editor', + icon : 'icon-sonarr-edit', + route : 'serieseditor' + }, + { + title : 'RSS Sync', + icon : 'icon-sonarr-rss', + command : 'rsssync', + errorMessage : 'RSS Sync Failed!' + }, + { + title : 'Update Library', + icon : 'icon-sonarr-refresh', + command : 'refreshseries', + successMessage : 'Library was updated!', + errorMessage : 'Library update failed!' + } + ] + }, + + initialize : function() { + this.seriesCollection = MoviesCollection.clone(); + this.seriesCollection.shadowCollection.bindSignalR(); + + this.listenTo(this.seriesCollection.shadowCollection, 'sync', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(this.seriesCollection.shadowCollection, 'add', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(this.seriesCollection.shadowCollection, 'remove', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.sortingOptions = { + type : 'sorting', + storeState : false, + viewCollection : this.seriesCollection, + items : [ + { + title : 'Title', + name : 'title' + }, + { + title : 'Seasons', + name : 'seasonCount' + }, + { + title : 'Quality', + name : 'profileId' + }, + { + title : 'Network', + name : 'network' + }, + { + title : 'Next Airing', + name : 'nextAiring' + }, + { + title : 'Episodes', + name : 'percentOfEpisodes' + } + ] + }; + + this.filteringOptions = { + type : 'radio', + storeState : true, + menuKey : 'series.filterMode', + defaultAction : 'all', + items : [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-sonarr-all', + callback : this._setFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-sonarr-monitored', + callback : this._setFilter + }, + { + key : 'continuing', + title : '', + tooltip : 'Continuing Only', + icon : 'icon-sonarr-series-continuing', + callback : this._setFilter + }, + { + key : 'ended', + title : '', + tooltip : 'Ended Only', + icon : 'icon-sonarr-series-ended', + callback : this._setFilter + }, + { + key : 'missing', + title : '', + tooltip : 'Missing', + icon : 'icon-sonarr-missing', + callback : this._setFilter + } + ] + }; + + this.viewButtons = { + type : 'radio', + storeState : true, + menuKey : 'seriesViewMode', + defaultAction : 'listView', + items : [ + { + key : 'posterView', + title : '', + tooltip : 'Posters', + icon : 'icon-sonarr-view-poster', + callback : this._showPosters + }, + { + key : 'listView', + title : '', + tooltip : 'Overview List', + icon : 'icon-sonarr-view-list', + callback : this._showList + }, + { + key : 'tableView', + title : '', + tooltip : 'Table', + icon : 'icon-sonarr-view-table', + callback : this._showTable + } + ] + }; + }, + + onShow : function() { + this._showToolbar(); + this._fetchCollection(); + }, + + _showTable : function() { + this.currentView = new Backgrid.Grid({ + collection : this.seriesCollection, + columns : this.columns, + className : 'table table-hover' + }); + + this._renderView(); + }, + + _showList : function() { + this.currentView = new ListCollectionView({ + collection : this.seriesCollection + }); + + this._renderView(); + }, + + _showPosters : function() { + this.currentView = new PosterCollectionView({ + collection : this.seriesCollection + }); + + this._renderView(); + }, + + _renderView : function() { + if (MoviesCollection.length === 0) { + this.seriesRegion.show(new EmptyView()); + + this.toolbar.close(); + this.toolbar2.close(); + } else { + this.seriesRegion.show(this.currentView); + + this._showToolbar(); + this._showFooter(); + } + }, + + _fetchCollection : function() { + this.seriesCollection.fetch(); + }, + + _setFilter : function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.seriesCollection.setFilterMode(mode); + }, + + _showToolbar : function() { + if (this.toolbar.currentView) { + return; + } + + this.toolbar2.show(new ToolbarLayout({ + right : [ + this.filteringOptions + ], + context : this + })); + + this.toolbar.show(new ToolbarLayout({ + right : [ + this.sortingOptions, + this.viewButtons + ], + left : [ + this.leftSideButtons + ], + context : this + })); + }, + + _showFooter : function() { + var footerModel = new FooterModel(); + var series = MoviesCollection.models.length; + var episodes = 0; + var episodeFiles = 0; + var ended = 0; + var continuing = 0; + var monitored = 0; + + _.each(MoviesCollection.models, function(model) { + episodes += model.get('episodeCount'); + episodeFiles += model.get('episodeFileCount'); + + if (model.get('status').toLowerCase() === 'ended') { + ended++; + } else { + continuing++; + } + + if (model.get('monitored')) { + monitored++; + } + }); + + footerModel.set({ + series : series, + ended : ended, + continuing : continuing, + monitored : monitored, + unmonitored : series - monitored, + episodes : episodes, + episodeFiles : episodeFiles + }); + + this.footer.show(new FooterView({ model : footerModel })); + } +}); diff --git a/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs new file mode 100644 index 000000000..0c41b4108 --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs @@ -0,0 +1,12 @@ +<div class="toolbars"> + <div id="x-toolbar"></div> + <div id="x-toolbar2"></div> +</div> + +<div class="row"> + <div class="col-md-12"> + <div id="x-series" class="table-responsive"></div> + </div> +</div> + +<div id="x-series-footer"></div> diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js new file mode 100644 index 000000000..7db4b76f0 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var ListItemView = require('./SeriesOverviewItemView'); + +module.exports = Marionette.CompositeView.extend({ + itemView : ListItemView, + itemViewContainer : '#x-series-list', + template : 'Series/Index/Overview/SeriesOverviewCollectionViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs new file mode 100644 index 000000000..046bb3348 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs @@ -0,0 +1 @@ +<div id="x-series-list"/> diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js new file mode 100644 index 000000000..0d3a23227 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js @@ -0,0 +1,7 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var SeriesIndexItemView = require('../MoviesIndexItemView'); + +module.exports = SeriesIndexItemView.extend({ + template : 'Movies/Index/Overview/MoviesOverviewItemViewTemplate' +}); diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs new file mode 100644 index 000000000..ee6ddddee --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs @@ -0,0 +1,56 @@ +<div class="series-item"> + <div class="row"> + <div class="col-md-2 col-xs-3"> + <a href="{{route}}"> + {{poster}} + </a> + </div> + <div class="col-md-10 col-xs-9"> + <div class="row"> + <div class="col-md-10 col-xs-10"> + <a href="{{route}}" target="_blank"> + <h2>{{title}}</h2> + </a> + </div> + <div class="col-md-2 col-xs-2"> + <div class="pull-right series-overview-list-actions"> + <i class="icon-sonarr-refresh x-refresh" title="Update series info and scan disk"/> + <i class="icon-sonarr-edit x-edit" title="Edit Series"/> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 col-xs-12"> + <a href="{{route}}"> + <div> + {{overview}} + </div> + </a> + </div> + </div> + <div class="row"> + <div class="col-md-12"> +   + </div> + </div> + <div class="row"> + <div class="col-md-10 col-xs-8"> + {{#if_eq status compare="ended"}} + <span class="label label-danger">Ended</span> + {{/if_eq}} + + {{#if nextAiring}} + <span class="label label-default">{{RelativeDate nextAiring}}</span> + {{/if}} + + {{seasonCountHelper}} + + {{profile profileId}} + </div> + <div class="col-md-2 col-xs-4"> + {{> EpisodeProgressPartial }} + </div> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js b/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js new file mode 100644 index 000000000..0d6094f1c --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var PosterItemView = require('./SeriesPostersItemView'); + +module.exports = Marionette.CompositeView.extend({ + itemView : PosterItemView, + itemViewContainer : '#x-series-posters', + template : 'Series/Index/Posters/SeriesPostersCollectionViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs b/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs new file mode 100644 index 000000000..11b8e8ac7 --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs @@ -0,0 +1 @@ +<ul id="x-series-posters" class="series-posters"></ul> \ No newline at end of file diff --git a/src/UI/Movies/Index/Posters/SeriesPostersItemView.js b/src/UI/Movies/Index/Posters/SeriesPostersItemView.js new file mode 100644 index 000000000..bc4545906 --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersItemView.js @@ -0,0 +1,19 @@ +var SeriesIndexItemView = require('../MoviesIndexItemView'); + +module.exports = SeriesIndexItemView.extend({ + tagName : 'li', + template : 'Movies/Index/Posters/SeriesPostersItemViewTemplate', + + initialize : function() { + this.events['mouseenter .x-series-poster-container'] = 'posterHoverAction'; + this.events['mouseleave .x-series-poster-container'] = 'posterHoverAction'; + + this.ui.controls = '.x-series-controls'; + this.ui.title = '.x-title'; + }, + + posterHoverAction : function() { + this.ui.controls.slideToggle(); + this.ui.title.slideToggle(); + } +}); diff --git a/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs new file mode 100644 index 000000000..fba301c4f --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs @@ -0,0 +1,30 @@ +<div class="series-posters-item"> + <div class="center"> + <div class="series-poster-container x-series-poster-container"> + <div class="series-controls x-series-controls"> + <i class="icon-sonarr-refresh x-refresh" title="Refresh Series"/> + <i class="icon-sonarr-edit x-edit" title="Edit Series"/> + </div> + {{#unless_eq status compare="continuing"}} + <div class="ended-banner">Ended</div> + {{/unless_eq}} + <a href="{{route}}"> + {{poster}} + <div class="center title">{{title}}</div> + </a> + <div class="hidden-title x-title"> + {{title}} + </div> + </div> + </div> + + <div class="center"> + <div class="labels"> + {{> EpisodeProgressPartial }} + + {{#if nextAiring}} + <span class="label label-default">{{RelativeDate nextAiring}}</span> + {{/if}} + </div> + </div> +</div> diff --git a/src/UI/Movies/MoviesController.js b/src/UI/Movies/MoviesController.js new file mode 100644 index 000000000..ba0f825eb --- /dev/null +++ b/src/UI/Movies/MoviesController.js @@ -0,0 +1,34 @@ +var NzbDroneController = require('../Shared/NzbDroneController'); +var AppLayout = require('../AppLayout'); +var MoviesCollection = require('./MoviesCollection'); +var MoviesIndexLayout = require('./Index/MoviesIndexLayout'); +var MoviesDetailsLayout = require('./Details/MoviesDetailsLayout'); + +module.exports = NzbDroneController.extend({ + _originalInit : NzbDroneController.prototype.initialize, + + initialize : function() { + this.route('', this.series); + this.route('movies', this.series); + this.route('movies/:query', this.seriesDetails); + + this._originalInit.apply(this, arguments); + }, + + series : function() { + this.setTitle('Movies'); + this.showMainRegion(new MoviesIndexLayout()); + }, + + seriesDetails : function(query) { + var series = MoviesCollection.where({ titleSlug : query }); + + if (series.length !== 0) { + var targetMovie = series[0]; + this.setTitle(targetMovie.get('title')); + this.showMainRegion(new MoviesDetailsLayout({ model : targetMovie })); + } else { + this.showNotFound(); + } + } +}); diff --git a/src/UI/main.js b/src/UI/main.js index f46f68b93..3978b36e0 100644 --- a/src/UI/main.js +++ b/src/UI/main.js @@ -5,7 +5,7 @@ var RouteBinder = require('./jQuery/RouteBinder'); var SignalRBroadcaster = require('./Shared/SignalRBroadcaster'); var NavbarLayout = require('./Navbar/NavbarLayout'); var AppLayout = require('./AppLayout'); -var SeriesController = require('./Series/SeriesController'); +var SeriesController = require('./Movies/MoviesController'); var Router = require('./Router'); var ModalController = require('./Shared/Modal/ModalController'); var ControlPanelController = require('./Shared/ControlPanel/ControlPanelController'); From 2efda4933de297dcd14da3a68b0dc3c9528c1596 Mon Sep 17 00:00:00 2001 From: Leonardo Galli <leonardo.galli@bluewin.ch> Date: Thu, 29 Dec 2016 17:44:51 +0100 Subject: [PATCH 10/40] Changed the name in the UI to Radarr. --- src/UI/Series/SeriesController.js | 4 ++-- src/UI/Shared/NzbDroneController.js | 8 ++++---- src/UI/index.html | 4 ++-- src/UI/login.html | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js index 60d1049cd..89ba13752 100644 --- a/src/UI/Series/SeriesController.js +++ b/src/UI/Series/SeriesController.js @@ -16,7 +16,7 @@ module.exports = NzbDroneController.extend({ }, series : function() { - this.setTitle('Sonarr'); + this.setTitle('Radarr'); this.showMainRegion(new SeriesIndexLayout()); }, @@ -31,4 +31,4 @@ module.exports = NzbDroneController.extend({ this.showNotFound(); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Shared/NzbDroneController.js b/src/UI/Shared/NzbDroneController.js index a97dea369..4068260a3 100644 --- a/src/UI/Shared/NzbDroneController.js +++ b/src/UI/Shared/NzbDroneController.js @@ -16,10 +16,10 @@ module.exports = Marionette.AppRouter.extend({ setTitle : function(title) { title = title; - if (title === 'Sonarr') { - document.title = 'Sonarr'; + if (title === 'Radarr') { + document.title = 'Radarr'; } else { - document.title = title + ' - Sonarr'; + document.title = title + ' - Radarr'; } if (window.NzbDrone.Analytics && window.Piwik) { @@ -64,4 +64,4 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.mainRegion.show(view); } } -}); \ No newline at end of file +}); diff --git a/src/UI/index.html b/src/UI/index.html index e0c128e72..75acdd7ff 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -1,7 +1,7 @@ <!doctype html> <html> <head> - <title>Sonarr + Radarr @@ -72,7 +72,7 @@
diff --git a/src/UI/login.html b/src/UI/login.html index 487e62680..7818bd690 100644 --- a/src/UI/login.html +++ b/src/UI/login.html @@ -1,7 +1,7 @@ - Sonarr - Login + Radarr - Login From c874122fc094ef85c69f61facebbbaceba3a5368 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 30 Dec 2016 11:25:25 +0100 Subject: [PATCH 11/40] Use different folder to store sqlite database. Fixes #10. --- src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs | 2 +- src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs index 75b75093e..0d35aed70 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Common.EnvironmentInfo } else { - AppDataFolder = Path.Combine(Environment.GetFolderPath(DATA_SPECIAL_FOLDER, Environment.SpecialFolderOption.None), "NzbDrone"); + AppDataFolder = Path.Combine(Environment.GetFolderPath(DATA_SPECIAL_FOLDER, Environment.SpecialFolderOption.None), "Radarr"); } StartUpFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index 53d7a9a17..02367724e 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -52,13 +52,10 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Images").AsString() .WithColumn("Path").AsString() .WithColumn("Monitored").AsBoolean() - .WithColumn("QualityProfileId").AsInt32() - .WithColumn("SeasonFolder").AsBoolean() + .WithColumn("ProfileId").AsInt32() .WithColumn("LastInfoSync").AsDateTime().Nullable() .WithColumn("LastDiskSync").AsDateTime().Nullable() .WithColumn("Runtime").AsInt32() - .WithColumn("BacklogSetting").AsInt32() - .WithColumn("CustomStartDate").AsDateTime().Nullable() .WithColumn("InCinemas").AsDateTime().Nullable() .WithColumn("Year").AsInt32().Nullable() .WithColumn("Added").AsDateTime().Nullable() From 782f63f510c1e6a164f739ce79ed96be4d3406b1 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 30 Dec 2016 11:34:11 +0100 Subject: [PATCH 12/40] Update readme.md --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 2ae43c9fb..c1aa3d5a9 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,8 @@ This fork of Sonarr aims to turn it into something like Couchpotato. * Full support for specials and multi-episode releases * And a beautiful UI +## Download +The latest precompiled binary versions can be found here: https://github.com/galli-leo/Radarr/releases. ## Configuring Development Environment: ## From 2ea35adb98e5305100355041ca09492750bd7712 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 30 Dec 2016 11:58:39 +0100 Subject: [PATCH 13/40] Fixed issue with Homepage movies not loading correctly. --- src/UI/Movies/Index/Overview/SeriesOverviewItemView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js index 0d3a23227..dd718d315 100644 --- a/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js +++ b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js @@ -3,5 +3,5 @@ var Marionette = require('marionette'); var SeriesIndexItemView = require('../MoviesIndexItemView'); module.exports = SeriesIndexItemView.extend({ - template : 'Movies/Index/Overview/MoviesOverviewItemViewTemplate' + template : 'Movies/Index/Overview/SeriesOverviewItemViewTemplate' }); From 0fd0b31a609cf75d5660c105ae54b700175762f5 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Sat, 31 Dec 2016 14:42:32 -0500 Subject: [PATCH 14/40] Fix template references and 'movie' strings --- .../Existing/AddExistingSeriesCollectionViewTemplate.hbs | 2 +- src/UI/AddMovies/NotFoundViewTemplate.hbs | 2 +- src/UI/AddMovies/RootFolders/RootFolderCollectionView.js | 2 +- src/UI/AddMovies/RootFolders/RootFolderItemView.js | 2 +- src/UI/AddMovies/RootFolders/RootFolderLayout.js | 2 +- src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs | 4 ++-- src/UI/Movies/Details/EpisodeNumberCell.js | 2 +- src/UI/Movies/Details/InfoView.js | 2 +- src/UI/Movies/Details/SeasonLayout.js | 2 +- src/UI/Movies/Index/EmptyTemplate.hbs | 6 +++--- src/UI/Movies/Index/EmptyView.js | 2 +- src/UI/Movies/Index/FooterView.js | 2 +- .../Movies/Index/Overview/SeriesOverviewCollectionView.js | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs index d613a52d4..0928c0f38 100644 --- a/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs +++ b/src/UI/AddMovies/Existing/AddExistingSeriesCollectionViewTemplate.hbs @@ -1,5 +1,5 @@
- Loading search results from TheTVDB for your series, this may take a few minutes. + Loading search results from TheTVDB for your movies, this may take a few minutes.
\ No newline at end of file diff --git a/src/UI/AddMovies/NotFoundViewTemplate.hbs b/src/UI/AddMovies/NotFoundViewTemplate.hbs index e2d99bb63..7cc6eb25a 100644 --- a/src/UI/AddMovies/NotFoundViewTemplate.hbs +++ b/src/UI/AddMovies/NotFoundViewTemplate.hbs @@ -2,6 +2,6 @@

Sorry. We couldn't find any movies matching '{{term}}'

- Why can't I find my show? + [UPDATE LINK] Why can't I find my movie?
diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js index f781f21d7..f0704f342 100644 --- a/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js +++ b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js @@ -2,7 +2,7 @@ var Marionette = require('marionette'); var RootFolderItemView = require('./RootFolderItemView'); module.exports = Marionette.CompositeView.extend({ - template : 'AddSeries/RootFolders/RootFolderCollectionViewTemplate', + template : 'AddMovies/RootFolders/RootFolderCollectionViewTemplate', itemViewContainer : '.x-root-folders', itemView : RootFolderItemView }); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderItemView.js b/src/UI/AddMovies/RootFolders/RootFolderItemView.js index a0e98100b..7397f4e94 100644 --- a/src/UI/AddMovies/RootFolders/RootFolderItemView.js +++ b/src/UI/AddMovies/RootFolders/RootFolderItemView.js @@ -1,7 +1,7 @@ var Marionette = require('marionette'); module.exports = Marionette.ItemView.extend({ - template : 'AddSeries/RootFolders/RootFolderItemViewTemplate', + template : 'AddMovies/RootFolders/RootFolderItemViewTemplate', className : 'recent-folder', tagName : 'tr', diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayout.js b/src/UI/AddMovies/RootFolders/RootFolderLayout.js index 6dae383d7..f48890076 100644 --- a/src/UI/AddMovies/RootFolders/RootFolderLayout.js +++ b/src/UI/AddMovies/RootFolders/RootFolderLayout.js @@ -7,7 +7,7 @@ var AsValidatedView = require('../../Mixins/AsValidatedView'); require('../../Mixins/FileBrowser'); var Layout = Marionette.Layout.extend({ - template : 'AddSeries/RootFolders/RootFolderLayoutTemplate', + template : 'AddMovies/RootFolders/RootFolderLayoutTemplate', ui : { pathInput : '.x-path' diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs index 83cb9535d..1d6eae265 100644 --- a/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs +++ b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs @@ -5,7 +5,7 @@