From 40d7590f80675fd4ab8a1cc9b63130e329cebb37 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Thu, 29 Dec 2016 14:06:51 +0100 Subject: [PATCH] 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&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*jQJS*q*#+ zW`@77C>yIATJ{a)W}bTO^;2)0KJ(`JPbC8g-y&@7WsH1wi4NPF>Xz3TMJF;jNyqmK z=+JJ7rCZy|+jos|KIxhJOc~^_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_s2?F^D0IVH_3?!G@0+Jch^d1fIdOIEm+Q3h&@uyoZnQG0x*Fe1jk4s)`C4 z?I|W>OJi@tVS=c63Sd#wyu1AVoP;xvS~+p{%Hpq zTqv(VQBpF2lnPS0ObLsXT0#1aZ_+j|rTjk9q{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@PPcOtZJmi}PePI(FU1A#Awc$%K6L2a;;fWhjQ-tI8D!!ZLRUU75=HR zPYk?6_xxRX)44dMw4w|POPgVshEknAcq~}q>U51<>FVNwSEuV`m5Yt1iq*3W+s5{> zJUhgWuxHuJ>^*jdon;@f&)Jvk8}6lI8`5((6x0h`c>CbXg* z9oUL4>_9&TFoa=@Vhk1zz=n?*Jc38@7@oq@IF4s<0&n4Myn_$$AWq5#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!ww9_+?mj9?!rQpZE2$ZNSlCvgya=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 @@ + + 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 + { + 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 MapToResource(IEnumerable 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 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 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 Genres { get; set; } + public HashSet 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 ToResource(this IEnumerable 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 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(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, 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(); + 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 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 }; + string imdbid = slug; - return new Tuple>(series, episodes.ToList()); + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) + { + return new List(); + } + + try + { + return new List { GetMovieInfo(imdbid) }; + } + catch (SeriesNotFoundException) + { + return new List(); + } + } + + 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 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(); + 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 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(); - - 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); 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 @@ + @@ -778,6 +779,8 @@ + + @@ -1067,6 +1070,7 @@ + @@ -1075,6 +1079,7 @@ Code + 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(); + Genres = new List(); + Actors = new List(); + Tags = new HashSet(); + } + + 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 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 Genres { get; set; } + public List 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 { get; set; } + public HashSet 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-IAYdS1AYdS1AYkBjV1UkSoHCPDy0 z9s-C&Wr_oYPt?b_kJA!`D~&0m2l%e=LkviFvPXG$!nlvq5~VwXbZ79N3_n7Fcslt- zMR$g{#2^nC2pE{p0AD^!SRZp(fte-o`#sa2hh?P;cMlhaCXFw9t>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`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+_4R5bfp 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-<61TcZoAEEZ5u;;o2wOh`>A~|9$kN>+D{{DZ +
+
+ + +
+
+ +
+
+
+
+
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}} +
+
+ {{folder.path}} +
+
{{/if}} + +
+
+
+ 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 @@ +
+ You can also search by imdbid using the imdb: prefixes. +
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 @@ +
+

+ There was an error searching for '{{term}}'. +

+ + If the movie title contains non-alphanumeric characters try removing them, otherwise try your search again later. +
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 @@ +
+
+ Loading search results from TheTVDB for your series, this may take a few minutes. +
+
\ 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 @@ +
+
All
+
Monitor all episodes except specials
+
Future
+
Monitor episodes that have not aired yet
+
Missing
+
Monitor episodes that do not have files or have not aired yet
+
Existing
+
Monitor episodes that have files or have not aired yet
+
First Season
+
Monitor all episodes of the first season. All other seasons will be ignored
+
Latest Season
+
Monitor all episodes of the latest season and future seasons
+
None
+
No episodes will be monitored.
+ + +
\ 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 @@ + 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 @@ +
+

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

+ Why can't I find my show? + +
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 @@ + + + + + + + + +
+ Path + + Free Space +
\ 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 @@ + + {{path}} + + + {{Bytes freeSpace}} + + + + 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 @@ + 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 @@ + + 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 @@ +
+
+ +
+
+
+

+ {{titleWithYear}} + + + {{network}} + {{#unless_eq status compare="announced"}} + Released + {{/unless_eq}} + +

+
+
+
+
+ {{overview}} +
+
+
+ {{#unless existing}} + {{#unless path}} +
+ + {{> RootFolderSelectionPartial rootFolders}} +
+ {{/unless}} + +
+ + +
+ +
+ + {{> ProfileSelectionPartial profiles}} +
+ +
+ + +
+
+ {{/unless}} + + {{#unless existing}} + {{#if title}} +
+ + +
+ + + +
+
+ {{else}} + +
+ +
+ {{/if}} + {{else}} + + + {{/unless}} +
+
+
+
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 @@ + 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 @@
- + Add Movie 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 @@ +