From 31b9ec11161c92618e8a2caa736d757beeb075c3 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 10:27:59 +0100 Subject: [PATCH 1/7] Create .travis.yml Should enable automatically testing. --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..fa73c2e4f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: csharp +solution: src/NzbDrone.sln +script: # the following commands are just examples, use whatever your build process requires + - ./build.sh + - ./test.sh +install: + - sudo apt-get install nodejs + - sudo apt-get install npm + - sudo ln -s /usr/bin/nodejs /usr/bin/node From be6e6b910eaddf878a1c417d55ec4555852e373d Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 10:32:23 +0100 Subject: [PATCH 2/7] Update .travis.yml symlinking node was already done in apt-get and therefore resulted in an error. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fa73c2e4f..20d0a7699 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,3 @@ script: # the following commands are just examples, use whatever your build p install: - sudo apt-get install nodejs - sudo apt-get install npm - - sudo ln -s /usr/bin/nodejs /usr/bin/node From 097982334cf0258289b9141c05fa5cb176668639 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 10:40:09 +0100 Subject: [PATCH 3/7] Update .travis.yml Fixed syntax for test.sh --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 20d0a7699..93bdbb47d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ language: csharp solution: src/NzbDrone.sln script: # the following commands are just examples, use whatever your build process requires - ./build.sh - - ./test.sh + - chmod +x test.sh + - ./test.sh Linux Unit install: - sudo apt-get install nodejs - sudo apt-get install npm From d9e60eff6b6f4376c97b0d581d313fb4acdf0bb0 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 11:25:28 +0100 Subject: [PATCH 4/7] Update .travis.yml Removed tests since they take far too long --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 93bdbb47d..87ea22b9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ solution: src/NzbDrone.sln script: # the following commands are just examples, use whatever your build process requires - ./build.sh - chmod +x test.sh - - ./test.sh Linux Unit +# - ./test.sh Linux Unit Takes far too long, maybe even crashes travis :/ install: - sudo apt-get install nodejs - sudo apt-get install npm From 1f930c18e404daef09716369e3d54f03bbe31c4f Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 11:45:11 +0100 Subject: [PATCH 5/7] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index c1aa3d5a9..c615441d6 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# Radarr # +# Radarr [![Build Status](https://travis-ci.org/galli-leo/Radarr.svg?branch=develop)](https://travis-ci.org/galli-leo/Radarr)# This fork of Sonarr aims to turn it into something like Couchpotato. From 74b1c846a58174b55fba00fcffd1bbb11b739d53 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 13:20:50 +0100 Subject: [PATCH 6/7] Fixed css for movies. --- .DS_Store | Bin 8196 -> 10244 bytes .gitignore | 1 + gulp/less.js | 1 + src/.DS_Store | Bin 8196 -> 8196 bytes src/UI/.DS_Store | Bin 8196 -> 10244 bytes src/UI/Movies/movies.less | 471 ++++++++++++++++++++++++++++++++++++++ src/UI/index.html | 1 + 7 files changed, 474 insertions(+) create mode 100644 src/UI/Movies/movies.less diff --git a/.DS_Store b/.DS_Store index aea3758c75ea4196314b71002ff096f2512214bc..474ee832f549d58148405d309218030bf93b61f6 100644 GIT binary patch delta 1600 zcmeH{O>7fK6vyYE#CXjHGEN*rHbiz(ND#Pyt3Yv3lYko%!bbx*0tzLvvrAZIvuk@z zK%mN0LT$xITWcjGR25q2p@ncjE&>icR)P~B0&(DgDz!(1gv7@Mi8t#Is3ppc6SLZ# zdGp?znSHsj4f*SE&m;Qu{e z7m(%8byYKS`Yv6|4(`>}-0p2jdss77-5l96uG@Ob&=O{HO!Fh+^WCoD{@U3Mio8Qj z=Ct*eD*RM_Q7UPg8Wd%_(8RMEeN{<1{FX#zWy`phwROwnYbPg{24rRHur)SDr?w~OVjYUIQ|!AgY3DvmYbKrP zs@S}Klg9ms*FsW4#a>iQe?H7Yr4bNy?2G+S&*WwNJ_N}S!j}y%k&XGWMXx@Uw z?Xk{PQvoWAiZcv3^9_XENFAOXIh4G9%hX>O!3t_?#Aq1W1CIFwJ2OBVeP1uYCAs9juDGbAaNgy(W;sAlj;~>7m zVI0MG_#Qvt1b)O%IEhpE4QFu<6S#;=xa@QVKm4RD(&L-6reT>Q#6SS5`ES*^lFjx} zpQJiWJ3)R$YLCSt;b{9u&RyR7@mn0qmak$XRMG}ZyX9OzCBAM}tmX~ysrImuvMd9| zM*oN>;GuiKqQ_wNl%|kb?5`~4QCOzFN8B+NGEXa=nmHQ-6{VYJzA&IaTOxWVJ?B_J WGZLee8!wCRea5`ojA{DcjCldi%73{4 delta 133 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aKK$^w!H@)?Q?l5+BsfV`hc1se;e zu}^H^-OSFx!okS0*;-PPadWyqT&Cxeegmx8mncg{WY|M%xRGZUGK zJPW#TKL5^BT_N^Fdpy1QPo6nrp>pl|y2j?#j;_s1PQm5&R1vaYl@%?g9#)lX?6@lD zrp6>ap=h$I9T}Zgbv2$;Ml>m;=V*Wm7-JY)>)`q6erld!;+rcl~FW0(;MsZFr8_Pd0S<-%tqoHIO}PMH06J| zHyTCm1NV{p!hPkwam(Cya99l|1h^o=j{us`j!o!72;GRHAA=aeFk%?TK}=v0Q^+6- z9XXuA1zf}>%;FyI;{oRJ6wmPjFYyYm@fO9>%{EvQY-f9&CFq9tbg^+UZXl^oXX43p zoXX2AM!T=Czt1@kEqwNr!H7|E%88Zo66`E2eu)OPuD+HKfuoF>X!QGhf{n&i@|s(G zL?|;eVQy<1@d>L;`b>6o(#mo(A0|WH)U7aM@CK+3Z=t<-6Em};e%cGX<8Qim*Yp^E z1S!2RB9vV(wqYlBV-NP4^d@l#5{@DX4QWd8Bu-O?XJJr=S8x^Aa2+>r6Sr_1ckmF8 xuz<&Sg2g|nmhk;zI`|tWd$XZfNL0^b_FQ$r?p-c7%wPJSBU+JA<^PPx^Aq9T>^cAd delta 1136 zcmdUuO-vI(7>4I<3(UGvrho`#jfN^#1uO`G5S8*%5CJiU3JAe$*;Q7y+lFlriMkgK z{)MPu{EG(@qLKKQ5Hx!A;=z-tiSah@E z8j5C|II1bCk<^ZB>ZssFsnBi6F_oVx^c4wgfet8gQtgZ@&K@PL_zHcSDD8LZO{1zk z^Rig%+afTTTHU5LvD{;uxLshT&^@9V%V$f)a>^7TqRUZL=j|%Q-ITQpJ!9&qsl^Rm z%oA&<&^r{5#q9T)yjJJiL)jZT=-k{Po5`+(YOK5|L}{O+6g4P{s!)Gu{0eb~KNLtu z;wNPKM^2^nj(i}W$rtjKd?VkP zFboYN(2>MhoP&k)xQc6-!F9~yF`nQlUg8yA<1OA}kzefskEE55`sHOh;FBF(BtqJB zb1V_n<54@Ei6(T-Oa_A=;c|O2GP9_#Mikddt^3=u4}|BJa^0|2Gc;n8R>!I1CCf?` z*d~e_MZX|0l1`)&DL`#knTu;Xr837e=H_ZptaRLA8C&A(k>nW=)H z-B_xZ8XP|u<-1Fyus|7BA%AgwqZL3oYOxRXXtc?OaS&ZNgv02^00wQcGEUlLV=$1w zX-wdZP1xak0he$YQ@DYfxQ#owi~D$hhnT}7EZ{j_tPtmL|L|_-IiEd*U~#@Pe-`su Y1wTF3$~X8&)c%H6oOb8Gp>_|y0mTLN+W-In diff --git a/src/UI/.DS_Store b/src/UI/.DS_Store index df70b98af2a4a591eadb11f72de6def1744bbbb7..6cf6f2ffc6f7bf33d4dfa8c9bff24ec39d01ad06 100644 GIT binary patch delta 543 zcmZp1XbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~50D9Q=sFar4u4EYRY44Dk648=+1 z8w;ngPi)}b%+A5W!3|Qu$>7M4!jQ6AKtP019?D_xMQF(>E(=J?$xmWnU|KWTR#1|~ zUD(5ObClpVMiB=Kpnf1wcF=$_7#Jo?3olM&{08AO0j&oEFDMPA7=Hj6*zGES+BGGq zASbi9#K7PdBNHK|nUEC;&8(O(Zw!|S0R?c-QbPp zhI`oEP!?R2my@5D4wPlw%p)+3O_Yrx7#Ma%kPu8MPR>cn&(GOd*v}-$3^7uH8%Vo? qLVjc6cjn3bGJzsYkoW+H3osB2Knf=p$n;KrB61OXxKYu~=8OPX4_Z6` delta 128 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aKK$^w!H@);OX8H$t2iwlx+@{=|e zPGg_gz`L29gN1{UW%F6FX>1!yQW*uAfr^1Zf*VMw&ze*qn& + From 2a3b0304cb65f32e409cbd986977fe137f532675 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 2 Jan 2017 18:05:55 +0100 Subject: [PATCH 7/7] Fixed a few things with displaying the Movie Details Page. Implemented the first round of Searching for and Downloading movie Releases. ATM this is still a bit hacky and alot of things need to be cleaned up. However, one can now manually search for and download (only in qBittorrent) a movie torrent. --- src/NzbDrone.Api/Indexers/ReleaseModule.cs | 26 ++++ src/NzbDrone.Api/Indexers/ReleaseResource.cs | 7 +- .../DownloadApprovedFixture.cs | 5 +- .../DecisionEngine/DownloadDecision.cs | 21 +++ .../DecisionEngine/DownloadDecisionMaker.cs | 76 ++++++++- .../DownloadDecisionPriorizationService.cs | 13 ++ .../Clients/QBittorrent/QBittorrent.cs | 2 +- src/NzbDrone.Core/Download/DownloadService.cs | 4 +- .../Definitions/MovieSearchCriteria.cs | 11 ++ .../Definitions/SearchCriteriaBase.cs | 2 + .../IndexerSearch/NzbSearchService.cs | 36 +++++ .../BitMeTv/BitMeTvRequestGenerator.cs | 8 +- .../BroadcastheNetRequestGenerator.cs | 6 + .../Indexers/Fanzub/FanzubRequestGenerator.cs | 8 +- .../Indexers/HDBits/HDBitsRequestGenerator.cs | 8 +- src/NzbDrone.Core/Indexers/HttpIndexerBase.cs | 12 ++ src/NzbDrone.Core/Indexers/IIndexer.cs | 1 + .../Indexers/IIndexerRequestGenerator.cs | 1 + .../IPTorrents/IPTorrentsRequestGenerator.cs | 8 +- src/NzbDrone.Core/Indexers/IndexerBase.cs | 1 + .../KickassTorrentsRequestGenerator.cs | 8 +- .../Newznab/NewznabRequestGenerator.cs | 8 +- .../Indexers/Nyaa/NyaaRequestGenerator.cs | 8 +- .../Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs | 8 +- .../Indexers/Rarbg/RarbgRequestGenerator.cs | 51 ++++++- .../Indexers/RssIndexerRequestGenerator.cs | 8 +- .../TorrentRssIndexerRequestGenerator.cs | 8 +- .../TorrentleechRequestGenerator.cs | 8 +- src/NzbDrone.Core/Indexers/Torznab/Torznab.cs | 3 + src/NzbDrone.Core/Indexers/Wombles/Wombles.cs | 6 +- .../MediaFiles/Commands/RescanMovieCommand.cs | 20 +++ .../MediaFiles/DiskScanService.cs | 93 +++++++++++ .../EpisodeImport/ImportDecisionMaker.cs | 110 +++++++++++++ .../Events/MovieScanSkippedEvent.cs | 24 +++ .../MediaFiles/Events/MovieScannedEvent.cs | 15 ++ .../MediaFiles/MediaFileService.cs | 16 ++ .../MediaFileTableCleanupService.cs | 61 ++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 11 ++ src/NzbDrone.Core/Parser/Model/RemoteMovie.cs | 20 +++ src/NzbDrone.Core/Parser/ParsingService.cs | 56 +++++++ .../Tv/Commands/RefreshMovieCommand.cs | 22 +++ .../Tv/Events/MovieRefreshStartingEvent.cs | 14 ++ src/NzbDrone.Core/Tv/MovieAddedHandler.cs | 22 +++ src/NzbDrone.Core/Tv/MovieEditedService.cs | 25 +++ src/NzbDrone.Core/Tv/RefreshMovieService.cs | 144 ++++++++++++++++++ src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs | 47 ++++++ src/UI/Episode/EpisodeDetailsLayout.js | 2 +- src/UI/Handlebars/Helpers/Series.js | 10 ++ src/UI/Movies/Details/InfoView.js | 8 +- src/UI/Movies/Details/InfoViewTemplate.hbs | 14 +- src/UI/Movies/Details/MoviesDetailsLayout.js | 58 +++++-- .../Movies/Details/MoviesDetailsTemplate.hbs | 11 +- .../Movies/History/MovieHistoryActionsCell.js | 35 +++++ .../Movies/History/MovieHistoryDetailsCell.js | 28 ++++ src/UI/Movies/History/MovieHistoryLayout.js | 83 ++++++++++ .../History/MovieHistoryLayoutTemplate.hbs | 1 + src/UI/Movies/History/NoHistoryView.js | 5 + .../Movies/History/NoHistoryViewTemplate.hbs | 3 + src/UI/Movies/Index/MoviesIndexItemView.js | 10 +- src/UI/Movies/Index/MoviesIndexLayout.js | 2 +- .../Posters/SeriesPostersItemViewTemplate.hbs | 8 +- src/UI/Movies/MoviesController.js | 6 +- src/UI/Movies/Search/ButtonsView.js | 5 + src/UI/Movies/Search/ButtonsViewTemplate.hbs | 4 + src/UI/Movies/Search/ManualLayout.js | 86 +++++++++++ src/UI/Movies/Search/ManualLayoutTemplate.hbs | 2 + src/UI/Movies/Search/MovieSearchLayout.js | 82 ++++++++++ .../Search/MovieSearchLayoutTemplate.hbs | 1 + src/UI/Movies/Search/NoResultsView.js | 5 + .../Movies/Search/NoResultsViewTemplate.hbs | 1 + src/UI/Navbar/Search.js | 6 +- src/UI/Release/ReleaseCollection.js | 7 +- src/UI/Series/SeriesController.js | 9 +- src/UI/Shared/Modal/ModalController.js | 12 +- src/UI/main.js | 4 +- src/UI/vent.js | 3 +- 76 files changed, 1509 insertions(+), 74 deletions(-) create mode 100644 src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs create mode 100644 src/NzbDrone.Core/Parser/Model/RemoteMovie.cs create mode 100644 src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs create mode 100644 src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs create mode 100644 src/NzbDrone.Core/Tv/MovieAddedHandler.cs create mode 100644 src/NzbDrone.Core/Tv/MovieEditedService.cs create mode 100644 src/NzbDrone.Core/Tv/RefreshMovieService.cs create mode 100644 src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs create mode 100644 src/UI/Movies/History/MovieHistoryActionsCell.js create mode 100644 src/UI/Movies/History/MovieHistoryDetailsCell.js create mode 100644 src/UI/Movies/History/MovieHistoryLayout.js create mode 100644 src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs create mode 100644 src/UI/Movies/History/NoHistoryView.js create mode 100644 src/UI/Movies/History/NoHistoryViewTemplate.hbs create mode 100644 src/UI/Movies/Search/ButtonsView.js create mode 100644 src/UI/Movies/Search/ButtonsViewTemplate.hbs create mode 100644 src/UI/Movies/Search/ManualLayout.js create mode 100644 src/UI/Movies/Search/ManualLayoutTemplate.hbs create mode 100644 src/UI/Movies/Search/MovieSearchLayout.js create mode 100644 src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs create mode 100644 src/UI/Movies/Search/NoResultsView.js create mode 100644 src/UI/Movies/Search/NoResultsViewTemplate.hbs diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 5729af932..79588c582 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -82,6 +82,11 @@ namespace NzbDrone.Api.Indexers return GetEpisodeReleases(Request.Query.episodeId); } + if (Request.Query.movieId != null) + { + return GetMovieReleases(Request.Query.movieId); + } + return GetRss(); } @@ -102,6 +107,27 @@ namespace NzbDrone.Api.Indexers return new List(); } + private List GetMovieReleases(int movieId) + { + try + { + var decisions = _nzbSearchService.MovieSearch(movieId, true); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisionsForMovies(decisions); + + return MapDecisions(prioritizedDecisions); + } + catch (NotImplementedException ex) + { + _logger.Error(ex, "One or more indexer you selected does not support movie search yet: " + ex.Message); + } + catch (Exception ex) + { + _logger.Error(ex, "Movie search failed: " + ex.Message); + } + + return new List(); + } + private List GetRss() { var reports = _rssFetcherAndParser.Fetch(); diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index b951b0fe0..4a8e7e041 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -86,6 +86,11 @@ namespace NzbDrone.Api.Indexers var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; var remoteEpisode = model.RemoteEpisode; var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); + var downloadAllowed = model.RemoteEpisode.DownloadAllowed; + if (model.IsForMovie) + { + downloadAllowed = model.RemoteMovie.DownloadAllowed; + } // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) return new ReleaseResource @@ -119,7 +124,7 @@ namespace NzbDrone.Api.Indexers CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, InfoUrl = releaseInfo.InfoUrl, - DownloadAllowed = remoteEpisode.DownloadAllowed, + DownloadAllowed = downloadAllowed, //ReleaseWeight MagnetUrl = torrentInfo.MagnetUrl, diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 76d22d669..356ad1e7e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_return_an_empty_list_when_none_are_appproved() { var decisions = new List(); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); + RemoteEpisode ep = null; + decisions.Add(new DownloadDecision(ep, new Rejection("Failure!"))); + decisions.Add(new DownloadDecision(ep, new Rejection("Failure!"))); Subject.GetQualifiedReports(decisions).Should().BeEmpty(); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs index cad8177cb..68c2efee0 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs @@ -7,6 +7,10 @@ namespace NzbDrone.Core.DecisionEngine public class DownloadDecision { public RemoteEpisode RemoteEpisode { get; private set; } + + public RemoteMovie RemoteMovie { get; private set; } + + public bool IsForMovie = false; public IEnumerable Rejections { get; private set; } public bool Approved => !Rejections.Any(); @@ -30,6 +34,23 @@ namespace NzbDrone.Core.DecisionEngine public DownloadDecision(RemoteEpisode episode, params Rejection[] rejections) { RemoteEpisode = episode; + RemoteMovie = new RemoteMovie + { + Release = episode.Release, + ParsedEpisodeInfo = episode.ParsedEpisodeInfo + }; + Rejections = rejections.ToList(); + } + + public DownloadDecision(RemoteMovie movie, params Rejection[] rejections) + { + RemoteMovie = movie; + RemoteEpisode = new RemoteEpisode + { + Release = movie.Release, + ParsedEpisodeInfo = movie.ParsedEpisodeInfo + }; + IsForMovie = true; Rejections = rejections.ToList(); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index c87a3bc16..af071b15d 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -37,9 +37,83 @@ namespace NzbDrone.Core.DecisionEngine public List GetSearchDecision(List reports, SearchCriteriaBase searchCriteriaBase) { + if (searchCriteriaBase.Movie != null) + { + return GetMovieDecisions(reports, searchCriteriaBase).ToList(); + } + return GetDecisions(reports, searchCriteriaBase).ToList(); } + private IEnumerable GetMovieDecisions(List reports, SearchCriteriaBase searchCriteria = null) + { + if (reports.Any()) + { + _logger.ProgressInfo("Processing {0} releases", reports.Count); + } + + else + { + _logger.ProgressInfo("No results found"); + } + + var reportNumber = 1; + + foreach (var report in reports) + { + DownloadDecision decision = null; + _logger.ProgressTrace("Processing release {0}/{1}", reportNumber, reports.Count); + + try + { + var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title); + + if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace()) + { + RemoteMovie remoteEpisode = _parsingService.Map(parsedEpisodeInfo, "", searchCriteria); + remoteEpisode.Release = report; + + if (remoteEpisode.Movie == null) + { + //remoteEpisode.DownloadAllowed = true; //Fuck you :) + //decision = GetDecisionForReport(remoteEpisode, searchCriteria); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Movie not Found.")); + } + else + { + remoteEpisode.DownloadAllowed = true; + //decision = GetDecisionForReport(remoteEpisode, searchCriteria); TODO: Rewrite this for movies! + decision = new DownloadDecision(remoteEpisode); + } + } + } + catch (Exception e) + { + _logger.Error(e, "Couldn't process release."); + + var remoteEpisode = new RemoteEpisode { Release = report }; + decision = new DownloadDecision(remoteEpisode, new Rejection("Unexpected error processing release")); + } + + reportNumber++; + + if (decision != null) + { + if (decision.Rejections.Any()) + { + _logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); + } + + else + { + _logger.Debug("Release accepted"); + } + + yield return decision; + } + } + } + private IEnumerable GetDecisions(List reports, SearchCriteriaBase searchCriteria = null) { if (reports.Any()) @@ -82,7 +156,7 @@ namespace NzbDrone.Core.DecisionEngine { //remoteEpisode.DownloadAllowed = true; //Fuck you :) //decision = GetDecisionForReport(remoteEpisode, searchCriteria); - decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Movie not Found.")); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Series not Found.")); } else if (remoteEpisode.Episodes.Empty()) { diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 33fc32f5d..9ba7b8ad2 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.DecisionEngine public interface IPrioritizeDownloadDecision { List PrioritizeDecisions(List decisions); + List PrioritizeDecisionsForMovies(List decisions); } public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision @@ -29,5 +30,17 @@ namespace NzbDrone.Core.DecisionEngine .Union(decisions.Where(c => c.RemoteEpisode.Series == null)) .ToList(); } + + public List PrioritizeDecisionsForMovies(List decisions) + { + return decisions.Where(c => c.RemoteMovie.Movie != null) + /*.GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) => + { + return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService)); + }) + .SelectMany(c => c)*/ + .Union(decisions.Where(c => c.RemoteMovie.Movie == null)) + .ToList(); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index ecd75c911..5840526d7 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); } - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + var isRecentEpisode = true;//remoteEpisode.IsRecentEpisode(); TODO: Update to use RemoteMovie! if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 4f76b1507..c5ad3f770 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -41,8 +41,8 @@ namespace NzbDrone.Core.Download public void DownloadReport(RemoteEpisode remoteEpisode) { - Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); - Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); + //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); + //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit var downloadTitle = remoteEpisode.Release.Title; var downloadClient = _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs new file mode 100644 index 000000000..12f9baf1d --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Core.IndexerSearch.Definitions +{ + public class MovieSearchCriteria : SearchCriteriaBase + { + + public override string ToString() + { + return string.Format("[{0}]", Movie.Title); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index c5e602e59..937b27880 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); public Series Series { get; set; } + + public Movie Movie { get; set; } public List SceneTitles { get; set; } public List Episodes { get; set; } public virtual bool MonitoredEpisodesOnly { get; set; } diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index cff3e290c..8c6b981c5 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -19,6 +19,8 @@ namespace NzbDrone.Core.IndexerSearch { List EpisodeSearch(int episodeId, bool userInvokedSearch); List EpisodeSearch(Episode episode, bool userInvokedSearch); + List MovieSearch(int movieId, bool userInvokedSearch); + List MovieSearch(Movie movie, bool userInvokedSearch); List SeasonSearch(int seriesId, int seasonNumber, bool missingOnly, bool userInvokedSearch); } @@ -29,6 +31,7 @@ namespace NzbDrone.Core.IndexerSearch private readonly ISeriesService _seriesService; private readonly IEpisodeService _episodeService; private readonly IMakeDownloadDecision _makeDownloadDecision; + private readonly IMovieService _movieService; private readonly Logger _logger; public NzbSearchService(IIndexerFactory indexerFactory, @@ -36,6 +39,7 @@ namespace NzbDrone.Core.IndexerSearch ISeriesService seriesService, IEpisodeService episodeService, IMakeDownloadDecision makeDownloadDecision, + IMovieService movieService, Logger logger) { _indexerFactory = indexerFactory; @@ -43,6 +47,7 @@ namespace NzbDrone.Core.IndexerSearch _seriesService = seriesService; _episodeService = episodeService; _makeDownloadDecision = makeDownloadDecision; + _movieService = movieService; _logger = logger; } @@ -53,6 +58,20 @@ namespace NzbDrone.Core.IndexerSearch return EpisodeSearch(episode, userInvokedSearch); } + public List MovieSearch(int movieId, bool userInvokedSearch) + { + var movie = _movieService.GetMovie(movieId); + + return MovieSearch(movie, userInvokedSearch); + } + + public List MovieSearch(Movie movie, bool userInvokedSearch) + { + var searchSpec = Get(movie, userInvokedSearch); + + return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + } + public List EpisodeSearch(Episode episode, bool userInvokedSearch) { var series = _seriesService.GetSeries(episode.SeriesId); @@ -245,6 +264,23 @@ namespace NzbDrone.Core.IndexerSearch return spec; } + private TSpec Get(Movie movie, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new() + { + var spec = new TSpec(); + + spec.Movie = movie; + /*spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId, + episodes.Select(e => e.SeasonNumber).Distinct().ToList(), + episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList()); + + spec.Episodes = episodes; + + spec.SceneTitles.Add(series.Title);*/ + spec.UserInvokedSearch = userInvokedSearch; + + return spec; + } + private List Dispatch(Func> searchAction, SearchCriteriaBase criteriaBase) { var indexers = _indexerFactory.SearchEnabled(); diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs index e7966dcba..0c631af39 100644 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers.BitMeTv return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs index b5a39a94c..579761d89 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; +using System; namespace NzbDrone.Core.Indexers.BroadcastheNet { @@ -189,5 +190,10 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet yield return new IndexerRequest(builder.Build()); } } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs index 19585dad5..ad207eb38 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -84,5 +85,10 @@ namespace NzbDrone.Core.Indexers.Fanzub { return RemoveCharactersRegex.Replace(title, ""); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index dacb87490..2b377a9fd 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -128,5 +129,10 @@ namespace NzbDrone.Core.Indexers.HDBits yield return new IndexerRequest(request); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 99ad741ca..c912291fa 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -111,6 +111,18 @@ namespace NzbDrone.Core.Indexers return FetchReleases(generator.GetSearchRequests(searchCriteria)); } + public override IList Fetch(MovieSearchCriteria searchCriteria) + { + if (!SupportsSearch) + { + return new List(); + } + + var generator = GetRequestGenerator(); + + return FetchReleases(generator.GetSearchRequests(searchCriteria)); + } + protected virtual IList FetchReleases(IndexerPageableRequestChain pageableRequestChain, bool isRecent = false) { var releases = new List(); diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 9f028b569..f83bc3162 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -17,5 +17,6 @@ namespace NzbDrone.Core.Indexers IList Fetch(DailyEpisodeSearchCriteria searchCriteria); IList Fetch(AnimeEpisodeSearchCriteria searchCriteria); IList Fetch(SpecialEpisodeSearchCriteria searchCriteria); + IList Fetch(MovieSearchCriteria searchCriteria); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs index 5ad2cc79e..f321dacd7 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs @@ -10,5 +10,6 @@ namespace NzbDrone.Core.Indexers IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria); IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria); IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria); + IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs index bf4d9e7b8..bd63b6f46 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers.IPTorrents return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 4e08e5aad..95fda4871 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -67,6 +67,7 @@ namespace NzbDrone.Core.Indexers public abstract IList Fetch(DailyEpisodeSearchCriteria searchCriteria); public abstract IList Fetch(AnimeEpisodeSearchCriteria searchCriteria); public abstract IList Fetch(SpecialEpisodeSearchCriteria searchCriteria); + public abstract IList Fetch(MovieSearchCriteria searchCriteria); protected virtual IList CleanupReleases(IEnumerable releases) { diff --git a/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs index 228b3e607..9caaa1685 100644 --- a/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -147,5 +148,10 @@ namespace NzbDrone.Core.Indexers.KickassTorrents { return query.Replace('+', ' '); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 915603c15..59fec15cc 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -273,5 +274,10 @@ namespace NzbDrone.Core.Indexers.Newznab { return title.Replace("+", "%20"); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs index b54f4576f..6eac44084 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -102,5 +103,10 @@ namespace NzbDrone.Core.Indexers.Nyaa { return query.Replace(' ', '+'); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs index 17663e8bf..983b15a32 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -101,5 +102,10 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs yield return new IndexerRequest(url.ToString(), HttpAccept.Rss); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs index 798d8ec4e..b3cb1d9d8 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -88,13 +89,12 @@ namespace NzbDrone.Core.Indexers.Rarbg if (tvdbId.HasValue) { - string imdbId = string.Format("tt{0:D7}", tvdbId); - requestBuilder.AddQueryParam("search_imdb", imdbId); + requestBuilder.AddQueryParam("search_tvdb", tvdbId.Value); } if (query.IsNotNullOrWhiteSpace()) { - //requestBuilder.AddQueryParam("search_string", string.Format(query, args)); + requestBuilder.AddQueryParam("search_string", string.Format(query, args)); } if (!Settings.RankedOnly) @@ -102,6 +102,36 @@ namespace NzbDrone.Core.Indexers.Rarbg requestBuilder.AddQueryParam("ranked", "0"); } + requestBuilder.AddQueryParam("category", "tv"); + requestBuilder.AddQueryParam("limit", "100"); + requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); + requestBuilder.AddQueryParam("format", "json_extended"); + requestBuilder.AddQueryParam("app_id", "Sonarr"); + + yield return new IndexerRequest(requestBuilder.Build()); + } + + private IEnumerable GetMovieRequest(MovieSearchCriteria searchCriteria) + { + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl) + .Resource("/pubapi_v2.php") + .Accept(HttpAccept.Json); + + if (Settings.CaptchaToken.IsNotNullOrWhiteSpace()) + { + requestBuilder.UseSimplifiedUserAgent = true; + requestBuilder.SetCookie("cf_clearance", Settings.CaptchaToken); + } + + requestBuilder.AddQueryParam("mode", "search"); + + requestBuilder.AddQueryParam("search_imdb", searchCriteria.Movie.ImdbId); + + if (!Settings.RankedOnly) + { + requestBuilder.AddQueryParam("ranked", "0"); + } + requestBuilder.AddQueryParam("category", "movies"); requestBuilder.AddQueryParam("limit", "100"); requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); @@ -110,5 +140,18 @@ namespace NzbDrone.Core.Indexers.Rarbg yield return new IndexerRequest(requestBuilder.Build()); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + + + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetMovieRequest(searchCriteria)); + + return pageableRequests; + + + } } } diff --git a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs index 2ae5d4ed4..f9de0d54c 100644 --- a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common.Http; +using System; +using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; namespace NzbDrone.Core.Indexers @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + throw new NotImplementedException(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs index a0bf58cbc..1a77709cd 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -23,6 +24,11 @@ namespace NzbDrone.Core.Indexers.TorrentRss return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs index ebfa73788..51b458c6d 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers.Torrentleech return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 8d2649c2d..602edc540 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -7,7 +7,9 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Newznab; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Torznab @@ -110,5 +112,6 @@ namespace NzbDrone.Core.Indexers.Torznab return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details"); } } + } } diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index 571a85288..7aa1eef9f 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -1,7 +1,11 @@ -using NLog; +using System; +using System.Collections.Generic; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Wombles diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs new file mode 100644 index 000000000..3671aa6af --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RescanMovieCommand : Command + { + public int? MovieId { get; set; } + + public override bool SendUpdatesToClient => true; + + public RescanMovieCommand() + { + } + + public RescanMovieCommand(int movieId) + { + MovieId = movieId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 84a75e8e6..916c5681b 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles public interface IDiskScanService { void Scan(Series series); + void Scan(Movie movie); string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true); List FilterFiles(Series series, IEnumerable files); @@ -30,6 +31,8 @@ namespace NzbDrone.Core.MediaFiles public class DiskScanService : IDiskScanService, IHandle, + IHandle, + IExecute, IExecute { private readonly IDiskProvider _diskProvider; @@ -39,6 +42,7 @@ namespace NzbDrone.Core.MediaFiles private readonly ISeriesService _seriesService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; private readonly IEventAggregator _eventAggregator; + private readonly IMovieService _movieService; private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, @@ -48,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles ISeriesService seriesService, IMediaFileTableCleanupService mediaFileTableCleanupService, IEventAggregator eventAggregator, + IMovieService movieService, Logger logger) { _diskProvider = diskProvider; @@ -57,6 +62,7 @@ namespace NzbDrone.Core.MediaFiles _seriesService = seriesService; _mediaFileTableCleanupService = mediaFileTableCleanupService; _eventAggregator = eventAggregator; + _movieService = movieService; _logger = logger; } @@ -121,6 +127,64 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); } + public void Scan(Movie movie) + { + var rootFolder = _diskProvider.GetParentFolder(movie.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderDoesNotExist)); + return; + } + + if (_diskProvider.GetDirectories(rootFolder).Empty()) + { + _logger.Warn("Series' root folder ({0}) is empty.", rootFolder); + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderIsEmpty)); + return; + } + + _logger.ProgressInfo("Scanning disk for {0}", movie.Title); + + if (!_diskProvider.FolderExists(movie.Path)) + { + if (_configService.CreateEmptySeriesFolders && + _diskProvider.FolderExists(rootFolder)) + { + _logger.Debug("Creating missing series folder: {0}", movie.Path); + _diskProvider.CreateFolder(movie.Path); + SetPermissions(movie.Path); + } + else + { + _logger.Debug("Series folder doesn't exist: {0}", movie.Path); + } + + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.MovieFolderDoesNotExist)); + return; + } + + var videoFilesStopwatch = Stopwatch.StartNew(); + var mediaFileList = FilterFiles(movie, GetVideoFiles(movie.Path)).ToList(); + + videoFilesStopwatch.Stop(); + _logger.Trace("Finished getting episode files for: {0} [{1}]", movie, videoFilesStopwatch.Elapsed); + + _logger.Debug("{0} Cleaning up media files in DB", movie); + _mediaFileTableCleanupService.Clean(movie, mediaFileList); + + var decisionsStopwatch = Stopwatch.StartNew(); + var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, movie); + decisionsStopwatch.Stop(); + _logger.Trace("Import decisions complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed); + + _importApprovedEpisodes.Import(decisions, false); + + _logger.Info("Completed scanning disk for {0}", movie.Title); + _eventAggregator.PublishEvent(new MovieScannedEvent(movie)); + } + public string[] GetVideoFiles(string path, bool allDirectories = true) { _logger.Debug("Scanning '{0}' for video files", path); @@ -156,6 +220,13 @@ namespace NzbDrone.Core.MediaFiles .ToList(); } + public List FilterFiles(Movie movie, IEnumerable files) + { + return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(movie.Path.GetRelativePath(file))) + .Where(file => !ExcludedFilesRegex.IsMatch(Path.GetFileName(file))) + .ToList(); + } + private void SetPermissions(string path) { if (!_configService.SetPermissionsLinux) @@ -182,6 +253,28 @@ namespace NzbDrone.Core.MediaFiles Scan(message.Series); } + public void Handle(MovieUpdatedEvent message) + { + Scan(message.Movie); + } + + public void Execute(RescanMovieCommand message) + { + if (message.MovieId.HasValue) + { + var series = _movieService.GetMovie(message.MovieId.Value); + } + else + { + var allMovies = _movieService.GetAllMovies(); + + foreach (var movie in allMovies) + { + Scan(movie); + } + } + } + public void Execute(RescanSeriesCommand message) { if (message.SeriesId.HasValue) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 8f03ca756..5594ebe97 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -18,6 +18,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IMakeImportDecision { List GetImportDecisions(List videoFiles, Series series); + List GetImportDecisions(List videoFiles, Movie movie); + List GetImportDecisions(List videoFiles, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource); //TODO: Needs changing to ParsedMovieInfo!! List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); } @@ -53,6 +55,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return GetImportDecisions(videoFiles, series, null, false); } + public List GetImportDecisions(List videoFiles, Movie movie) + { + return GetImportDecisions(videoFiles, movie, null, false); + } + public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) { var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); @@ -70,6 +77,75 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return decisions; } + public List GetImportDecisions(List videoFiles, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource) + { + var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), movie); + + _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); + + var shouldUseFolderName = ShouldUseFolderName(videoFiles, movie, folderInfo); + var decisions = new List(); + + foreach (var file in newFiles) + { + decisions.AddIfNotNull(GetDecision(file, movie, folderInfo, sceneSource, shouldUseFolderName)); + } + + return decisions; + } + + private ImportDecision GetDecision(string file, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) + { + ImportDecision decision = null; + + /*try + { + var localEpisode = _parsingService.GetLocalEpisode(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); + + if (localEpisode != null) + { + localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, movie); + localEpisode.Size = _diskProvider.GetFileSize(file); + + _logger.Debug("Size: {0}", localEpisode.Size); + + //TODO: make it so media info doesn't ruin the import process of a new series + if (sceneSource) + { + localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + } + + if (localEpisode.Episodes.Empty()) + { + decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + } + else + { + decision = GetDecision(localEpisode); + } + } + + else + { + localEpisode = new LocalEpisode(); + localEpisode.Path = file; + + decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); + } + } + catch (Exception e) + { + _logger.Error(e, "Couldn't import file. " + file); + + var localEpisode = new LocalEpisode { Path = file }; + decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); + }*/ + + decision = new ImportDecision(null, new Rejection("IMPLEMENTATION MISSING!!!")); + + return decision; + } + private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) { ImportDecision decision = null; @@ -182,6 +258,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport }) == 1; } + private bool ShouldUseFolderName(List videoFiles, Movie movie, ParsedEpisodeInfo folderInfo) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.FullSeason) + { + return false; + } + + return videoFiles.Count(file => + { + var size = _diskProvider.GetFileSize(file); + var fileQuality = QualityParser.ParseQuality(file); + //var sample = null;//_detectSample.IsSample(movie, GetQuality(folderInfo, fileQuality, movie), file, size, folderInfo.IsPossibleSpecialEpisode); //Todo to this + + return true; + + //if (sample) + { + return false; + } + + if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) + { + return false; + } + + return true; + }) == 1; + } + private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) { if (UseFolderQuality(folderInfo, fileQuality, series)) diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs new file mode 100644 index 000000000..ecca0436d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs @@ -0,0 +1,24 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieScanSkippedEvent : IEvent + { + public Movie Movie { get; private set; } + public MovieScanSkippedReason Reason { get; set; } + + public MovieScanSkippedEvent(Movie movie, MovieScanSkippedReason reason) + { + Movie = movie; + Reason = reason; + } + } + + public enum MovieScanSkippedReason + { + RootFolderDoesNotExist, + RootFolderIsEmpty, + MovieFolderDoesNotExist + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs new file mode 100644 index 000000000..a0299d408 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieScannedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieScannedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index ca3f68ce2..051e85863 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -16,9 +16,11 @@ namespace NzbDrone.Core.MediaFiles void Update(EpisodeFile episodeFile); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); List GetFilesBySeries(int seriesId); + List GetFilesByMovie(int movieId); List GetFilesBySeason(int seriesId, int seasonNumber); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Series series); + List FilterExistingFiles(List files, Movie movie); EpisodeFile Get(int id); List Get(IEnumerable ids); @@ -64,6 +66,11 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesBySeries(seriesId); } + public List GetFilesByMovie(int movieId) + { + return _mediaFileRepository.GetFilesBySeries(movieId); //TODO: Update implementation for movie files. + } + public List GetFilesBySeason(int seriesId, int seasonNumber) { return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); @@ -83,6 +90,15 @@ namespace NzbDrone.Core.MediaFiles return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); } + public List FilterExistingFiles(List files, Movie movie) + { + var seriesFiles = GetFilesBySeries(movie.Id).Select(f => Path.Combine(movie.Path, f.RelativePath)).ToList(); + + if (!seriesFiles.Any()) return files; + + return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); + } + public EpisodeFile Get(int id) { return _mediaFileRepository.Get(id); diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index b275fb03e..49eb11ca9 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -11,6 +11,8 @@ namespace NzbDrone.Core.MediaFiles public interface IMediaFileTableCleanupService { void Clean(Series series, List filesOnDisk); + + void Clean(Movie movie, List filesOnDisk); } public class MediaFileTableCleanupService : IMediaFileTableCleanupService @@ -84,5 +86,64 @@ namespace NzbDrone.Core.MediaFiles } } } + + public void Clean(Movie movie, List filesOnDisk) + { + + //TODO: Update implementation for movies. + var seriesFiles = _mediaFileService.GetFilesBySeries(movie.Id); + var episodes = _episodeService.GetEpisodeBySeries(movie.Id); + + var filesOnDiskKeys = new HashSet(filesOnDisk, PathEqualityComparer.Instance); + + foreach (var seriesFile in seriesFiles) + { + var episodeFile = seriesFile; + var episodeFilePath = Path.Combine(movie.Path, episodeFile.RelativePath); + + try + { + if (!filesOnDiskKeys.Contains(episodeFilePath)) + { + _logger.Debug("File [{0}] no longer exists on disk, removing from db", episodeFilePath); + _mediaFileService.Delete(seriesFile, DeleteMediaFileReason.MissingFromDisk); + continue; + } + + if (episodes.None(e => e.EpisodeFileId == episodeFile.Id)) + { + _logger.Debug("File [{0}] is not assigned to any episodes, removing from db", episodeFilePath); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.NoLinkedEpisodes); + continue; + } + + // var localEpsiode = _parsingService.GetLocalEpisode(episodeFile.Path, series); + // + // if (localEpsiode == null || episodes.Count != localEpsiode.Episodes.Count) + // { + // _logger.Debug("File [{0}] parsed episodes has changed, removing from db", episodeFile.Path); + // _mediaFileService.Delete(episodeFile); + // continue; + // } + } + + catch (Exception ex) + { + var errorMessage = string.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id); + _logger.Error(ex, errorMessage); + } + } + + foreach (var e in episodes) + { + var episode = e; + + if (episode.EpisodeFileId > 0 && seriesFiles.None(f => f.Id == episode.EpisodeFileId)) + { + episode.EpisodeFileId = 0; + _episodeService.UpdateEpisode(episode); + } + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 33fb52fd4..01c680406 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -562,6 +562,7 @@ + @@ -695,6 +696,7 @@ + @@ -737,6 +739,8 @@ + + @@ -868,6 +872,7 @@ + @@ -1053,6 +1058,7 @@ + @@ -1070,6 +1076,7 @@ + @@ -1077,12 +1084,15 @@ + + + @@ -1095,6 +1105,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs new file mode 100644 index 000000000..d8ba07803 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Parser.Model +{ + public class RemoteMovie + { + public ReleaseInfo Release { get; set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do. + public Movie Movie { get; set; } + public bool DownloadAllowed { get; set; } + + public override string ToString() + { + return Release.Title; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 777c5d0ed..937c9cf94 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Parser Series GetSeries(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds); + RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); } @@ -27,16 +28,19 @@ namespace NzbDrone.Core.Parser private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; private readonly ISceneMappingService _sceneMappingService; + private readonly IMovieService _movieService; private readonly Logger _logger; public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, ISceneMappingService sceneMappingService, + IMovieService movieService, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; _sceneMappingService = sceneMappingService; + _movieService = movieService; _logger = logger; } @@ -134,6 +138,25 @@ namespace NzbDrone.Core.Parser return remoteEpisode; } + public RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null) + { + var remoteEpisode = new RemoteMovie + { + ParsedEpisodeInfo = parsedEpisodeInfo, + }; + + var movie = GetMovie(parsedEpisodeInfo, imdbId, searchCriteria); + + if (movie == null) + { + return remoteEpisode; + } + + remoteEpisode.Movie = movie; + + return remoteEpisode; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds) { return new RemoteEpisode @@ -248,6 +271,39 @@ namespace NzbDrone.Core.Parser return null; } + private Movie GetMovie(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + if (searchCriteria.Movie.CleanTitle == parsedEpisodeInfo.SeriesTitle.CleanSeriesTitle()) + { + return searchCriteria.Movie; + } + + if (imdbId.IsNotNullOrWhiteSpace() && imdbId == searchCriteria.Movie.ImdbId) + { + //TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import + return searchCriteria.Movie; + } + } + + Movie movie = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + + if (movie == null && imdbId.IsNotNullOrWhiteSpace()) + { + //TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import + movie = _movieService.FindByImdbId(imdbId); + } + + if (movie == null) + { + _logger.Debug("No matching movie {0}", parsedEpisodeInfo.SeriesTitle); + return null; + } + + return movie; + } + private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria) { Series series = null; diff --git a/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs b/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs new file mode 100644 index 000000000..08c7b2e72 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Tv.Commands +{ + public class RefreshMovieCommand : Command + { + public int? MovieId { get; set; } + + public RefreshMovieCommand() + { + } + + public RefreshMovieCommand(int? movieId) + { + MovieId = movieId; + } + + public override bool SendUpdatesToClient => true; + + public override bool UpdateScheduledTask => !MovieId.HasValue; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs new file mode 100644 index 000000000..201b5f9bb --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieRefreshStartingEvent : IEvent + { + public bool ManualTrigger { get; set; } + + public MovieRefreshStartingEvent(bool manualTrigger) + { + ManualTrigger = manualTrigger; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieAddedHandler.cs b/src/NzbDrone.Core/Tv/MovieAddedHandler.cs new file mode 100644 index 000000000..765446ee1 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieAddedHandler.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieAddedHandler : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public MovieAddedHandler(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(MovieAddedEvent message) + { + _commandQueueManager.Push(new RefreshMovieCommand(message.Movie.Id)); + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieEditedService.cs b/src/NzbDrone.Core/Tv/MovieEditedService.cs new file mode 100644 index 000000000..5f011c5ba --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieEditedService.cs @@ -0,0 +1,25 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieEditedService : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public MovieEditedService(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(MovieEditedEvent message) + { + if (message.Movie.ImdbId != message.OldMovie.ImdbId) + { + _commandQueueManager.Push(new RefreshMovieCommand(message.Movie.Id)); //Probably not needed, as metadata should stay the same. + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/RefreshMovieService.cs b/src/NzbDrone.Core/Tv/RefreshMovieService.cs new file mode 100644 index 000000000..7b01bcf87 --- /dev/null +++ b/src/NzbDrone.Core/Tv/RefreshMovieService.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +//using NzbDrone.Core.DataAugmentation.DailyMovie; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class RefreshMovieService : IExecute + { + private readonly IProvideMovieInfo _movieInfo; + private readonly IMovieService _movieService; + private readonly IRefreshEpisodeService _refreshEpisodeService; + private readonly IEventAggregator _eventAggregator; + private readonly IDiskScanService _diskScanService; + private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed; + private readonly Logger _logger; + + public RefreshMovieService(IProvideMovieInfo movieInfo, + IMovieService movieService, + IRefreshEpisodeService refreshEpisodeService, + IEventAggregator eventAggregator, + IDiskScanService diskScanService, + ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed, + Logger logger) + { + _movieInfo = movieInfo; + _movieService = movieService; + _refreshEpisodeService = refreshEpisodeService; + _eventAggregator = eventAggregator; + _diskScanService = diskScanService; + _checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed; + _logger = logger; + } + + private void RefreshMovieInfo(Movie movie) + { + _logger.ProgressInfo("Updating Info for {0}", movie.Title); + + Movie movieInfo; + + try + { + movieInfo = _movieInfo.GetMovieInfo(movie.ImdbId); + } + catch (MovieNotFoundException) + { + _logger.Error("Movie '{0}' (imdbid {1}) was not found, it may have been removed from TheTVDB.", movie.Title, movie.ImdbId); + return; + } + + if (movie.ImdbId != movieInfo.ImdbId) + { + _logger.Warn("Movie '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", movie.Title, movie.ImdbId, movieInfo.Title, movieInfo.ImdbId); + movie.ImdbId = movieInfo.ImdbId; + } + + movie.Title = movieInfo.Title; + movie.TitleSlug = movieInfo.TitleSlug; + movie.ImdbId = movieInfo.ImdbId; + movie.Overview = movieInfo.Overview; + movie.Status = movieInfo.Status; + movie.CleanTitle = movieInfo.CleanTitle; + movie.SortTitle = movieInfo.SortTitle; + movie.LastInfoSync = DateTime.UtcNow; + movie.Runtime = movieInfo.Runtime; + movie.Images = movieInfo.Images; + movie.Ratings = movieInfo.Ratings; + movie.Actors = movieInfo.Actors; + movie.Genres = movieInfo.Genres; + movie.Certification = movieInfo.Certification; + movie.InCinemas = movieInfo.InCinemas; + movie.Year = movieInfo.Year; + + try + { + movie.Path = new DirectoryInfo(movie.Path).FullName; + movie.Path = movie.Path.GetActualCasing(); + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't update movie path for " + movie.Path); + } + + _movieService.UpdateMovie(movie); + + _logger.Debug("Finished movie refresh for {0}", movie.Title); + _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); + } + + public void Execute(RefreshMovieCommand message) + { + _eventAggregator.PublishEvent(new MovieRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); + + if (message.MovieId.HasValue) + { + var movie = _movieService.GetMovie(message.MovieId.Value); + RefreshMovieInfo(movie); + } + else + { + var allMovie = _movieService.GetAllMovies().OrderBy(c => c.SortTitle).ToList(); + + foreach (var movie in allMovie) + { + if (message.Trigger == CommandTrigger.Manual || _checkIfMovieShouldBeRefreshed.ShouldRefresh(movie)) + { + try + { + RefreshMovieInfo(movie); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't refresh info for {0}".Inject(movie)); + } + } + + else + { + try + { + _logger.Info("Skipping refresh of movie: {0}", movie.Title); + _diskScanService.Scan(movie); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't rescan movie {0}".Inject(movie)); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs new file mode 100644 index 000000000..4b5d650eb --- /dev/null +++ b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using NLog; + +namespace NzbDrone.Core.Tv +{ + public interface ICheckIfMovieShouldBeRefreshed + { + bool ShouldRefresh(Movie movie); + } + + public class ShouldRefreshMovie : ICheckIfMovieShouldBeRefreshed + { + private readonly IEpisodeService _episodeService; + private readonly Logger _logger; + + public ShouldRefreshMovie(IEpisodeService episodeService, Logger logger) + { + _episodeService = episodeService; + _logger = logger; + } + + public bool ShouldRefresh(Movie movie) + { + if (movie.LastInfoSync < DateTime.UtcNow.AddDays(-30)) + { + _logger.Trace("Movie {0} last updated more than 30 days ago, should refresh.", movie.Title); + return true; + } + + if (movie.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) + { + _logger.Trace("Movie {0} last updated less than 6 hours ago, should not be refreshed.", movie.Title); + return false; + } + + if (movie.Status != MovieStatusType.TBA) + { + _logger.Trace("Movie {0} is announced or released, should refresh.", movie.Title); //We probably have to change this. + return true; + } + + _logger.Trace("Movie {0} ended long ago, should not be refreshed.", movie.Title); + return false; + } + } +} diff --git a/src/UI/Episode/EpisodeDetailsLayout.js b/src/UI/Episode/EpisodeDetailsLayout.js index ba1631d0e..8bdf13525 100644 --- a/src/UI/Episode/EpisodeDetailsLayout.js +++ b/src/UI/Episode/EpisodeDetailsLayout.js @@ -127,4 +127,4 @@ module.exports = Marionette.Layout.extend({ this.ui.monitored.removeClass('icon-sonarr-monitored'); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index a95b2b10e..fc96495a0 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -31,6 +31,16 @@ Handlebars.registerHelper('tvdbUrl', function() { return 'http://imdb.com/title/tt' + this.imdbId; }); +Handlebars.registerHelper('inCinemas', function() { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +]; + var cinemasDate = new Date(this.inCinemas); + var year = cinemasDate.getFullYear(); + var month = monthNames[cinemasDate.getMonth()]; + return "In Cinemas " + month + " " + year; +}) + Handlebars.registerHelper('tvRageUrl', function() { return 'http://www.tvrage.com/shows/id-' + this.tvRageId; }); diff --git a/src/UI/Movies/Details/InfoView.js b/src/UI/Movies/Details/InfoView.js index 2ac803988..4a2f71b29 100644 --- a/src/UI/Movies/Details/InfoView.js +++ b/src/UI/Movies/Details/InfoView.js @@ -4,15 +4,15 @@ module.exports = Marionette.ItemView.extend({ template : 'Movies/Details/InfoViewTemplate', initialize : function(options) { - this.episodeFileCollection = options.episodeFileCollection; + //this.episodeFileCollection = options.episodeFileCollection; this.listenTo(this.model, 'change', this.render); - this.listenTo(this.episodeFileCollection, 'sync', this.render); + //this.listenTo(this.episodeFileCollection, 'sync', this.render); TODO: Update this; }, templateHelpers : function() { return { - fileCount : this.episodeFileCollection.length + fileCount : 0 }; } -}); \ No newline at end of file +}); diff --git a/src/UI/Movies/Details/InfoViewTemplate.hbs b/src/UI/Movies/Details/InfoViewTemplate.hbs index 666003f77..355db6750 100644 --- a/src/UI/Movies/Details/InfoViewTemplate.hbs +++ b/src/UI/Movies/Details/InfoViewTemplate.hbs @@ -21,10 +21,10 @@ {{fileCount}} files {{/if_eq}} - {{#if_eq status compare="continuing"}} - Continuing + {{#if_eq status compare="released"}} + {{inCinemas}} {{else}} - Ended + Announced {{/if_eq}}
@@ -36,14 +36,6 @@ {{#if imdbId}} IMDB {{/if}} - - {{#if tvRageId}} - TV Rage - {{/if}} - - {{#if tvMazeId}} - TV Maze - {{/if}}
diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js index eb8a74e28..6dd34e360 100644 --- a/src/UI/Movies/Details/MoviesDetailsLayout.js +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -9,6 +9,8 @@ var InfoView = require('./InfoView'); var CommandController = require('../../Commands/CommandController'); var LoadingView = require('../../Shared/LoadingView'); var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); +var HistoryLayout = require('../History/MovieHistoryLayout'); +var SearchLayout = require('../Search/MovieSearchLayout'); require('backstrech'); require('../../Mixins/backbone.signalr.mixin'); @@ -18,9 +20,12 @@ module.exports = Marionette.Layout.extend({ regions : { seasons : '#seasons', - info : '#info' + info : '#info', + search : '#movie-search', + history : '#movie-history' }, + ui : { header : '.x-header', monitored : '.x-monitored', @@ -29,7 +34,9 @@ module.exports = Marionette.Layout.extend({ rename : '.x-rename', search : '.x-search', poster : '.x-movie-poster', - manualSearch : '.x-manual-search' + manualSearch : '.x-manual-search', + history : '.x-movie-history', + search : '.x-movie-search' }, events : { @@ -39,7 +46,9 @@ module.exports = Marionette.Layout.extend({ 'click .x-refresh' : '_refreshMovies', 'click .x-rename' : '_renameMovies', 'click .x-search' : '_moviesSearch', - 'click .x-manual-search' : '_manualSearchM' + 'click .x-manual-search' : '_showSearch', + 'click .x-movie-history' : '_showHistory', + 'click .x-movie-search' : '_showSearch' }, initialize : function() { @@ -60,17 +69,21 @@ module.exports = Marionette.Layout.extend({ }, onShow : function() { + this.searchLayout = new SearchLayout({ model : this.model }); + this.searchLayout.startManualSearch = true; + this._showBackdrop(); this._showSeasons(); this._setMonitoredState(); this._showInfo(); + this._showHistory(); }, onRender : function() { CommandController.bindToCommand({ element : this.ui.refresh, command : { - name : 'refreshMovies' + name : 'refreshMovie' } }); CommandController.bindToCommand({ @@ -110,6 +123,26 @@ module.exports = Marionette.Layout.extend({ return undefined; }, + _showHistory : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.history.tab('show'); + this.history.show(new HistoryLayout({ + model : this.model + })); + }, + + _showSearch : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.search.tab('show'); + this.search.show(this.searchLayout); + }, + _toggleMonitored : function() { var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); @@ -138,8 +171,8 @@ module.exports = Marionette.Layout.extend({ }, _refreshMovies : function() { - CommandController.Execute('refreshMovies', { - name : 'refreshMovies', + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', movieId : this.model.id }); }, @@ -198,8 +231,7 @@ module.exports = Marionette.Layout.extend({ _showInfo : function() { this.info.show(new InfoView({ - model : this.model, - episodeFileCollection : this.episodeFileCollection + model : this.model })); }, @@ -212,9 +244,9 @@ module.exports = Marionette.Layout.extend({ }, _refresh : function() { - this.seasonCollection.add(this.model.get('seasons'), { merge : true }); - this.episodeCollection.fetch(); - this.episodeFileCollection.fetch(); + //this.seasonCollection.add(this.model.get('seasons'), { merge : true }); + //this.episodeCollection.fetch(); + //this.episodeFileCollection.fetch(); this._setMonitoredState(); this._showInfo(); @@ -252,11 +284,11 @@ module.exports = Marionette.Layout.extend({ _manualSearchM : function() { console.warn("Manual Search started"); - console.warn(this.model.get("moviesId")); + console.warn(this.model.id); console.warn(this.model) console.warn(this.episodeCollection); vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.episodeCollection.models[0], + episode : this.model, hideMoviesLink : true, openingTab : 'search' }); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs index 8bacf94b0..cce213b01 100644 --- a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -35,4 +35,13 @@
-
+
+ +
+
+ +
diff --git a/src/UI/Movies/History/MovieHistoryActionsCell.js b/src/UI/Movies/History/MovieHistoryActionsCell.js new file mode 100644 index 000000000..c8c352aab --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryActionsCell.js @@ -0,0 +1,35 @@ +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-actions-cell', + + events : { + 'click .x-failed' : '_markAsFailed' + }, + + render : function() { + this.$el.empty(); + + if (this.model.get('eventType') === 'grabbed') { + this.$el.html(''); + } + + return this; + }, + + _markAsFailed : function() { + var url = window.NzbDrone.ApiRoot + '/history/failed'; + var data = { + id : this.model.get('id') + }; + + $.ajax({ + url : url, + type : 'POST', + data : data + }); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/History/MovieHistoryDetailsCell.js b/src/UI/Movies/History/MovieHistoryDetailsCell.js new file mode 100644 index 000000000..366a25040 --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryDetailsCell.js @@ -0,0 +1,28 @@ +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); +require('bootstrap'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-history-details-cell', + + render : function() { + this.$el.empty(); + this.$el.html(''); + + var html = new HistoryDetailsView({ model : this.model }).render().$el; + + this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : 'Details', + placement : 'left', + container : this.$el + }); + + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/History/MovieHistoryLayout.js b/src/UI/Movies/History/MovieHistoryLayout.js new file mode 100644 index 000000000..cb8b4f54b --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryLayout.js @@ -0,0 +1,83 @@ +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var HistoryCollection = require('../../Activity/History/HistoryCollection'); +var EventTypeCell = require('../../Cells/EventTypeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var EpisodeHistoryActionsCell = require('./MovieHistoryActionsCell'); +var EpisodeHistoryDetailsCell = require('./MovieHistoryDetailsCell'); +var NoHistoryView = require('./NoHistoryView'); +var LoadingView = require('../../Shared/LoadingView'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/History/MovieHistoryLayoutTemplate', + + regions : { + historyTable : '.history-table' + }, + + columns : [ + { + name : 'eventType', + label : '', + cell : EventTypeCell, + cellValue : 'this' + }, + { + name : 'sourceTitle', + label : 'Source Title', + cell : 'string' + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell + }, + { + name : 'date', + label : 'Date', + cell : RelativeDateCell + }, + { + name : 'this', + label : '', + cell : EpisodeHistoryDetailsCell, + sortable : false + }, + { + name : 'this', + label : '', + cell : EpisodeHistoryActionsCell, + sortable : false + } + ], + + initialize : function(options) { + this.model = options.model; + + this.collection = new HistoryCollection({ + episodeId : this.model.id, + tableName : 'episodeHistory' + }); + this.collection.fetch(); + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onRender : function() { + this.historyTable.show(new LoadingView()); + }, + + _showTable : function() { + if (this.collection.any()) { + this.historyTable.show(new Backgrid.Grid({ + collection : this.collection, + columns : this.columns, + className : 'table table-hover table-condensed' + })); + } + + else { + this.historyTable.show(new NoHistoryView()); + } + } +}); diff --git a/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs b/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs new file mode 100644 index 000000000..a9dfe8197 --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs @@ -0,0 +1 @@ +
diff --git a/src/UI/Movies/History/NoHistoryView.js b/src/UI/Movies/History/NoHistoryView.js new file mode 100644 index 000000000..554534a3b --- /dev/null +++ b/src/UI/Movies/History/NoHistoryView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/History/NoHistoryViewTemplate' +}); diff --git a/src/UI/Movies/History/NoHistoryViewTemplate.hbs b/src/UI/Movies/History/NoHistoryViewTemplate.hbs new file mode 100644 index 000000000..561e84d59 --- /dev/null +++ b/src/UI/Movies/History/NoHistoryViewTemplate.hbs @@ -0,0 +1,3 @@ +

+ No history for this episode. +

\ No newline at end of file diff --git a/src/UI/Movies/Index/MoviesIndexItemView.js b/src/UI/Movies/Index/MoviesIndexItemView.js index 427fe489e..e7b38fae7 100644 --- a/src/UI/Movies/Index/MoviesIndexItemView.js +++ b/src/UI/Movies/Index/MoviesIndexItemView.js @@ -16,7 +16,7 @@ module.exports = Marionette.ItemView.extend({ CommandController.bindToCommand({ element : this.ui.refresh, command : { - name : 'refreshSeries', + name : 'refreshMovie', seriesId : this.model.get('id') } }); @@ -27,9 +27,9 @@ module.exports = Marionette.ItemView.extend({ }, _refreshSeries : function() { - CommandController.Execute('refreshSeries', { - name : 'refreshSeries', - seriesId : this.model.id + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', + movieId : 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 index 128575333..68fc34bc5 100644 --- a/src/UI/Movies/Index/MoviesIndexLayout.js +++ b/src/UI/Movies/Index/MoviesIndexLayout.js @@ -103,7 +103,7 @@ module.exports = Marionette.Layout.extend({ { title : 'Update Library', icon : 'icon-sonarr-refresh', - command : 'refreshseries', + command : 'refreshmovie', successMessage : 'Library was updated!', errorMessage : 'Library update failed!' } diff --git a/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs index fba301c4f..9ba5c0dfb 100644 --- a/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs +++ b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs @@ -2,11 +2,11 @@
- - + +
- {{#unless_eq status compare="continuing"}} -
Ended
+ {{#unless_eq status compare="released"}} +
Released
{{/unless_eq}} {{poster}} diff --git a/src/UI/Movies/MoviesController.js b/src/UI/Movies/MoviesController.js index ba0f825eb..3d5ee48bb 100644 --- a/src/UI/Movies/MoviesController.js +++ b/src/UI/Movies/MoviesController.js @@ -3,6 +3,7 @@ var AppLayout = require('../AppLayout'); var MoviesCollection = require('./MoviesCollection'); var MoviesIndexLayout = require('./Index/MoviesIndexLayout'); var MoviesDetailsLayout = require('./Details/MoviesDetailsLayout'); +var SeriesDetailsLayout = require('../Series/Details/SeriesDetailsLayout'); module.exports = NzbDroneController.extend({ _originalInit : NzbDroneController.prototype.initialize, @@ -22,10 +23,13 @@ module.exports = NzbDroneController.extend({ seriesDetails : function(query) { var series = MoviesCollection.where({ titleSlug : query }); - if (series.length !== 0) { var targetMovie = series[0]; + console.log(AppLayout.mainRegion); + this.setTitle(targetMovie.get('title')); + //this.showNotFound(); + //this.showMainRegion(new SeriesDetailsLayout({model : targetMovie})); this.showMainRegion(new MoviesDetailsLayout({ model : targetMovie })); } else { this.showNotFound(); diff --git a/src/UI/Movies/Search/ButtonsView.js b/src/UI/Movies/Search/ButtonsView.js new file mode 100644 index 000000000..534e2f960 --- /dev/null +++ b/src/UI/Movies/Search/ButtonsView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Search/ButtonsViewTemplate' +}); diff --git a/src/UI/Movies/Search/ButtonsViewTemplate.hbs b/src/UI/Movies/Search/ButtonsViewTemplate.hbs new file mode 100644 index 000000000..6ad9474d5 --- /dev/null +++ b/src/UI/Movies/Search/ButtonsViewTemplate.hbs @@ -0,0 +1,4 @@ +
+ + +
\ No newline at end of file diff --git a/src/UI/Movies/Search/ManualLayout.js b/src/UI/Movies/Search/ManualLayout.js new file mode 100644 index 000000000..daae5d781 --- /dev/null +++ b/src/UI/Movies/Search/ManualLayout.js @@ -0,0 +1,86 @@ +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); +var FileSizeCell = require('../../Cells/FileSizeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); +var DownloadReportCell = require('../../Release/DownloadReportCell'); +var AgeCell = require('../../Release/AgeCell'); +var ProtocolCell = require('../../Release/ProtocolCell'); +var PeersCell = require('../../Release/PeersCell'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Search/ManualLayoutTemplate', + + regions : { + grid : '#episode-release-grid' + }, + + columns : [ + { + name : 'protocol', + label : 'Source', + cell : ProtocolCell + }, + { + name : 'age', + label : 'Age', + cell : AgeCell + }, + { + name : 'title', + label : 'Title', + cell : ReleaseTitleCell + }, + { + name : 'indexer', + label : 'Indexer', + cell : Backgrid.StringCell + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell + }, + { + name : 'seeders', + label : 'Peers', + cell : PeersCell + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell + }, + { + name : 'rejections', + label : '', + tooltip : 'Rejections', + cell : ApprovalStatusCell, + sortable : true, + sortType : 'fixed', + direction : 'ascending', + title : 'Release Rejected' + }, + { + name : 'download', + label : '', + tooltip : 'Auto-Search Prioritization', + cell : DownloadReportCell, + sortable : true, + sortType : 'fixed', + direction : 'ascending' + } + ], + + onShow : function() { + if (!this.isClosed) { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.collection, + className : 'table table-hover' + })); + } + } +}); diff --git a/src/UI/Movies/Search/ManualLayoutTemplate.hbs b/src/UI/Movies/Search/ManualLayoutTemplate.hbs new file mode 100644 index 000000000..1797eb289 --- /dev/null +++ b/src/UI/Movies/Search/ManualLayoutTemplate.hbs @@ -0,0 +1,2 @@ +
+ \ No newline at end of file diff --git a/src/UI/Movies/Search/MovieSearchLayout.js b/src/UI/Movies/Search/MovieSearchLayout.js new file mode 100644 index 000000000..aa8d994c3 --- /dev/null +++ b/src/UI/Movies/Search/MovieSearchLayout.js @@ -0,0 +1,82 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var ButtonsView = require('./ButtonsView'); +var ManualSearchLayout = require('./ManualLayout'); +var ReleaseCollection = require('../../Release/ReleaseCollection'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var NoResultsView = require('./NoResultsView'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Search/MovieSearchLayoutTemplate', + + regions : { + main : '#episode-search-region' + }, + + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, + + initialize : function() { + this.mainView = new ButtonsView(); + this.releaseCollection = new ReleaseCollection(); + + this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + }, + + onShow : function() { + if (this.startManualSearch) { + this._searchManual(); + } + + else { + this._showMainView(); + } + }, + + _searchAuto : function(e) { + if (e) { + e.preventDefault(); + } + + CommandController.Execute('episodeSearch', { + episodeIds : [this.model.get('id')] + }); + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _searchManual : function(e) { + if (e) { + e.preventDefault(); + } + + this.mainView = new LoadingView(); + this._showMainView(); + this.releaseCollection.fetchMovieReleases(this.model.id); + }, + + _showMainView : function() { + this.main.show(this.mainView); + }, + + _showButtons : function() { + this.mainView = new ButtonsView(); + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } +}); diff --git a/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs b/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs new file mode 100644 index 000000000..879e0b356 --- /dev/null +++ b/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/UI/Movies/Search/NoResultsView.js b/src/UI/Movies/Search/NoResultsView.js new file mode 100644 index 000000000..2b8bffd7c --- /dev/null +++ b/src/UI/Movies/Search/NoResultsView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Search/NoResultsViewTemplate' +}); diff --git a/src/UI/Movies/Search/NoResultsViewTemplate.hbs b/src/UI/Movies/Search/NoResultsViewTemplate.hbs new file mode 100644 index 000000000..7904e5520 --- /dev/null +++ b/src/UI/Movies/Search/NoResultsViewTemplate.hbs @@ -0,0 +1 @@ +
No results found
\ No newline at end of file diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js index ec1e14ead..dce7e8204 100644 --- a/src/UI/Navbar/Search.js +++ b/src/UI/Navbar/Search.js @@ -2,7 +2,7 @@ var _ = require('underscore'); var $ = require('jquery'); var vent = require('vent'); var Backbone = require('backbone'); -var SeriesCollection = require('../Series/SeriesCollection'); +var SeriesCollection = require('../Movies/MoviesCollection'); require('typeahead'); vent.on(vent.Hotkeys.NavbarSearch, function() { @@ -32,6 +32,6 @@ $.fn.bindSearch = function() { $(this).on('typeahead:selected typeahead:autocompleted', function(e, series) { this.blur(); $(this).val(''); - Backbone.history.navigate('/series/{0}'.format(series.titleSlug), { trigger : true }); + Backbone.history.navigate('/movies/{0}'.format(series.titleSlug), { trigger : true }); }); -}; \ No newline at end of file +}; diff --git a/src/UI/Release/ReleaseCollection.js b/src/UI/Release/ReleaseCollection.js index a66547f00..1e67296be 100644 --- a/src/UI/Release/ReleaseCollection.js +++ b/src/UI/Release/ReleaseCollection.js @@ -48,9 +48,14 @@ var Collection = PagableCollection.extend({ fetchEpisodeReleases : function(episodeId) { return this.fetch({ data : { episodeId : episodeId } }); + }, + + fetchMovieReleases : function(movieId) { + return this.fetch({ data : { movieId : movieId}}); } + }); Collection = AsSortedCollection.call(Collection); -module.exports = Collection; \ No newline at end of file +module.exports = Collection; diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js index 89ba13752..5638b5d85 100644 --- a/src/UI/Series/SeriesController.js +++ b/src/UI/Series/SeriesController.js @@ -8,10 +8,10 @@ module.exports = NzbDroneController.extend({ _originalInit : NzbDroneController.prototype.initialize, initialize : function() { - this.route('', this.series); + //this.route('', this.series); this.route('series', this.series); this.route('series/:query', this.seriesDetails); - + this._originalInit.apply(this, arguments); }, @@ -21,10 +21,13 @@ module.exports = NzbDroneController.extend({ }, seriesDetails : function(query) { + console.warn(AppLayout.mainRegion) + var series = SeriesCollection.where({ titleSlug : query }); - + if (series.length !== 0) { var targetSeries = series[0]; + this.setTitle(targetSeries.get('title')); this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries })); } else { diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index ae5c1ec8c..8339a69bc 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -19,6 +19,7 @@ module.exports = Marionette.AppRouter.extend({ vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); + vent.on(vent.Commands.ShowMovieDetails, this._showMovie, this); vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); @@ -62,6 +63,15 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.modalRegion.show(view); }, + _showMovie : function(options) { + var view = new MoviesDetailsLayout({ + model : options.movie, + hideSeriesLink : options.hideSeriesLink, + openingTab : options.openingTab + }); + AppLayout.modalRegion.show(view); + }, + _showHistory : function(options) { var view = new HistoryDetailsLayout({ model : options.model }); AppLayout.modalRegion.show(view); @@ -90,4 +100,4 @@ module.exports = Marionette.AppRouter.extend({ _closeFileBrowser : function() { AppLayout.modalRegion2.closeModal(); } -}); \ No newline at end of file +}); diff --git a/src/UI/main.js b/src/UI/main.js index 3978b36e0..d9da5ede1 100644 --- a/src/UI/main.js +++ b/src/UI/main.js @@ -5,7 +5,8 @@ var RouteBinder = require('./jQuery/RouteBinder'); var SignalRBroadcaster = require('./Shared/SignalRBroadcaster'); var NavbarLayout = require('./Navbar/NavbarLayout'); var AppLayout = require('./AppLayout'); -var SeriesController = require('./Movies/MoviesController'); +var MoviesController = require('./Movies/MoviesController'); +var SeriesController = require('./Series/SeriesController'); var Router = require('./Router'); var ModalController = require('./Shared/Modal/ModalController'); var ControlPanelController = require('./Shared/ControlPanel/ControlPanelController'); @@ -20,6 +21,7 @@ require('./Hotkeys/Hotkeys'); require('./Shared/piwikCheck'); require('./Shared/VersionChangeMonitor'); +new MoviesController(); new SeriesController(); new ModalController(); new ControlPanelController(); diff --git a/src/UI/vent.js b/src/UI/vent.js index 1b9346529..3cd619eef 100644 --- a/src/UI/vent.js +++ b/src/UI/vent.js @@ -18,6 +18,7 @@ vent.Commands = { OpenModal2Command : 'OpenModal2Command', CloseModal2Command : 'CloseModal2Command', ShowEpisodeDetails : 'ShowEpisodeDetails', + ShowMovieDetails : 'ShowMovieDetails', ShowHistoryDetails : 'ShowHistoryDetails', ShowLogDetails : 'ShowLogDetails', SaveSettings : 'saveSettings', @@ -36,4 +37,4 @@ vent.Hotkeys = { ShowHotkeys : 'hotkeys:show' }; -module.exports = vent; \ No newline at end of file +module.exports = vent;