diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index d85cf74d8..a17b9963c 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -3,6 +3,7 @@ using Nancy; using NzbDrone.Api.Episodes; using NzbDrone.Api.Extensions; using NzbDrone.Api.Series; +using NzbDrone.Api.Movie; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; @@ -34,12 +35,18 @@ namespace NzbDrone.Api.History resource.Series = model.Series.ToResource(); resource.Episode = model.Episode.ToResource(); + resource.Movie = model.Movie.ToResource(); if (model.Series != null) { resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Series.Profile.Value, model.Quality); } + if (model.Movie != null) + { + resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Movie.Profile.Value, model.Quality); + } + return resource; } @@ -47,6 +54,8 @@ namespace NzbDrone.Api.History { var episodeId = Request.Query.EpisodeId; + var movieId = Request.Query.MovieId; + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); if (pagingResource.FilterKey == "eventType") @@ -61,6 +70,12 @@ namespace NzbDrone.Api.History pagingSpec.FilterExpression = h => h.EpisodeId == i; } + if (movieId.HasValue) + { + int i = (int)movieId; + pagingSpec.FilterExpression = h => h.MovieId == i; + } + return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource); } diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index dba4149dd..93ec372c7 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Api.Episodes; using NzbDrone.Api.REST; using NzbDrone.Api.Series; +using NzbDrone.Api.Movie; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; @@ -12,6 +13,7 @@ namespace NzbDrone.Api.History public class HistoryResource : RestResource { public int EpisodeId { get; set; } + public int MovieId { get; set; } public int SeriesId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } @@ -22,7 +24,7 @@ namespace NzbDrone.Api.History public HistoryEventType EventType { get; set; } public Dictionary Data { get; set; } - + public MovieResource Movie { get; set; } public EpisodeResource Episode { get; set; } public SeriesResource Series { get; set; } } @@ -39,6 +41,7 @@ namespace NzbDrone.Api.History EpisodeId = model.EpisodeId, SeriesId = model.SeriesId, + MovieId = model.MovieId, SourceTitle = model.SourceTitle, Quality = model.Quality, //QualityCutoffNotMet diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 79588c582..7f92215fb 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Api.Indexers private readonly Logger _logger; private readonly ICached _remoteEpisodeCache; + private readonly ICached _remoteMovieCache; public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, ISearchForNzb nzbSearchService, @@ -49,6 +50,7 @@ namespace NzbDrone.Api.Indexers PostValidator.RuleFor(s => s.Guid).NotEmpty(); _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); + _remoteMovieCache = cacheManager.GetCache(GetType(), "remoteMovies"); } private Response DownloadRelease(ReleaseResource release) @@ -59,7 +61,26 @@ namespace NzbDrone.Api.Indexers { _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); - return new NotFoundResponse(); + var remoteMovie = _remoteMovieCache.Find(release.Guid); + + if (remoteMovie == null) + { + return new NotFoundResponse(); + } + + try + { + _downloadService.DownloadReport(remoteMovie); + } + catch (ReleaseDownloadException ex) + { + _logger.Error(ex, ex.Message); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); + } + + return release.AsResponse(); + + } try @@ -139,7 +160,15 @@ namespace NzbDrone.Api.Indexers protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) { - _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + if (decision.IsForMovie) + { + _remoteMovieCache.Set(decision.RemoteMovie.Release.Guid, decision.RemoteMovie, TimeSpan.FromMinutes(30)); + } + else + { + _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + } + return base.MapDecision(decision, initialWeight); } } diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index cf1356c49..e90a9bace 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Api.Episodes; +using NzbDrone.Api.Movie; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using System.Linq; @@ -14,6 +15,7 @@ namespace NzbDrone.Api.Queue { public SeriesResource Series { get; set; } public EpisodeResource Episode { get; set; } + public MovieResource Movie { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } @@ -49,7 +51,8 @@ namespace NzbDrone.Api.Queue TrackedDownloadStatus = model.TrackedDownloadStatus, StatusMessages = model.StatusMessages, DownloadId = model.DownloadId, - Protocol = model.Protocol + Protocol = model.Protocol, + Movie = model.Movie.ToResource() }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index 02367724e..2fc722cb4 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -104,7 +104,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Quality").AsString() .WithColumn("Indexer").AsString() .WithColumn("NzbInfoUrl").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable(); + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("MovieId").AsInt32().WithDefaultValue(0); Create.TableForModel("Notifications") .WithColumn("Name").AsString() diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 5eab58b3b..299a56b13 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -58,6 +58,32 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic return GetDownloadClientId(strmFile); } + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + if (remoteEpisode.ParsedEpisodeInfo.FullSeason) + { + throw new NotSupportedException("Full season releases are not supported with Pneumatic."); + } + + title = FileNameBuilder.CleanFileName(title); + + //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) + var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); + _httpClient.DownloadFile(url, nzbFile); + + _logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile); + + var strmFile = WriteStrmFile(title, nzbFile); + + + return GetDownloadClientId(strmFile); + } + public bool IsConfigured => !string.IsNullOrWhiteSpace(Settings.NzbFolder); public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 5840526d7..d8c7e825a 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 = true;//remoteEpisode.IsRecentEpisode(); TODO: Update to use RemoteMovie! + var isRecentEpisode = remoteEpisode.IsRecentEpisode(); if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) @@ -71,6 +71,46 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return hash; } + protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, Settings); + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + } + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + }*/ //TODO: Maybe reimplement for movies + + return hash; + } + + protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, Byte[] fileContent) + { + _proxy.AddTorrentFromFile(filename, fileContent, Settings); + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + } + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + }*/ + + return hash; + } + public override string Name => "qBittorrent"; public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 98ade2a69..0e48207ba 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -57,6 +57,7 @@ namespace NzbDrone.Core.Download get; } + public abstract string Download(RemoteEpisode remoteEpisode); public abstract IEnumerable GetItems(); public abstract void RemoveItem(string downloadId, bool deleteData); @@ -147,5 +148,7 @@ namespace NzbDrone.Core.Download return null; } + + public abstract string Download(RemoteMovie remoteMovie); } } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index c5ad3f770..6d0963199 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Download public interface IDownloadService { void DownloadReport(RemoteEpisode remoteEpisode); + void DownloadReport(RemoteMovie remoteMovie); } @@ -91,5 +92,62 @@ namespace NzbDrone.Core.Download _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); _eventAggregator.PublishEvent(episodeGrabbedEvent); } + + public void DownloadReport(RemoteMovie remoteMovie) + { + //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); + //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit + + var downloadTitle = remoteMovie.Release.Title; + var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol); + + if (downloadClient == null) + { + _logger.Warn("{0} Download client isn't configured yet.", remoteMovie.Release.DownloadProtocol); + return; + } + + // Limit grabs to 2 per second. + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + var url = new HttpUri(remoteMovie.Release.DownloadUrl); + _rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2)); + } + + string downloadClientId = ""; + try + { + downloadClientId = downloadClient.Download(remoteMovie); + _indexerStatusService.RecordSuccess(remoteMovie.Release.IndexerId); + } + catch (NotImplementedException ex) + { + _logger.Error(ex, "The download client you are using is currently not configured to download movies. Please choose another one."); + } + catch (ReleaseDownloadException ex) + { + var http429 = ex.InnerException as TooManyRequestsException; + if (http429 != null) + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId, http429.RetryAfter); + } + else + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId); + } + throw; + } + + var episodeGrabbedEvent = new MovieGrabbedEvent(remoteMovie); + episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; + + if (!string.IsNullOrWhiteSpace(downloadClientId)) + { + episodeGrabbedEvent.DownloadId = downloadClientId; + } + + _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); + _eventAggregator.PublishEvent(episodeGrabbedEvent); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 6703d8a22..ecf76844e 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download DownloadProtocol Protocol { get; } string Download(RemoteEpisode remoteEpisode); + string Download(RemoteMovie remoteMovie); IEnumerable GetItems(); void RemoveItem(string downloadId, bool deleteData); DownloadClientStatus GetStatus(); diff --git a/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs new file mode 100644 index 000000000..cb331b24a --- /dev/null +++ b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs @@ -0,0 +1,17 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public class MovieGrabbedEvent : IEvent + { + public RemoteMovie Movie { get; private set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } + + public MovieGrabbedEvent(RemoteMovie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 8585a1704..d5fbff7d8 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.Download.Pending public class PendingReleaseService : IPendingReleaseService, IHandle, IHandle, + IHandle, IHandle { private readonly IIndexerStatusService _indexerStatusService; @@ -341,6 +342,11 @@ namespace NzbDrone.Core.Download.Pending RemoveGrabbed(message.Episode); } + public void Handle(MovieGrabbedEvent message) + { + + } + public void Handle(RssSyncCompleteEvent message) { RemoveRejected(message.ProcessedDecisions.Rejected); diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index b1fcd7e2e..83c013ad7 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -40,21 +40,109 @@ namespace NzbDrone.Core.Download protected abstract string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink); protected abstract string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent); + protected virtual string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) + { + throw new NotImplementedException(); + } + protected virtual string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException(); + } + + public override string Download(RemoteMovie remoteMovie) + { + var torrentInfo = remoteMovie.Release as TorrentInfo; + + string magnetUrl = null; + string torrentUrl = null; + + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + magnetUrl = remoteMovie.Release.DownloadUrl; + } + else + { + torrentUrl = remoteMovie.Release.DownloadUrl; + } + + if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) + { + magnetUrl = torrentInfo.MagnetUrl; + } + + if (PreferTorrentFile) + { + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + catch (Exception ex) + { + if (!magnetUrl.IsNullOrWhiteSpace()) + { + throw; + } + + _logger.Debug("Torrent download failed, trying magnet. ({0})", ex.Message); + } + } + + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + } + } + else + { + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + if (torrentUrl.IsNullOrWhiteSpace()) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + + _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); + } + } - public override string Download(RemoteEpisode remoteEpisode) + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + } + + return null; + } + + public override string Download(RemoteEpisode remoteMovie) { - var torrentInfo = remoteEpisode.Release as TorrentInfo; + var torrentInfo = remoteMovie.Release as TorrentInfo; string magnetUrl = null; string torrentUrl = null; - if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteEpisode.Release.DownloadUrl.StartsWith("magnet:")) + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) { - magnetUrl = remoteEpisode.Release.DownloadUrl; + magnetUrl = remoteMovie.Release.DownloadUrl; } else { - torrentUrl = remoteEpisode.Release.DownloadUrl; + torrentUrl = remoteMovie.Release.DownloadUrl; } if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) @@ -68,7 +156,7 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } catch (Exception ex) { @@ -85,11 +173,11 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } } } @@ -99,13 +187,13 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { if (torrentUrl.IsNullOrWhiteSpace()) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); @@ -114,13 +202,113 @@ namespace NzbDrone.Core.Download if (torrentUrl.IsNotNullOrWhiteSpace()) { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } } return null; } + private string DownloadFromWebUrl(RemoteMovie remoteEpisode, string torrentUrl) + { + byte[] torrentFile = null; + + try + { + var request = new HttpRequest(torrentUrl); + request.Headers.Accept = "application/x-bittorrent"; + request.AllowAutoRedirect = false; + + var response = _httpClient.Get(request); + + if (response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.Found) + { + var locationHeader = response.Headers.GetSingleValue("Location"); + + _logger.Trace("Torrent request is being redirected to: {0}", locationHeader); + + if (locationHeader != null) + { + if (locationHeader.StartsWith("magnet:")) + { + return DownloadFromMagnetUrl(remoteEpisode, locationHeader); + } + + return DownloadFromWebUrl(remoteEpisode, locationHeader); + } + + throw new WebException("Remote website tried to redirect without providing a location."); + } + + torrentFile = response.ResponseData; + + _logger.Debug("Downloading torrent for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, torrentFile.Length, torrentUrl); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", torrentUrl); + } + else + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + + var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteEpisode.Release.Title)); + var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile); + var actualHash = AddFromTorrentFile(remoteEpisode, hash, filename, torrentFile); + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + + private string DownloadFromMagnetUrl(RemoteMovie remoteEpisode, string magnetUrl) + { + string hash = null; + string actualHash = null; + + try + { + hash = new MagnetLink(magnetUrl).InfoHash.ToHex(); + } + catch (FormatException ex) + { + _logger.Error(ex, "Failed to parse magnetlink for episode '{0}': '{1}'", remoteEpisode.Release.Title, magnetUrl); + + return null; + } + + if (hash != null) + { + actualHash = AddFromMagnetLink(remoteEpisode, hash, magnetUrl); + } + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + private string DownloadFromWebUrl(RemoteEpisode remoteEpisode, string torrentUrl) { byte[] torrentFile = null; diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index be012d57b..57ce35578 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public TrackedDownloadStage State { get; set; } public TrackedDownloadStatus Status { get; private set; } public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 55ce7398d..c7ba36cbc 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -62,6 +62,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (parsedEpisodeInfo != null) { trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + trackedDownload.RemoteMovie = _parsingService.Map(parsedEpisodeInfo, "", null); } if (historyItems.Any()) @@ -69,10 +70,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); - if (parsedEpisodeInfo == null || + if ((parsedEpisodeInfo == null || trackedDownload.RemoteEpisode == null || trackedDownload.RemoteEpisode.Series == null || - trackedDownload.RemoteEpisode.Episodes.Empty()) + trackedDownload.RemoteEpisode.Episodes.Empty()) && trackedDownload.RemoteMovie == null) { // Try parsing the original source title and if that fails, try parsing it as a special // TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item @@ -85,7 +86,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - if (trackedDownload.RemoteEpisode == null) + if (trackedDownload.RemoteEpisode == null && trackedDownload.RemoteMovie == null) { return null; } diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index a6c0ed7d5..e45bb2d89 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -1,4 +1,5 @@ using System.Net; +using System; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; @@ -31,6 +32,11 @@ namespace NzbDrone.Core.Download protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent); + protected virtual string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + throw new NotImplementedException(); + } + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; @@ -67,5 +73,42 @@ namespace NzbDrone.Core.Download _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); return AddFromNzbFile(remoteEpisode, filename, nzbData); } + + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var filename = FileNameBuilder.CleanFileName(remoteEpisode.Release.Title) + ".nzb"; + + byte[] nzbData; + + try + { + nzbData = _httpClient.Get(new HttpRequest(url)).ResponseData; + + _logger.Debug("Downloaded nzb for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, nzbData.Length, url); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", url); + } + else + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + + _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); + return AddFromNzbFile(remoteEpisode, filename, nzbData); + } } } diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index be35637c8..451e9b1d5 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -17,9 +17,11 @@ namespace NzbDrone.Core.History public int EpisodeId { get; set; } public int SeriesId { get; set; } + public int MovieId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } + public Movie Movie { get; set; } public Episode Episode { get; set; } public Series Series { get; set; } public HistoryEventType EventType { get; set; } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 35199a878..bc0a54a5a 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.History List FindByDownloadId(string downloadId); List FindDownloadHistory(int idSeriesId, QualityModel quality); void DeleteForSeries(int seriesId); + History MostRecentForMovie(int movieId); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -71,10 +73,20 @@ namespace NzbDrone.Core.History protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { - var baseQuery = query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) - .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id); + var baseQuery = query/*.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) + .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id)*/ + .Join(JoinType.Inner, h => h.Movie, (h, e) => h.MovieId == e.Id); + + return base.GetPagedQuery(baseQuery, pagingSpec); } + + public History MostRecentForMovie(int movieId) + { + return Query.Where(h => h.MovieId == movieId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 32815beef..8185a1c6e 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.History { QualityModel GetBestQualityInHistory(Profile profile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); + History MostRecentForMovie(int movieId); History MostRecentForEpisode(int episodeId); History MostRecentForDownloadId(string downloadId); History Get(int historyId); @@ -29,6 +30,7 @@ namespace NzbDrone.Core.History public class HistoryService : IHistoryService, IHandle, + IHandle, IHandle, IHandle, IHandle, @@ -53,6 +55,11 @@ namespace NzbDrone.Core.History return _historyRepository.MostRecentForEpisode(episodeId); } + public History MostRecentForMovie(int movieId) + { + return _historyRepository.MostRecentForMovie(movieId); + } + public History MostRecentForDownloadId(string downloadId) { return _historyRepository.MostRecentForDownloadId(downloadId); @@ -138,7 +145,8 @@ namespace NzbDrone.Core.History SourceTitle = message.Episode.Release.Title, SeriesId = episode.SeriesId, EpisodeId = episode.Id, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + MovieId = 0 }; history.Data.Add("Indexer", message.Episode.Release.Indexer); @@ -172,6 +180,50 @@ namespace NzbDrone.Core.History } } + public void Handle(MovieGrabbedEvent message) + { + var history = new History + { + EventType = HistoryEventType.Grabbed, + Date = DateTime.UtcNow, + Quality = message.Movie.ParsedEpisodeInfo.Quality, + SourceTitle = message.Movie.Release.Title, + SeriesId = 0, + EpisodeId = 0, + DownloadId = message.DownloadId, + MovieId = message.Movie.Movie.Id + }; + + history.Data.Add("Indexer", message.Movie.Release.Indexer); + history.Data.Add("NzbInfoUrl", message.Movie.Release.InfoUrl); + history.Data.Add("ReleaseGroup", message.Movie.ParsedEpisodeInfo.ReleaseGroup); + history.Data.Add("Age", message.Movie.Release.Age.ToString()); + history.Data.Add("AgeHours", message.Movie.Release.AgeHours.ToString()); + history.Data.Add("AgeMinutes", message.Movie.Release.AgeMinutes.ToString()); + history.Data.Add("PublishedDate", message.Movie.Release.PublishDate.ToString("s") + "Z"); + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("Size", message.Movie.Release.Size.ToString()); + history.Data.Add("DownloadUrl", message.Movie.Release.DownloadUrl); + history.Data.Add("Guid", message.Movie.Release.Guid); + history.Data.Add("TvdbId", message.Movie.Release.TvdbId.ToString()); + history.Data.Add("TvRageId", message.Movie.Release.TvRageId.ToString()); + history.Data.Add("Protocol", ((int)message.Movie.Release.DownloadProtocol).ToString()); + + if (!message.Movie.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) + { + history.Data.Add("ReleaseHash", message.Movie.ParsedEpisodeInfo.ReleaseHash); + } + + var torrentRelease = message.Movie.Release as TorrentInfo; + + if (torrentRelease != null) + { + history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash); + } + + _historyRepository.Insert(history); + } + public void Handle(EpisodeImportedEvent message) { if (!message.NewDownload) @@ -189,15 +241,18 @@ namespace NzbDrone.Core.History foreach (var episode in message.EpisodeInfo.Episodes) { var history = new History - { - EventType = HistoryEventType.DownloadFolderImported, - Date = DateTime.UtcNow, - Quality = message.EpisodeInfo.Quality, - SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), - SeriesId = message.ImportedEpisode.SeriesId, - EpisodeId = episode.Id, - DownloadId = downloadId - }; + { + EventType = HistoryEventType.DownloadFolderImported, + Date = DateTime.UtcNow, + Quality = message.EpisodeInfo.Quality, + SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), + SeriesId = message.ImportedEpisode.SeriesId, + EpisodeId = episode.Id, + DownloadId = downloadId, + MovieId = 0, + + + }; //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); @@ -249,6 +304,7 @@ namespace NzbDrone.Core.History SourceTitle = message.EpisodeFile.Path, SeriesId = message.EpisodeFile.SeriesId, EpisodeId = episode.Id, + MovieId = 0 }; history.Data.Add("Reason", message.Reason.ToString()); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 01c680406..4bc218fe3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -457,6 +457,7 @@ + diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 7164a17ae..851c404e2 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Queue { public Series Series { get; set; } public Episode Episode { get; set; } + public Movie Movie { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } @@ -24,6 +25,7 @@ namespace NzbDrone.Core.Queue public List StatusMessages { get; set; } public string DownloadId { get; set; } public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } public DownloadProtocol Protocol { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 264645ed8..d867b4ddb 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -51,12 +51,42 @@ namespace NzbDrone.Core.Queue yield return MapEpisode(trackedDownload, episode); } } - else + else if (trackedDownload.RemoteMovie.Movie != null) { - // FIXME: Present queue items with unknown series/episodes + yield return MapMovie(trackedDownload, trackedDownload.RemoteMovie.Movie); } } + private Queue MapMovie(TrackedDownload trackedDownload, Movie movie) + { + var queue = new Queue + { + Id = HashConverter.GetHashInt31(string.Format("trackedDownload-{0}", trackedDownload.DownloadItem.DownloadId)), + Series = null, + Episode = null, + Quality = trackedDownload.RemoteMovie.ParsedEpisodeInfo.Quality, + Title = trackedDownload.DownloadItem.Title, + Size = trackedDownload.DownloadItem.TotalSize, + Sizeleft = trackedDownload.DownloadItem.RemainingSize, + Timeleft = trackedDownload.DownloadItem.RemainingTime, + Status = trackedDownload.DownloadItem.Status.ToString(), + TrackedDownloadStatus = trackedDownload.Status.ToString(), + StatusMessages = trackedDownload.StatusMessages.ToList(), + RemoteEpisode = trackedDownload.RemoteEpisode, + RemoteMovie = trackedDownload.RemoteMovie, + DownloadId = trackedDownload.DownloadItem.DownloadId, + Protocol = trackedDownload.Protocol, + Movie = movie + }; + + if (queue.Timeleft.HasValue) + { + queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value); + } + + return queue; + } + private Queue MapEpisode(TrackedDownload trackedDownload, Episode episode) { var queue = new Queue diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayout.js b/src/UI/Activity/History/Details/HistoryDetailsLayout.js index 5654a3e72..42f583ddb 100644 --- a/src/UI/Activity/History/Details/HistoryDetailsLayout.js +++ b/src/UI/Activity/History/Details/HistoryDetailsLayout.js @@ -32,4 +32,4 @@ module.exports = Marionette.Layout.extend({ vent.trigger(vent.Commands.CloseModalCommand); } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/History/HistoryCollection.js b/src/UI/Activity/History/HistoryCollection.js index 3bd564309..5eb4dc4f7 100644 --- a/src/UI/Activity/History/HistoryCollection.js +++ b/src/UI/Activity/History/HistoryCollection.js @@ -50,16 +50,20 @@ var Collection = PageableCollection.extend({ }, sortMappings : { - 'series' : { sortKey : 'series.sortTitle' } + 'movie' : { sortKey : 'movie.sortTitle' } }, initialize : function(options) { delete this.queryParams.episodeId; + delete this.queryParams.movieId; if (options) { if (options.episodeId) { this.queryParams.episodeId = options.episodeId; } + if (options.movieId) { + this.queryParams.movieId = options.movieId; + } } }, @@ -80,4 +84,4 @@ Collection = AsFilteredCollection.call(Collection); Collection = AsSortedCollection.call(Collection); Collection = AsPersistedStateCollection.call(Collection); -module.exports = Collection; \ No newline at end of file +module.exports = Collection; diff --git a/src/UI/Activity/History/HistoryLayout.js b/src/UI/Activity/History/HistoryLayout.js index ae7e4c93e..28e919365 100644 --- a/src/UI/Activity/History/HistoryLayout.js +++ b/src/UI/Activity/History/HistoryLayout.js @@ -2,7 +2,7 @@ var Marionette = require('marionette'); var Backgrid = require('backgrid'); var HistoryCollection = require('./HistoryCollection'); var EventTypeCell = require('../../Cells/EventTypeCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); +var MovieTitleCell = require('../../Cells/MovieTitleCell'); var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); var HistoryQualityCell = require('./HistoryQualityCell'); @@ -29,11 +29,11 @@ module.exports = Marionette.Layout.extend({ cellValue : 'this' }, { - name : 'series', - label : 'Series', - cell : SeriesTitleCell + name : 'movies', + label : 'Movie Title', + cell : MovieTitleCell }, - { + /*{ name : 'episode', label : 'Episode', cell : EpisodeNumberCell, @@ -44,7 +44,7 @@ module.exports = Marionette.Layout.extend({ label : 'Episode Title', cell : EpisodeTitleCell, sortable : false - }, + },*/ { name : 'this', label : 'Quality', diff --git a/src/UI/Activity/History/HistoryModel.js b/src/UI/Activity/History/HistoryModel.js index f8ec8c538..967b7ba22 100644 --- a/src/UI/Activity/History/HistoryModel.js +++ b/src/UI/Activity/History/HistoryModel.js @@ -1,12 +1,20 @@ var Backbone = require('backbone'); var SeriesModel = require('../../Series/SeriesModel'); var EpisodeModel = require('../../Series/EpisodeModel'); +var MovieModel = require('../../Movies/MovieModel'); module.exports = Backbone.Model.extend({ parse : function(model) { - model.series = new SeriesModel(model.series); - model.episode = new EpisodeModel(model.episode); - model.episode.set('series', model.series); + if (model.series) { + model.series = new SeriesModel(model.series); + model.episode = new EpisodeModel(model.episode); + model.episode.set('series', model.series); + } + + if (model.movie) { + model.movie = new MovieModel(model.movie); + } + return model; } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/History/HistoryQualityCell.js b/src/UI/Activity/History/HistoryQualityCell.js index c65aa042b..f779c714e 100644 --- a/src/UI/Activity/History/HistoryQualityCell.js +++ b/src/UI/Activity/History/HistoryQualityCell.js @@ -27,4 +27,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/Queue/QueueCollection.js b/src/UI/Activity/Queue/QueueCollection.js index 474cafe6b..bd3aa065e 100644 --- a/src/UI/Activity/Queue/QueueCollection.js +++ b/src/UI/Activity/Queue/QueueCollection.js @@ -35,6 +35,14 @@ var QueueCollection = PageableCollection.extend({ } }, + movie : { + sortValue : function(model, attr) { + var movie = model.get(attr); + + return movie.get('sortTitle'); + } + }, + episode : { sortValue : function(model, attr) { var episode = model.get('episode'); @@ -84,4 +92,4 @@ QueueCollection = AsPageableCollection.call(QueueCollection); var collection = new QueueCollection().bindSignalR(); collection.fetch(); -module.exports = collection; \ No newline at end of file +module.exports = collection; diff --git a/src/UI/Activity/Queue/QueueLayout.js b/src/UI/Activity/Queue/QueueLayout.js index 462c6a568..4416cb07b 100644 --- a/src/UI/Activity/Queue/QueueLayout.js +++ b/src/UI/Activity/Queue/QueueLayout.js @@ -1,7 +1,7 @@ var Marionette = require('marionette'); var Backgrid = require('backgrid'); var QueueCollection = require('./QueueCollection'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); +var SeriesTitleCell = require('../../Cells/MovieTitleCell'); var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); var QualityCell = require('../../Cells/QualityCell'); @@ -28,11 +28,11 @@ module.exports = Marionette.Layout.extend({ cellValue : 'this' }, { - name : 'series', - label : 'Series', + name : 'movie', + label : 'Movie', cell : SeriesTitleCell }, - { + /*{ name : 'episode', label : 'Episode', cell : EpisodeNumberCell @@ -42,7 +42,7 @@ module.exports = Marionette.Layout.extend({ label : 'Episode Title', cell : EpisodeTitleCell, cellValue : 'episode' - }, + },*/ { name : 'quality', label : 'Quality', diff --git a/src/UI/Activity/Queue/QueueModel.js b/src/UI/Activity/Queue/QueueModel.js index f8ec8c538..e9b3fb045 100644 --- a/src/UI/Activity/Queue/QueueModel.js +++ b/src/UI/Activity/Queue/QueueModel.js @@ -1,12 +1,14 @@ var Backbone = require('backbone'); var SeriesModel = require('../../Series/SeriesModel'); var EpisodeModel = require('../../Series/EpisodeModel'); +var MovieModel = require('../../Movies/MovieModel'); module.exports = Backbone.Model.extend({ parse : function(model) { model.series = new SeriesModel(model.series); model.episode = new EpisodeModel(model.episode); model.episode.set('series', model.series); + model.movie = new MovieModel(model.movie); return model; } -}); \ No newline at end of file +}); diff --git a/src/UI/Cells/MovieTitleCell.js b/src/UI/Cells/MovieTitleCell.js new file mode 100644 index 000000000..4ae8fe20c --- /dev/null +++ b/src/UI/Cells/MovieTitleCell.js @@ -0,0 +1,11 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Cells/SeriesTitleTemplate', + + render : function() { + this.$el.html(this.model.get("movie").get("title")); //Hack, but somehow handlebar helper does not work. + return this; + } +}); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs index cce213b01..3a63cd045 100644 --- a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -35,7 +35,7 @@
-
+