Merged branch develop into feature/net-import

pull/2/head
Leonardo Galli 8 years ago
commit 451f2d30e4

@ -1,6 +1,8 @@
Please use the search bar and make sure you are not submitting an already submitted issue.
Provide a description of the feature request or bug, the more details the better. Provide a description of the feature request or bug, the more details the better.
When possible include a log! When possible include a log!

@ -4,13 +4,17 @@
[![GitHub pull requests](https://img.shields.io/github/issues-pr/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/pulls) [![GitHub pull requests](https://img.shields.io/github/issues-pr/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/pulls)
[![GNU GPL v3](https://img.shields.io/badge/license-GNU%20GPL%20v3-blue.svg?maxAge=60&style=flat-square)](http://www.gnu.org/licenses/gpl.html) [![GNU GPL v3](https://img.shields.io/badge/license-GNU%20GPL%20v3-blue.svg?maxAge=60&style=flat-square)](http://www.gnu.org/licenses/gpl.html)
[![Copyright 2010-2017](https://img.shields.io/badge/copyright-2017-blue.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr) [![Copyright 2010-2017](https://img.shields.io/badge/copyright-2017-blue.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr)
[![Github Releases](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg?maxAge=60&style=flat-square)](https://github.com/Radar/Radarr/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg?maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr/)
| Service | Master | Develop | | Service | Master | Develop |
|----------|:---------------------------:|:----------------------------:| |----------|:---------------------------:|:----------------------------:|
| AppVeyor | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr/master.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr) | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr-usby1/develop.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr-usby1) | | AppVeyor | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr/master.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr) | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr-usby1/develop.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr-usby1) |
| Travis | [![Travis](https://img.shields.io/travis/Radarr/Radarr/master.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) | [![Travis](https://img.shields.io/travis/Radarr/Radarr/develop.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) | | Travis | [![Travis](https://img.shields.io/travis/Radarr/Radarr/master.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) | [![Travis](https://img.shields.io/travis/Radarr/Radarr/develop.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) |
This fork of Sonarr aims to turn it into something like CouchPotato. A fork of [Sonarr](https://github.com/Sonarr/Sonarr) to work with movies à la Couchpotato.
**This fork works independently of Sonarr and will not interfere with it.**
## Downloads ## Downloads
@ -18,41 +22,45 @@ This fork of Sonarr aims to turn it into something like CouchPotato.
[![AppVeyor Builds](https://img.shields.io/badge/downloads-continuous-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/build/artifacts) [![AppVeyor Builds](https://img.shields.io/badge/downloads-continuous-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/build/artifacts)
[![Docker x64](https://img.shields.io/badge/docker-x64-blue.svg?maxAge=60&style=flat-square)](https://store.docker.com/community/images/linuxserver/radarr) [![Docker x64](https://img.shields.io/badge/docker-x64-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/linuxserver/radarr)
[![Docker armhf](https://img.shields.io/badge/docker-armhf-blue.svg?maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr) [![Docker armhf](https://img.shields.io/badge/docker-armhf-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr)
[![Docker aarch64](https://img.shields.io/badge/docker-aarch64-blue.svg?maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr-aarch64) [![Docker aarch64](https://img.shields.io/badge/docker-aarch64-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr-aarch64)
To connect to the UI, fire up your browser and open <http://localhost:7878> or <http://your-ip:7878>. To connect to the UI, fire up your browser and open <http://localhost:7878> or <http://your-ip:7878>.
## Support
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60&style=flat-square)](https://discord.gg/AD3UP37)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60&style=flat-square)](https://www.reddit.com/r/radarr)
[![GitHub](https://img.shields.io/badge/github-issues-181717.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
## Features ## Features
### Currently Working ### Current Features
* Adding new movies * Adding new movies with lots of information, such as trailers, ratings, etc.
* Manually searching for releases of movies * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically searching for releases * Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
* Automatically searching for releases as well as RSS Sync
* Automatically importing downloaded movies * Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc. * Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs * Identifying releases with hardcoded subs
* Rarbg.to, Torznab and Newznab Indexer * All indexers supported by Sonarr also supported
* QBittorrent and Deluge download client (Other clients are coming) * New PassThePopcorn Indexer
* QBittorrent, Deluge, rTorrent, Transmission and uTorrent download client (Other clients are coming)
* New TorrentPotato Indexer (Works well with [Jackett](https://github.com/Jackett/Jackett)) * New TorrentPotato Indexer (Works well with [Jackett](https://github.com/Jackett/Jackett))
* And a beautiful UI
### Planned Features ### Planned Features
* Scanning PreDB to know when a new release is available * Scanning PreDB to know when a new release is available
* Fixing the other Indexers and download clients * Fixing the other Indexers and download clients
* Importing of Sonarr config * Importing movies from various online sources, such as IMDb Watchlists (A complete list can be found [here](https://github.com/Radarr/Radarr/issues/114))
### Major Features
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata) * Full integration with Kodi, Plex (notification, library update, metadata)
* And a beautiful UI
## Configuring Development Environment ## Configuring Development Environment
@ -70,7 +78,8 @@ To connect to the UI, fire up your browser and open <http://localhost:7878> or <
* Install the required Node Packages `npm install` * Install the required Node Packages `npm install`
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command. * Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
*Please note: gulp must be running at all times while you are working with Radarr client source files.* > **Notice**
> Gulp must be running at all times while you are working with Radarr client source files.
### Development ### Development

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Api.Movie;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Api.Series; using NzbDrone.Api.Series;
@ -11,13 +12,14 @@ namespace NzbDrone.Api.Blacklist
{ {
public int SeriesId { get; set; } public int SeriesId { get; set; }
public List<int> EpisodeIds { get; set; } public List<int> EpisodeIds { get; set; }
public int MovieId { get; set; }
public string SourceTitle { get; set; } public string SourceTitle { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public string Indexer { get; set; } public string Indexer { get; set; }
public string Message { get; set; } public string Message { get; set; }
public MovieResource Movie { get; set; }
public SeriesResource Series { get; set; } public SeriesResource Series { get; set; }
} }
@ -30,7 +32,7 @@ namespace NzbDrone.Api.Blacklist
return new BlacklistResource return new BlacklistResource
{ {
Id = model.Id, Id = model.Id,
MovieId = model.MovieId,
SeriesId = model.SeriesId, SeriesId = model.SeriesId,
EpisodeIds = model.EpisodeIds, EpisodeIds = model.EpisodeIds,
SourceTitle = model.SourceTitle, SourceTitle = model.SourceTitle,
@ -39,7 +41,7 @@ namespace NzbDrone.Api.Blacklist
Protocol = model.Protocol, Protocol = model.Protocol,
Indexer = model.Indexer, Indexer = model.Indexer,
Message = model.Message, Message = model.Message,
Movie = model.Movie.ToResource(),
Series = model.Series.ToResource() Series = model.Series.ToResource()
}; };
} }

@ -2,24 +2,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Api.Episodes; using NzbDrone.Api.Episodes;
using NzbDrone.Api.Movie;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MovieStats;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Validation;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.SignalR; using NzbDrone.SignalR;
namespace NzbDrone.Api.Calendar namespace NzbDrone.Api.Calendar
{ {
public class CalendarModule : EpisodeModuleWithSignalR public class CalendarModule : MovieModule
{ {
public CalendarModule(IEpisodeService episodeService, public CalendarModule(IBroadcastSignalRMessage signalR,
ISeriesService seriesService, IMovieService moviesService,
IQualityUpgradableSpecification qualityUpgradableSpecification, IMovieStatisticsService moviesStatisticsService,
IBroadcastSignalRMessage signalRBroadcaster) ISceneMappingService sceneMappingService,
: base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "calendar") IMapCoversToLocal coverMapper)
: base(signalR, moviesService, moviesStatisticsService, sceneMappingService, coverMapper, "calendar")
{ {
GetResourceAll = GetCalendar; GetResourceAll = GetCalendar;
} }
private List<EpisodeResource> GetCalendar() private List<MovieResource> GetCalendar()
{ {
var start = DateTime.Today; var start = DateTime.Today;
var end = DateTime.Today.AddDays(2); var end = DateTime.Today.AddDays(2);
@ -33,9 +47,9 @@ namespace NzbDrone.Api.Calendar
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
if (queryIncludeUnmonitored.HasValue) includeUnmonitored = Convert.ToBoolean(queryIncludeUnmonitored.Value); if (queryIncludeUnmonitored.HasValue) includeUnmonitored = Convert.ToBoolean(queryIncludeUnmonitored.Value);
var resources = MapToResource(_episodeService.EpisodesBetweenDates(start, end, includeUnmonitored), true, true); var resources = _moviesService.GetMoviesBetweenDates(start, end, includeUnmonitored).Select(MapToResource);
return resources.OrderBy(e => e.AirDateUtc).ToList(); return resources.OrderBy(e => e.InCinemas).ToList();
} }
} }
} }

@ -12,7 +12,7 @@ namespace NzbDrone.Api.Movies
private readonly IRenameMovieFileService _renameMovieFileService; private readonly IRenameMovieFileService _renameMovieFileService;
public RenameMovieModule(IRenameMovieFileService renameMovieFileService) public RenameMovieModule(IRenameMovieFileService renameMovieFileService)
: base("rename") : base("renameMovie")
{ {
_renameMovieFileService = renameMovieFileService; _renameMovieFileService = renameMovieFileService;

@ -264,6 +264,7 @@
<Compile Include="Wanted\CutoffModule.cs" /> <Compile Include="Wanted\CutoffModule.cs" />
<Compile Include="Wanted\LegacyMissingModule.cs" /> <Compile Include="Wanted\LegacyMissingModule.cs" />
<Compile Include="Wanted\MissingModule.cs" /> <Compile Include="Wanted\MissingModule.cs" />
<Compile Include="Wanted\MovieMissingModule.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" /> <None Include="app.config" />

@ -28,7 +28,7 @@ namespace NzbDrone.Api.Movie
IHandle<MediaCoversUpdatedEvent> IHandle<MediaCoversUpdatedEvent>
{ {
private readonly IMovieService _moviesService; protected readonly IMovieService _moviesService;
private readonly IMovieStatisticsService _moviesStatisticsService; private readonly IMovieStatisticsService _moviesStatisticsService;
private readonly IMapCoversToLocal _coverMapper; private readonly IMapCoversToLocal _coverMapper;
@ -78,13 +78,33 @@ namespace NzbDrone.Api.Movie
PutValidator.RuleFor(s => s.Path).IsValidPath(); PutValidator.RuleFor(s => s.Path).IsValidPath();
} }
public MovieModule(IBroadcastSignalRMessage signalRBroadcaster,
IMovieService moviesService,
IMovieStatisticsService moviesStatisticsService,
ISceneMappingService sceneMappingService,
IMapCoversToLocal coverMapper,
string resource)
: base(signalRBroadcaster, resource)
{
_moviesService = moviesService;
_moviesStatisticsService = moviesStatisticsService;
_coverMapper = coverMapper;
GetResourceAll = AllMovie;
GetResourceById = GetMovie;
CreateResource = AddMovie;
UpdateResource = UpdateMovie;
DeleteResource = DeleteMovie;
}
private MovieResource GetMovie(int id) private MovieResource GetMovie(int id)
{ {
var movies = _moviesService.GetMovie(id); var movies = _moviesService.GetMovie(id);
return MapToResource(movies); return MapToResource(movies);
} }
private MovieResource MapToResource(Core.Tv.Movie movies) protected MovieResource MapToResource(Core.Tv.Movie movies)
{ {
if (movies == null) return null; if (movies == null) return null;
@ -182,6 +202,8 @@ namespace NzbDrone.Api.Movie
//if (mappings == null) return; //if (mappings == null) return;
//Not necessary anymore
//resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); //resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
} }
@ -219,7 +241,7 @@ namespace NzbDrone.Api.Movie
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
//BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
} }
} }
} }

@ -34,6 +34,8 @@ namespace NzbDrone.Api.Movie
public string RemotePoster { get; set; } public string RemotePoster { get; set; }
public int Year { get; set; } public int Year { get; set; }
public bool HasFile { get; set; } public bool HasFile { get; set; }
public string YouTubeTrailerId { get; set; }
public string Studio { get; set; }
//View & Edit //View & Edit
public string Path { get; set; } public string Path { get; set; }
@ -144,7 +146,9 @@ namespace NzbDrone.Api.Movie
AddOptions = model.AddOptions, AddOptions = model.AddOptions,
AlternativeTitles = model.AlternativeTitles, AlternativeTitles = model.AlternativeTitles,
Ratings = model.Ratings, Ratings = model.Ratings,
MovieFile = movieFile MovieFile = movieFile,
YouTubeTrailerId = model.YouTubeTrailerId,
Studio = model.Studio
}; };
} }
@ -191,7 +195,9 @@ namespace NzbDrone.Api.Movie
Added = resource.Added, Added = resource.Added,
AddOptions = resource.AddOptions, AddOptions = resource.AddOptions,
AlternativeTitles = resource.AlternativeTitles, AlternativeTitles = resource.AlternativeTitles,
Ratings = resource.Ratings Ratings = resource.Ratings,
YouTubeTrailerId = resource.YouTubeTrailerId,
Studio = resource.Studio
}; };
} }

@ -236,7 +236,7 @@ namespace NzbDrone.Api.Series
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, message.Series.Id); //BroadcastResourceChange(ModelAction.Updated, message.Series.Id);
} }
} }
} }

@ -12,7 +12,7 @@ namespace NzbDrone.Api.Wanted
ISeriesService seriesService, ISeriesService seriesService,
IQualityUpgradableSpecification qualityUpgradableSpecification, IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster) IBroadcastSignalRMessage signalRBroadcaster)
: base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing") : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing_episodes")
{ {
GetResourcePaged = GetMissingEpisodes; GetResourcePaged = GetMissingEpisodes;
} }

@ -0,0 +1,77 @@
using NzbDrone.Api.Movie;
using NzbDrone.Api.Movies;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Datastore;
using NzbDrone.SignalR;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using System;
using NzbDrone.Core.Datastore.Events;
namespace NzbDrone.Api.Wanted
{
class MovieMissingModule : NzbDroneRestModuleWithSignalR<MovieResource, Core.Tv.Movie>,
IHandle<MovieGrabbedEvent>,
IHandle<MovieDownloadedEvent>
{
protected readonly IMovieService _movieService;
public MovieMissingModule(IMovieService movieService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(signalRBroadcaster, "wanted/missing")
{
_movieService = movieService;
GetResourcePaged = GetMissingMovies;
}
private PagingResource<MovieResource> GetMissingMovies(PagingResource<MovieResource> pagingResource)
{
var pagingSpec = pagingResource.MapToPagingSpec<MovieResource, Core.Tv.Movie>("physicalRelease", SortDirection.Descending);
if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false")
{
pagingSpec.FilterExpression = v => v.Monitored == false;
}
else
{
pagingSpec.FilterExpression = v => v.Monitored == true;
}
var resource = ApplyToPage(_movieService.MoviesWithoutFiles, pagingSpec, v => MapToResource(v, false));
return resource;
}
private MovieResource GetMovie(int id)
{
var movie = _movieService.GetMovie(id);
var resource = MapToResource(movie, true);
return resource;
}
private MovieResource MapToResource(Core.Tv.Movie movie, bool includeMovieFile)
{
var resource = movie.ToResource();
return resource;
}
public void Handle(MovieGrabbedEvent message)
{
var resource = message.Movie.Movie.ToResource();
//add a grabbed field in MovieResource?
//resource.Grabbed = true;
BroadcastResourceChange(ModelAction.Updated, resource);
}
public void Handle(MovieDownloadedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Movie.Id);
}
}
}

@ -92,14 +92,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
protected void GivenFailedDownload() protected void GivenFailedDownload()
{ {
Mocker.GetMock<INzbgetProxy>() Mocker.GetMock<INzbgetProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>())) .Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<NzbgetSettings>()))
.Returns((string)null); .Returns((string)null);
} }
protected void GivenSuccessfulDownload() protected void GivenSuccessfulDownload()
{ {
Mocker.GetMock<INzbgetProxy>() Mocker.GetMock<INzbgetProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>())) .Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<NzbgetSettings>()))
.Returns(Guid.NewGuid().ToString().Replace("-", "")); .Returns(Guid.NewGuid().ToString().Replace("-", ""));
} }

@ -47,5 +47,17 @@ namespace NzbDrone.Core.Test.ParserTests
{ {
QualityParser.ParseQuality(title).Revision.Version.Should().Be(version); QualityParser.ParseQuality(title).Revision.Version.Should().Be(version);
} }
[TestCase("Deadpool 2016 2160p 4K UltraHD BluRay DTS-HD MA 7 1 x264-Whatevs", 19)]
[TestCase("Deadpool 2016 2160p 4K UltraHD DTS-HD MA 7 1 x264-Whatevs", 16)]
[TestCase("Deadpool 2016 4K 2160p UltraHD BluRay AAC2 0 HEVC x265", 19)]
[TestCase("The Revenant 2015 2160p UHD BluRay DTS x264-Whatevs", 19)]
[TestCase("The Revenant 2015 2160p UHD BluRay FLAC 7 1 x264-Whatevs", 19)]
[TestCase("The Martian 2015 2160p Ultra HD BluRay DTS-HD MA 7 1 x264-Whatevs", 19)]
[TestCase("Into the Inferno 2016 2160p Netflix WEBRip DD5 1 x264-Whatevs", 18)]
public void should_parse_ultrahd_from_title(string title, int version)
{
QualityParser.ParseQuality(title).Quality.Id.Should().Be(version);
}
} }
} }

@ -11,6 +11,8 @@ namespace NzbDrone.Core.Blacklisting
{ {
public int SeriesId { get; set; } public int SeriesId { get; set; }
public Series Series { get; set; } public Series Series { get; set; }
public int MovieId { get; set; }
public Movie Movie { get; set; }
public List<int> EpisodeIds { get; set; } public List<int> EpisodeIds { get; set; }
public string SourceTitle { get; set; } public string SourceTitle { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }

@ -10,7 +10,7 @@ namespace NzbDrone.Core.Blacklisting
{ {
List<Blacklist> BlacklistedByTitle(int seriesId, string sourceTitle); List<Blacklist> BlacklistedByTitle(int seriesId, string sourceTitle);
List<Blacklist> BlacklistedByTorrentInfoHash(int seriesId, string torrentInfoHash); List<Blacklist> BlacklistedByTorrentInfoHash(int seriesId, string torrentInfoHash);
List<Blacklist> BlacklistedBySeries(int seriesId); List<Blacklist> BlacklistedByMovie(int seriesId);
} }
public class BlacklistRepository : BasicRepository<Blacklist>, IBlacklistRepository public class BlacklistRepository : BasicRepository<Blacklist>, IBlacklistRepository
@ -20,15 +20,15 @@ namespace NzbDrone.Core.Blacklisting
{ {
} }
public List<Blacklist> BlacklistedByTitle(int seriesId, string sourceTitle) public List<Blacklist> BlacklistedByTitle(int movieId, string sourceTitle)
{ {
return Query.Where(e => e.SeriesId == seriesId) return Query.Where(e => e.MovieId == movieId)
.AndWhere(e => e.SourceTitle.Contains(sourceTitle)); .AndWhere(e => e.SourceTitle.Contains(sourceTitle));
} }
public List<Blacklist> BlacklistedByTorrentInfoHash(int seriesId, string torrentInfoHash) public List<Blacklist> BlacklistedByTorrentInfoHash(int movieId, string torrentInfoHash)
{ {
return Query.Where(e => e.SeriesId == seriesId) return Query.Where(e => e.MovieId == movieId)
.AndWhere(e => e.TorrentInfoHash.Contains(torrentInfoHash)); .AndWhere(e => e.TorrentInfoHash.Contains(torrentInfoHash));
} }
@ -37,9 +37,14 @@ namespace NzbDrone.Core.Blacklisting
return Query.Where(b => b.SeriesId == seriesId); return Query.Where(b => b.SeriesId == seriesId);
} }
public List<Blacklist> BlacklistedByMovie(int movieId)
{
return Query.Where(b => b.MovieId == movieId);
}
protected override SortBuilder<Blacklist> GetPagedQuery(QueryBuilder<Blacklist> query, PagingSpec<Blacklist> pagingSpec) protected override SortBuilder<Blacklist> GetPagedQuery(QueryBuilder<Blacklist> query, PagingSpec<Blacklist> pagingSpec)
{ {
var baseQuery = query.Join<Blacklist, Series>(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id); var baseQuery = query.Join<Blacklist, Movie>(JoinType.Inner, h => h.Movie, (h, s) => h.MovieId == s.Id);
return base.GetPagedQuery(baseQuery, pagingSpec); return base.GetPagedQuery(baseQuery, pagingSpec);
} }

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Blacklisting
IExecute<ClearBlacklistCommand>, IExecute<ClearBlacklistCommand>,
IHandle<DownloadFailedEvent>, IHandle<DownloadFailedEvent>,
IHandleAsync<SeriesDeletedEvent> IHandleAsync<MovieDeletedEvent>
{ {
private readonly IBlacklistRepository _blacklistRepository; private readonly IBlacklistRepository _blacklistRepository;
@ -128,8 +128,9 @@ namespace NzbDrone.Core.Blacklisting
{ {
var blacklist = new Blacklist var blacklist = new Blacklist
{ {
SeriesId = message.SeriesId, SeriesId = 0,
EpisodeIds = message.EpisodeIds, EpisodeIds = message.EpisodeIds,
MovieId = message.MovieId,
SourceTitle = message.SourceTitle, SourceTitle = message.SourceTitle,
Quality = message.Quality, Quality = message.Quality,
Date = DateTime.UtcNow, Date = DateTime.UtcNow,
@ -144,9 +145,9 @@ namespace NzbDrone.Core.Blacklisting
_blacklistRepository.Insert(blacklist); _blacklistRepository.Insert(blacklist);
} }
public void HandleAsync(SeriesDeletedEvent message) public void HandleAsync(MovieDeletedEvent message)
{ {
var blacklisted = _blacklistRepository.BlacklistedBySeries(message.Series.Id); var blacklisted = _blacklistRepository.BlacklistedByMovie(message.Movie.Id);
_blacklistRepository.DeleteMany(blacklisted); _blacklistRepository.DeleteMany(blacklisted);
} }

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System.Data;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(119)]
public class add_youtube_trailer_id : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Movies").AddColumn("YouTubeTrailerId").AsString().Nullable();
}
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System.Data;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(120)]
public class add_studio : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Movies").AddColumn("Studio").AsString().Nullable();
}
}
}

@ -0,0 +1,67 @@
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System.Text;
using System.Text.RegularExpressions;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(121)]
public class update_filedate_config : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(SetTitleSlug);
}
private void SetTitleSlug(IDbConnection conn, IDbTransaction tran)
{
using (IDbCommand getSeriesCmd = conn.CreateCommand())
{
getSeriesCmd.Transaction = tran;
getSeriesCmd.CommandText = @"SELECT Id, Value FROM Config WHERE Key = 'filedate'";
using (IDataReader seriesReader = getSeriesCmd.ExecuteReader())
{
while (seriesReader.Read())
{
var id = seriesReader.GetInt32(0);
var value = seriesReader.GetString(1);
using (IDbCommand updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE Config SET Value = 'Release' WHERE Id = ?";
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
public static string ToUrlSlug(string value)
{
//First to lower case
value = value.ToLowerInvariant();
//Remove all accents
var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value);
value = Encoding.ASCII.GetString(bytes);
//Replace spaces
value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled);
//Remove invalid chars
value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled);
//Trim dashes from end
value = value.Trim('-', '_');
//Replace double occurences of - or _
value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled);
return value;
}
}
}

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System.Data;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(122)]
public class add_movieid_to_blacklist : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Blacklist").AddColumn("MovieId").AsInt32().Nullable().WithDefaultValue(0);
Alter.Table("Blacklist").AlterColumn("SeriesId").AsInt32().Nullable();
Alter.Table("Blacklist").AlterColumn("EpisodeIds").AsString().Nullable();
}
}
}

@ -68,16 +68,10 @@ namespace NzbDrone.Core.DecisionEngine
private int CompareProtocol(DownloadDecision x, DownloadDecision y) private int CompareProtocol(DownloadDecision x, DownloadDecision y)
{ {
var result = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode =>
{
var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Series.Tags);
var downloadProtocol = remoteEpisode.Release.DownloadProtocol;
return downloadProtocol == delayProfile.PreferredProtocol;
});
if (x.IsForMovie) if (x.IsForMovie)
{ {
result = CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => return CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode =>
{ {
var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Movie.Tags); var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Movie.Tags);
var downloadProtocol = remoteEpisode.Release.DownloadProtocol; var downloadProtocol = remoteEpisode.Release.DownloadProtocol;
@ -85,6 +79,15 @@ namespace NzbDrone.Core.DecisionEngine
}); });
} }
var result = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode =>
{
var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Series.Tags);
var downloadProtocol = remoteEpisode.Release.DownloadProtocol;
return downloadProtocol == delayProfile.PreferredProtocol;
});
return result; return result;
} }
@ -125,8 +128,8 @@ namespace NzbDrone.Core.DecisionEngine
private int CompareAgeIfUsenet(DownloadDecision x, DownloadDecision y) private int CompareAgeIfUsenet(DownloadDecision x, DownloadDecision y)
{ {
if (x.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Usenet || if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Usenet ||
y.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Usenet) y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Usenet)
{ {
return 0; return 0;
} }

@ -32,7 +32,7 @@ namespace NzbDrone.Core.DecisionEngine
public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports) public List<DownloadDecision> GetRssDecision(List<ReleaseInfo> reports)
{ {
return GetDecisions(reports).ToList(); return GetMovieDecisions(reports).ToList();
} }
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase) public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
@ -83,7 +83,7 @@ namespace NzbDrone.Core.DecisionEngine
{ {
if (parsedEpisodeInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace()) if (parsedEpisodeInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace())
{ {
remoteEpisode.DownloadAllowed = false; remoteEpisode.DownloadAllowed = true;
decision = new DownloadDecision(remoteEpisode, new Rejection("Hardcoded subs found: " + parsedEpisodeInfo.Quality.HardcodedSubs)); decision = new DownloadDecision(remoteEpisode, new Rejection("Hardcoded subs found: " + parsedEpisodeInfo.Quality.HardcodedSubs));
} }
else else

@ -34,11 +34,11 @@ namespace NzbDrone.Core.DecisionEngine
public List<DownloadDecision> PrioritizeDecisionsForMovies(List<DownloadDecision> decisions) public List<DownloadDecision> PrioritizeDecisionsForMovies(List<DownloadDecision> decisions)
{ {
return decisions.Where(c => c.RemoteMovie.Movie != null) return decisions.Where(c => c.RemoteMovie.Movie != null)
/*.GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) => .GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) =>
{ {
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService)); return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService));
}) })
.SelectMany(c => c)*/ .SelectMany(c => c)
.Union(decisions.Where(c => c.RemoteMovie.Movie == null)) .Union(decisions.Where(c => c.RemoteMovie.Movie == null))
.ToList(); .ToList();
} }

@ -33,7 +33,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
{ {
throw new NotImplementedException(); if (_blacklistService.Blacklisted(subject.Movie.Id, subject.Release))
{
_logger.Debug("{0} is blacklisted, rejecting.", subject.Release.Title);
return Decision.Reject("Release is blacklisted");
}
return Decision.Accept(); return Decision.Accept();
} }

@ -32,9 +32,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents) protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents)
{ {
var category = Settings.TvCategory; var category = Settings.TvCategory;
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); var addpaused = Settings.AddPaused;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, addpaused, Settings);
if (response == null) if (response == null)
{ {
@ -47,9 +50,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{ {
var category = Settings.TvCategory; // TODO: Update this to MovieCategory? var category = Settings.TvCategory; // TODO: Update this to MovieCategory?
var priority = Settings.RecentTvPriority; var priority = Settings.RecentTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); var addpaused = Settings.AddPaused;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, addpaused, Settings);
if(response == null) if(response == null)
{ {

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
public interface INzbgetProxy public interface INzbgetProxy
{ {
string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings); string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings);
NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings); NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings);
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings); List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings); List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings);
@ -45,12 +45,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return version >= minimumVersion; return version >= minimumVersion;
} }
public string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings) public string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings)
{ {
if (HasVersion(16, settings)) if (HasVersion(16, settings))
{ {
var droneId = Guid.NewGuid().ToString().Replace("-", ""); var droneId = Guid.NewGuid().ToString().Replace("-", "");
var response = ProcessRequest<int>(settings, "append", title, nzbData, category, priority, false, false, string.Empty, 0, "all", new string[] { "drone", droneId }); var response = ProcessRequest<int>(settings, "append", title, nzbData, category, priority, false, addpaused, string.Empty, 0, "all", new string[] { "drone", droneId });
if (response <= 0) if (response <= 0)
{ {
return null; return null;

@ -57,6 +57,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)] [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));

@ -124,7 +124,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
if (false) if (SupportsMovieSearch)
{ {
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie",
string.Format("&imdbid={0}", searchCriteria.Movie.ImdbId.Substring(2)))); //strip off the "tt" - VERY HACKY string.Format("&imdbid={0}", searchCriteria.Movie.ImdbId.Substring(2)))); //strip off the "tt" - VERY HACKY

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
@ -36,11 +37,10 @@ namespace NzbDrone.Core.Indexers.TorrentPotato
torrentInfo.Size = (long)torrent.size*1000*1000; torrentInfo.Size = (long)torrent.size*1000*1000;
torrentInfo.DownloadUrl = torrent.download_url; torrentInfo.DownloadUrl = torrent.download_url;
torrentInfo.InfoUrl = torrent.details_url; torrentInfo.InfoUrl = torrent.details_url;
torrentInfo.PublishDate = new System.DateTime(); torrentInfo.PublishDate = torrent.publish_date.ToUniversalTime();
torrentInfo.Seeders = torrent.seeders; torrentInfo.Seeders = torrent.seeders;
torrentInfo.Peers = torrent.leechers + torrent.seeders; torrentInfo.Peers = torrent.leechers + torrent.seeders;
torrentInfo.Freeleech = torrent.freeleech; torrentInfo.Freeleech = torrent.freeleech;
torrentInfo.PublishDate = torrent.publishdate.ToUniversalTime();
results.Add(torrentInfo); results.Add(torrentInfo);
} }

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Indexers.TorrentPotato
public int size { get; set; } public int size { get; set; }
public int leechers { get; set; } public int leechers { get; set; }
public int seeders { get; set; } public int seeders { get; set; }
public DateTime publishdate { get; set; } public DateTime publish_date { get; set; }
} }
} }

@ -39,7 +39,13 @@ namespace NzbDrone.Core.Indexers.Torznab
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
{ {
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
if (GetImdbId(item) != null)
{
if (torrentInfo != null)
{
torrentInfo.ImdbId = int.Parse(GetImdbId(item).Substring(2)); torrentInfo.ImdbId = int.Parse(GetImdbId(item).Substring(2));
}
}
return torrentInfo; return torrentInfo;
} }

@ -64,9 +64,9 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 0.25f, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 0.25f, TypeName = typeof(CheckForFinishedDownloadCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, // new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(RefreshMovieCommand).FullName},
new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName},
new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName}, new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName},
@ -80,6 +80,7 @@ namespace NzbDrone.Core.Jobs
{ {
Interval = _configService.DownloadedEpisodesScanInterval, Interval = _configService.DownloadedEpisodesScanInterval,
TypeName = typeof(DownloadedEpisodesScanCommand).FullName TypeName = typeof(DownloadedEpisodesScanCommand).FullName
//TypeName = typeof(DownloadedMovieScanCommand).FullName
}, },
}; };

@ -1,5 +1,8 @@
using NzbDrone.Common.Disk; using System;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using System.Drawing;
using NLog;
namespace NzbDrone.Core.MediaCover namespace NzbDrone.Core.MediaCover
{ {
@ -12,11 +15,13 @@ namespace NzbDrone.Core.MediaCover
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public CoverAlreadyExistsSpecification(IDiskProvider diskProvider, IHttpClient httpClient) public CoverAlreadyExistsSpecification(IDiskProvider diskProvider, IHttpClient httpClient, Logger logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger;
} }
public bool AlreadyExists(string url, string path) public bool AlreadyExists(string url, string path)
@ -26,9 +31,31 @@ namespace NzbDrone.Core.MediaCover
return false; return false;
} }
if (!IsValidGDIPlusImage(path))
{
_diskProvider.DeleteFile(path);
return false;
}
var headers = _httpClient.Head(new HttpRequest(url)).Headers; var headers = _httpClient.Head(new HttpRequest(url)).Headers;
var fileSize = _diskProvider.GetFileSize(path); var fileSize = _diskProvider.GetFileSize(path);
return fileSize == headers.ContentLength; return fileSize == headers.ContentLength;
} }
private bool IsValidGDIPlusImage(string filename)
{
try
{
using (var bmp = new Bitmap(filename))
{
}
return true;
}
catch (Exception ex)
{
_logger.Debug(ex, "Corrupted image found at: {0}. Redownloading...", filename);
return false;
}
}
} }
} }

@ -114,7 +114,7 @@ namespace NzbDrone.Core.MediaCover
} }
} }
private void EnsureCovers(Movie movie) private void EnsureCovers(Movie movie, int retried = 0)
{ {
foreach (var cover in movie.Images) foreach (var cover in movie.Images)
{ {
@ -130,7 +130,25 @@ namespace NzbDrone.Core.MediaCover
} }
catch (WebException e) catch (WebException e)
{ {
_logger.Warn(string.Format("Couldn't download media cover for {0}. {1}", movie, e.Message)); if (e.Status == WebExceptionStatus.ProtocolError)
{
_logger.Warn(e, "Server returned different code than 200. The poster is probably not available yet.");
return;
}
_logger.Warn(e, string.Format("Couldn't download media cover for {0}. {1}", movie, e.Message));
if (retried < 3)
{
retried = +1;
_logger.Warn("Retrying for the {0}. time in ten seconds.", retried);
System.Threading.Thread.Sleep(10*1000);
EnsureCovers(movie, retried);
}
else
{
_logger.Warn(e, "Couldn't download media cover even after retrying five times :(.");
}
} }
catch (Exception e) catch (Exception e)
{ {

@ -68,22 +68,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{ {
//check if already imported //check if already imported
if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie) if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie)
.Select(e => e.Id).Contains(localMovie.Movie.Id)) .Select(m => m.Id).Contains(localMovie.Movie.Id))
{ {
importResults.Add(new ImportResult(importDecision, "Movie has already been imported")); importResults.Add(new ImportResult(importDecision, "Movie has already been imported"));
continue; continue;
} }
var episodeFile = new MovieFile(); var movieFile = new MovieFile();
episodeFile.DateAdded = DateTime.UtcNow; movieFile.DateAdded = DateTime.UtcNow;
episodeFile.MovieId = localMovie.Movie.Id; movieFile.MovieId = localMovie.Movie.Id;
episodeFile.Path = localMovie.Path.CleanFilePath(); movieFile.Path = localMovie.Path.CleanFilePath();
episodeFile.Size = _diskProvider.GetFileSize(localMovie.Path); movieFile.Size = _diskProvider.GetFileSize(localMovie.Path);
episodeFile.Quality = localMovie.Quality; movieFile.Quality = localMovie.Quality;
episodeFile.MediaInfo = localMovie.MediaInfo; movieFile.MediaInfo = localMovie.MediaInfo;
episodeFile.Movie = localMovie.Movie; movieFile.Movie = localMovie.Movie;
episodeFile.ReleaseGroup = localMovie.ParsedMovieInfo.ReleaseGroup; movieFile.ReleaseGroup = localMovie.ParsedMovieInfo.ReleaseGroup;
episodeFile.Edition = localMovie.ParsedMovieInfo.Edition; movieFile.Edition = localMovie.ParsedMovieInfo.Edition;
bool copyOnly; bool copyOnly;
switch (importMode) switch (importMode)
@ -102,17 +102,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (newDownload) if (newDownload)
{ {
episodeFile.SceneName = GetSceneName(downloadClientItem, localMovie); movieFile.SceneName = GetSceneName(downloadClientItem, localMovie);
var moveResult = _episodeFileUpgrader.UpgradeMovieFile(episodeFile, localMovie, copyOnly); var moveResult = _episodeFileUpgrader.UpgradeMovieFile(movieFile, localMovie, copyOnly); //TODO: Check if this works
oldFiles = moveResult.OldFiles; oldFiles = moveResult.OldFiles;
} }
else else
{ {
episodeFile.RelativePath = localMovie.Movie.Path.GetRelativePath(episodeFile.Path); movieFile.RelativePath = localMovie.Movie.Path.GetRelativePath(movieFile.Path);
} }
_mediaFileService.Add(episodeFile); _mediaFileService.Add(movieFile);
importResults.Add(new ImportResult(importDecision)); importResults.Add(new ImportResult(importDecision));
if (newDownload) if (newDownload)
@ -122,22 +122,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (downloadClientItem != null) if (downloadClientItem != null)
{ {
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly));
} }
else else
{ {
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload)); _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload));
} }
if (newDownload) if (newDownload)
{ {
_eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, episodeFile, oldFiles)); _eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, movieFile, oldFiles));
} }
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Warn(e, "Couldn't import episode " + localMovie); _logger.Warn(e, "Couldn't import movie " + localMovie);
importResults.Add(new ImportResult(importDecision, "Failed to import episode")); importResults.Add(new ImportResult(importDecision, "Failed to import movie"));
} }
} }

@ -3,7 +3,7 @@
public enum FileDateType public enum FileDateType
{ {
None = 0, None = 0,
LocalAirDate = 1, Cinemas = 1,
UtcAirDate = 2 Release = 2
} }
} }

@ -111,11 +111,11 @@ namespace NzbDrone.Core.MediaFiles
public List<string> FilterExistingFiles(List<string> files, Movie movie) public List<string> FilterExistingFiles(List<string> files, Movie movie)
{ {
var seriesFiles = GetFilesBySeries(movie.Id).Select(f => Path.Combine(movie.Path, f.RelativePath)).ToList(); var movieFiles = GetFilesByMovie(movie.Id).Select(f => Path.Combine(movie.Path, f.RelativePath)).ToList();
if (!seriesFiles.Any()) return files; if (!movieFiles.Any()) return files;
return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); return files.Except(movieFiles, PathEqualityComparer.Instance).ToList();
} }
public EpisodeFile Get(int id) public EpisodeFile Get(int id)

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using NLog; using NLog;
@ -18,14 +18,17 @@ namespace NzbDrone.Core.MediaFiles
public class MediaFileTableCleanupService : IMediaFileTableCleanupService public class MediaFileTableCleanupService : IMediaFileTableCleanupService
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IMovieService _movieService;
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly Logger _logger; private readonly Logger _logger;
public MediaFileTableCleanupService(IMediaFileService mediaFileService, public MediaFileTableCleanupService(IMediaFileService mediaFileService,
IMovieService movieService,
IEpisodeService episodeService, IEpisodeService episodeService,
Logger logger) Logger logger)
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_movieService = movieService;
_episodeService = episodeService; _episodeService = episodeService;
_logger = logger; _logger = logger;
} }
@ -89,61 +92,39 @@ namespace NzbDrone.Core.MediaFiles
public void Clean(Movie movie, List<string> filesOnDisk) public void Clean(Movie movie, List<string> filesOnDisk)
{ {
var movieFiles = _mediaFileService.GetFilesByMovie(movie.Id);
//TODO: Update implementation for movies.
var seriesFiles = _mediaFileService.GetFilesBySeries(movie.Id);
var episodes = _episodeService.GetEpisodeBySeries(movie.Id);
var filesOnDiskKeys = new HashSet<string>(filesOnDisk, PathEqualityComparer.Instance); var filesOnDiskKeys = new HashSet<string>(filesOnDisk, PathEqualityComparer.Instance);
foreach (var seriesFile in seriesFiles) foreach(var movieFile in movieFiles)
{ {
var episodeFile = seriesFile; var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
var episodeFilePath = Path.Combine(movie.Path, episodeFile.RelativePath);
try try
{ {
if (!filesOnDiskKeys.Contains(episodeFilePath)) if (!filesOnDiskKeys.Contains(movieFilePath))
{ {
_logger.Debug("File [{0}] no longer exists on disk, removing from db", episodeFilePath); _logger.Debug("File [{0}] no longer exists on disk, removing from db", movieFilePath);
_mediaFileService.Delete(seriesFile, DeleteMediaFileReason.MissingFromDisk); _mediaFileService.Delete(movieFile, DeleteMediaFileReason.MissingFromDisk);
continue; continue;
} }
if (episodes.None(e => e.EpisodeFileId == episodeFile.Id)) //var localMovie = _parsingService.GetLocalMovie(movieFile.Path, movie);
{
_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 (localMovie == null)
//
// if (localEpsiode == null || episodes.Count != localEpsiode.Episodes.Count)
//{ //{
// _logger.Debug("File [{0}] parsed episodes has changed, removing from db", episodeFile.Path); // _logger.Debug("File [{0}] parsed episodes has changed, removing from db", localMovie.Path);
// _mediaFileService.Delete(episodeFile); // _mediaFileService.Delete(localMovie);
// continue; // continue;
//} //}
} }
catch (Exception ex) catch (Exception ex)
{ {
var errorMessage = string.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id); var errorMessage = string.Format("Unable to cleanup MovieFile in DB: {0}", movieFile.Id);
_logger.Error(ex, errorMessage); _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);
}
}
} }
} }
} }

@ -48,7 +48,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? AudioChannels - 1 + 0.1m : AudioChannels; return AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? AudioChannels - 1 + 0.1m : AudioChannels;
} }
return AudioChannelPositions.Split('/').Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); decimal channels = 0;
decimal.TryParse(AudioChannelPositions.Split('/').First(), out channels);
return channels;
} }
} }
} }

@ -49,7 +49,7 @@ namespace NzbDrone.Core.MediaFiles
switch (_configService.FileDate) switch (_configService.FileDate)
{ {
case FileDateType.LocalAirDate: case FileDateType.Release:
{ {
var airDate = episodes.First().AirDate; var airDate = episodes.First().AirDate;
var airTime = series.AirTime; var airTime = series.AirTime;
@ -62,7 +62,7 @@ namespace NzbDrone.Core.MediaFiles
return ChangeFileDateToLocalAirDate(episodeFilePath, airDate, airTime); return ChangeFileDateToLocalAirDate(episodeFilePath, airDate, airTime);
} }
case FileDateType.UtcAirDate: case FileDateType.Cinemas:
{ {
var airDateUtc = episodes.First().AirDateUtc; var airDateUtc = episodes.First().AirDateUtc;

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles
} }
public class UpdateMovieFileService : IUpdateMovieFileService, public class UpdateMovieFileService : IUpdateMovieFileService,
IHandle<SeriesScannedEvent> IHandle<MovieScannedEvent>
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
@ -47,17 +47,67 @@ namespace NzbDrone.Core.MediaFiles
{ {
var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath); var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
switch (_configService.FileDate)
{
case FileDateType.Release:
{
var airDate = movie.PhysicalRelease;
if (airDate == null)
{
return false;
}
return ChangeFileDate(movieFilePath, airDate.Value);
}
case FileDateType.Cinemas:
{
var airDate = movie.InCinemas;
if (airDate == null)
{
return false;
}
return ChangeFileDate(movieFilePath, airDate.Value);
}
}
return false;
}
private bool ChangeFileDate(string filePath, DateTime date)
{
DateTime oldDateTime = _diskProvider.FileGetLastWrite(filePath);
if (!DateTime.Equals(date, oldDateTime))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, date);
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldDateTime, date);
return true;
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]");
}
}
return false; return false;
} }
public void Handle(SeriesScannedEvent message) public void Handle(MovieScannedEvent message)
{ {
if (_configService.FileDate == FileDateType.None) if (_configService.FileDate == FileDateType.None)
{ {
return; return;
} }
/* var movies = _movieService.MoviesWithFiles(message.Series.Id); var movies = _movieService.MoviesWithFiles(message.Movie.Id);
var movieFiles = new List<MovieFile>(); var movieFiles = new List<MovieFile>();
var updated = new List<MovieFile>(); var updated = new List<MovieFile>();
@ -69,7 +119,7 @@ namespace NzbDrone.Core.MediaFiles
movieFiles.Add(movieFile); movieFiles.Add(movieFile);
if (ChangeFileDate(movieFile, message.Series, moviesInFile)) if (ChangeFileDate(movieFile, message.Movie))
{ {
updated.Add(movieFile); updated.Add(movieFile);
} }
@ -77,13 +127,13 @@ namespace NzbDrone.Core.MediaFiles
if (updated.Any()) if (updated.Any())
{ {
_logger.ProgressDebug("Changed file date for {0} files of {1} in {2}", updated.Count, movieFiles.Count, message.Series.Title); _logger.ProgressDebug("Changed file date for {0} files of {1} in {2}", updated.Count, movieFiles.Count, message.Movie.Title);
} }
else else
{ {
_logger.ProgressDebug("No file dates changed for {0}", message.Series.Title); _logger.ProgressDebug("No file dates changed for {0}", message.Movie.Title);
}*/ }
} }
private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime) private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime)

@ -38,10 +38,13 @@ namespace NzbDrone.Core.MediaFiles
public MovieFileMoveResult UpgradeMovieFile(MovieFile episodeFile, LocalMovie localEpisode, bool copyOnly = false) public MovieFileMoveResult UpgradeMovieFile(MovieFile episodeFile, LocalMovie localEpisode, bool copyOnly = false)
{ {
_logger.Trace("Upgrading existing episode file.");
var moveFileResult = new MovieFileMoveResult(); var moveFileResult = new MovieFileMoveResult();
localEpisode.Movie.MovieFile.LazyLoad();
var existingFile = localEpisode.Movie.MovieFile; var existingFile = localEpisode.Movie.MovieFile;
existingFile.LazyLoad();
if (existingFile.IsLoaded) if (existingFile.IsLoaded && existingFile.Value != null)
{ {
var file = existingFile.Value; var file = existingFile.Value;
var episodeFilePath = Path.Combine(localEpisode.Movie.Path, file.RelativePath); var episodeFilePath = Path.Combine(localEpisode.Movie.Path, file.RelativePath);
@ -55,6 +58,10 @@ namespace NzbDrone.Core.MediaFiles
moveFileResult.OldFiles.Add(file); moveFileResult.OldFiles.Add(file);
_mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade);
} }
else
{
_logger.Warn("The existing movie file was not lazy loaded.");
}

@ -66,6 +66,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public int vote_count { get; set; } public int vote_count { get; set; }
public AlternativeTitles alternative_titles { get; set; } public AlternativeTitles alternative_titles { get; set; }
public ReleaseDatesResource release_dates { get; set; } public ReleaseDatesResource release_dates { get; set; }
public VideosResource videos { get; set; }
} }
public class ReleaseDatesResource public class ReleaseDatesResource
@ -130,4 +131,21 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public string iso_3166_1 { get; set; } public string iso_3166_1 { get; set; }
public string title { get; set; } public string title { get; set; }
} }
public class VideosResource
{
public List<Video> results { get; set; }
}
public class Video
{
public string id { get; set; }
public string iso_639_1 { get; set; }
public string iso_3166_1 { get; set; }
public string key { get; set; }
public string name { get; set; }
public string site { get; set; }
public string size { get; set; }
public string type { get; set; }
}
} }

@ -73,7 +73,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
.SetSegment("route", "movie") .SetSegment("route", "movie")
.SetSegment("id", TmdbId.ToString()) .SetSegment("id", TmdbId.ToString())
.SetSegment("secondaryRoute", "") .SetSegment("secondaryRoute", "")
.AddQueryParam("append_to_response", "alternative_titles,release_dates") .AddQueryParam("append_to_response", "alternative_titles,release_dates,videos")
.AddQueryParam("country", "US") .AddQueryParam("country", "US")
.Build(); .Build();
@ -89,9 +89,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.TmdbId = TmdbId; movie.TmdbId = TmdbId;
movie.ImdbId = resource.imdb_id; movie.ImdbId = resource.imdb_id;
movie.Title = resource.title; movie.Title = resource.title;
movie.TitleSlug = ToUrlSlug(movie.Title); movie.TitleSlug = ToUrlSlug(resource.title);
movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title); movie.CleanTitle = Parser.Parser.CleanSeriesTitle(resource.title);
movie.SortTitle = Parser.Parser.NormalizeTitle(movie.Title); movie.SortTitle = Parser.Parser.NormalizeTitle(resource.title);
movie.Overview = resource.overview; movie.Overview = resource.overview;
movie.Website = resource.homepage; movie.Website = resource.homepage;
if (resource.release_date.IsNotNullOrWhiteSpace()) if (resource.release_date.IsNotNullOrWhiteSpace())
@ -150,6 +150,29 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.Status = MovieStatusType.Announced; movie.Status = MovieStatusType.Announced;
} }
if (resource.videos != null)
{
foreach (Video video in resource.videos.results)
{
if (video.type == "Trailer" && video.site == "YouTube")
{
if (video.key != null)
{
movie.YouTubeTrailerId = video.key;
break;
}
}
}
}
if (resource.production_companies != null)
{
if (resource.production_companies.Any())
{
movie.Studio = resource.production_companies[0].name;
}
}
return movie; return movie;
} }
@ -186,6 +209,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
var lowerTitle = title.ToLower(); var lowerTitle = title.ToLower();
lowerTitle = lowerTitle.Replace(".", "");
var parserResult = Parser.Parser.ParseMovieTitle(title, true); var parserResult = Parser.Parser.ParseMovieTitle(title, true);
var yearTerm = ""; var yearTerm = "";
@ -193,7 +218,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
if (parserResult != null && parserResult.MovieTitle != title) if (parserResult != null && parserResult.MovieTitle != title)
{ {
//Parser found something interesting! //Parser found something interesting!
lowerTitle = parserResult.MovieTitle.ToLower(); lowerTitle = parserResult.MovieTitle.ToLower().Replace(".", " "); //TODO Update so not every period gets replaced (e.g. R.I.P.D.)
if (parserResult.Year > 1800) if (parserResult.Year > 1800)
{ {
yearTerm = parserResult.Year.ToString(); yearTerm = parserResult.Year.ToString();
@ -326,25 +351,19 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
imdbMovie.SortTitle = Parser.Parser.NormalizeTitle(result.title); imdbMovie.SortTitle = Parser.Parser.NormalizeTitle(result.title);
imdbMovie.Title = result.title; imdbMovie.Title = result.title;
string titleSlug = ToUrlSlug(result.title); imdbMovie.TitleSlug = ToUrlSlug(result.title);
imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-");
if (result.release_date.IsNotNullOrWhiteSpace()) if (result.release_date.IsNotNullOrWhiteSpace())
{ {
imdbMovie.Year = DateTime.Parse(result.release_date).Year; imdbMovie.Year = DateTime.Parse(result.release_date).Year;
} }
//var slugResult = _movieService.FindByTitleSlug(imdbMovie.TitleSlug);
//if (slugResult != null) imdbMovie.TitleSlug += "-" + imdbMovie.Year;
//{
// _logger.Debug("Movie with this title slug already exists. Adding year...");
//}
imdbMovie.TitleSlug += "-" + imdbMovie.Year.ToString();
imdbMovie.Images = new List<MediaCover.MediaCover>(); imdbMovie.Images = new List<MediaCover.MediaCover>();
imdbMovie.Overview = result.overview; imdbMovie.Overview = result.overview;
try try
{ {
string url = result.poster_path;
var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster); var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster);
imdbMovie.Images.Add(imdbPoster); imdbMovie.Images.Add(imdbPoster);
} }

@ -2,7 +2,7 @@
{ {
public enum PushoverPriority public enum PushoverPriority
{ {
Silent = -1, Silent = -2,
Quiet = -1, Quiet = -1,
Normal = 0, Normal = 0,
High = 1, High = 1,

@ -204,6 +204,10 @@
<Compile Include="Datastore\Migration\002_remove_tvrage_imdb_unique_constraint.cs" /> <Compile Include="Datastore\Migration\002_remove_tvrage_imdb_unique_constraint.cs" />
<Compile Include="Datastore\Migration\003_remove_clean_title_from_scene_mapping.cs" /> <Compile Include="Datastore\Migration\003_remove_clean_title_from_scene_mapping.cs" />
<Compile Include="Datastore\Migration\004_updated_history.cs" /> <Compile Include="Datastore\Migration\004_updated_history.cs" />
<Compile Include="Datastore\Migration\122_add_movieid_to_blacklist.cs" />
<Compile Include="Datastore\Migration\121_update_filedate_config.cs" />
<Compile Include="Datastore\Migration\120_add_studio_to_table.cs" />
<Compile Include="Datastore\Migration\119_add_youtube_trailer_id_table .cs" />
<Compile Include="Datastore\Migration\118_update_movie_slug.cs" /> <Compile Include="Datastore\Migration\118_update_movie_slug.cs" />
<Compile Include="Datastore\Migration\117_update_movie_file.cs" /> <Compile Include="Datastore\Migration\117_update_movie_file.cs" />
<Compile Include="Datastore\Migration\116_update_movie_sorttitle_again.cs" /> <Compile Include="Datastore\Migration\116_update_movie_sorttitle_again.cs" />

@ -58,7 +58,7 @@ namespace NzbDrone.Core.Organizer
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})", public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean)?Title)\})", public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean)?Title(The)?)\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled);
@ -161,6 +161,7 @@ namespace NzbDrone.Core.Organizer
AddMovieTokens(tokenHandlers, movie); AddMovieTokens(tokenHandlers, movie);
AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year
AddImdbIdTokens(tokenHandlers, movie.ImdbId);
AddQualityTokens(tokenHandlers, movie, movieFile); AddQualityTokens(tokenHandlers, movie, movieFile);
AddMediaInfoTokens(tokenHandlers, movieFile); AddMediaInfoTokens(tokenHandlers, movieFile);
AddMovieFileTokens(tokenHandlers, movieFile); AddMovieFileTokens(tokenHandlers, movieFile);
@ -301,6 +302,7 @@ namespace NzbDrone.Core.Organizer
AddMovieTokens(tokenHandlers, movie); AddMovieTokens(tokenHandlers, movie);
AddReleaseDateTokens(tokenHandlers, movie.Year); AddReleaseDateTokens(tokenHandlers, movie.Year);
AddImdbIdTokens(tokenHandlers, movie.ImdbId);
return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig));
} }
@ -314,11 +316,27 @@ namespace NzbDrone.Core.Organizer
return title; return title;
} }
public static string TitleThe(string title)
{
string[] prefixes = { "The ", "An ", "A " };
foreach (string prefix in prefixes)
{
int prefix_length = prefix.Length;
if (prefix.ToLower() == title.Substring(0, prefix_length).ToLower())
{
title = title.Substring(prefix_length) + ", " + prefix.Trim();
break;
}
}
return title.Trim();
}
public static string CleanFileName(string name, bool replace = true) public static string CleanFileName(string name, bool replace = true)
{ {
string result = name; string result = name;
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" }; string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" };
for (int i = 0; i < badCharacters.Length; i++) for (int i = 0; i < badCharacters.Length; i++)
{ {
@ -470,6 +488,7 @@ namespace NzbDrone.Core.Organizer
{ {
tokenHandlers["{Movie Title}"] = m => movie.Title; tokenHandlers["{Movie Title}"] = m => movie.Title;
tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title);
tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title);
} }
private void AddReleaseDateTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int releaseYear) private void AddReleaseDateTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int releaseYear)
@ -477,6 +496,11 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat?
} }
private void AddImdbIdTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, string imdbId)
{
tokenHandlers["{IMDb Id}"] = m => $"{imdbId}";
}
private void AddSeasonTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int seasonNumber) private void AddSeasonTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int seasonNumber)
{ {
tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat);
@ -508,6 +532,7 @@ namespace NzbDrone.Core.Organizer
{ {
tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile);
tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile);
//tokenHandlers["{IMDb Id}"] = m =>
tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr");
} }

@ -46,8 +46,9 @@ namespace NzbDrone.Core.Organizer
_movie = new Movie _movie = new Movie
{ {
Title = "Movie Title", Title = "The Movie Title",
Year = 2010 Year = 2010,
ImdbId = "tt0066921"
}; };
_standardSeries = new Series _standardSeries = new Series

@ -268,7 +268,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7})", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|576[ip]|720[ip]|1080[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)\s*", private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)\s*",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*", private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*",

@ -28,7 +28,7 @@ namespace NzbDrone.Core.Parser
)\b", )\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex HardcodedSubsRegex = new Regex(@"\b(?<hcsub>(\w+SUB))|(?<hc>(HC))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); private static readonly Regex HardcodedSubsRegex = new Regex(@"\b(?<hcsub>(\w+SUB)\b)|(?<hc>(HC))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>RawHD|1080i[-_. ]HDTV|Raw[-_. ]HD|MPEG[-_. ]?2)\b", private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>RawHD|1080i[-_. ]HDTV|Raw[-_. ]HD|MPEG[-_. ]?2)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);

@ -48,6 +48,8 @@ namespace NzbDrone.Core.Tv
public LazyLoaded<MovieFile> MovieFile { get; set; } public LazyLoaded<MovieFile> MovieFile { get; set; }
public int MovieFileId { get; set; } public int MovieFileId { get; set; }
public List<string> AlternativeTitles { get; set; } public List<string> AlternativeTitles { get; set; }
public string YouTubeTrailerId{ get; set; }
public string Studio { get; set; }
public bool HasFile => MovieFileId > 0; public bool HasFile => MovieFileId > 0;

@ -3,7 +3,9 @@ using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Datastore.Extensions;
using Marr.Data.QGen;
using NzbDrone.Core.MediaFiles;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@ -14,6 +16,9 @@ namespace NzbDrone.Core.Tv
Movie FindByTitle(string cleanTitle, int year); Movie FindByTitle(string cleanTitle, int year);
Movie FindByImdbId(string imdbid); Movie FindByImdbId(string imdbid);
Movie FindByTitleSlug(string slug); Movie FindByTitleSlug(string slug);
List<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
List<Movie> MoviesWithFiles(int movieId);
PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec);
List<Movie> GetMoviesByFileId(int fileId); List<Movie> GetMoviesByFileId(int fileId);
void SetFileId(int fileId, int movieId); void SetFileId(int fileId, int movieId);
} }
@ -119,5 +124,42 @@ namespace NzbDrone.Core.Tv
{ {
return Query.Where(m => m.TitleSlug == slug).FirstOrDefault(); return Query.Where(m => m.TitleSlug == slug).FirstOrDefault();
} }
public List<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored)
{
var query = Query.Where(m => m.InCinemas >= start && m.InCinemas <= end).OrWhere(m => m.PhysicalRelease >= start && m.PhysicalRelease <= end);
if (!includeUnmonitored)
{
query.AndWhere(e => e.Monitored);
}
return query.ToList();
}
public List<Movie> MoviesWithFiles(int movieId)
{
return Query.Join<Movie, MovieFile>(JoinType.Inner, m => m.MovieFile, (m, mf) => m.MovieFileId == mf.Id)
.Where(m => m.Id == movieId);
}
public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
{
pagingSpec.TotalRecords = GetMoviesWithoutFilesQuery(pagingSpec).GetRowCount();
pagingSpec.Records = GetMoviesWithoutFilesQuery(pagingSpec).ToList();
return pagingSpec;
}
public SortBuilder<Movie> GetMoviesWithoutFilesQuery(PagingSpec<Movie> pagingSpec)
{
return Query.Where(pagingSpec.FilterExpression)
.AndWhere(m => m.MovieFileId == 0)
.AndWhere(m => m.Status == MovieStatusType.Released)
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize);
}
} }
} }

@ -12,6 +12,7 @@ using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@ -26,12 +27,15 @@ namespace NzbDrone.Core.Tv
Movie FindByTitleInexact(string title); Movie FindByTitleInexact(string title);
Movie FindByTitleSlug(string slug); Movie FindByTitleSlug(string slug);
Movie GetMovieByFileId(int fileId); Movie GetMovieByFileId(int fileId);
List<Movie> GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec);
void DeleteMovie(int movieId, bool deleteFiles); void DeleteMovie(int movieId, bool deleteFiles);
List<Movie> GetAllMovies(); List<Movie> GetAllMovies();
Movie UpdateMovie(Movie movie); Movie UpdateMovie(Movie movie);
List<Movie> UpdateMovie(List<Movie> movie); List<Movie> UpdateMovie(List<Movie> movie);
bool MoviePathExists(string folder); bool MoviePathExists(string folder);
void RemoveAddOptions(Movie movie); void RemoveAddOptions(Movie movie);
List<Movie> MoviesWithFiles(int movieId);
} }
public class MovieService : IMovieService, IHandle<MovieFileAddedEvent>, public class MovieService : IMovieService, IHandle<MovieFileAddedEvent>,
@ -224,5 +228,24 @@ namespace NzbDrone.Core.Tv
{ {
return _movieRepository.FindByTitleSlug(slug); return _movieRepository.FindByTitleSlug(slug);
} }
public List<Movie> GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored)
{
var episodes = _movieRepository.MoviesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored);
return episodes;
}
public List<Movie> MoviesWithFiles(int movieId)
{
return _movieRepository.MoviesWithFiles(movieId);
}
public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
{
var movieResult = _movieRepository.MoviesWithoutFiles(pagingSpec);
return movieResult;
}
} }
} }

@ -84,6 +84,8 @@ namespace NzbDrone.Core.Tv
movie.AlternativeTitles = movieInfo.AlternativeTitles; movie.AlternativeTitles = movieInfo.AlternativeTitles;
movie.Year = movieInfo.Year; movie.Year = movieInfo.Year;
movie.PhysicalRelease = movieInfo.PhysicalRelease; movie.PhysicalRelease = movieInfo.PhysicalRelease;
movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId;
movie.Studio = movieInfo.Studio;
try try
{ {

@ -26,6 +26,12 @@ var QueueCollection = PageableCollection.extend({
}); });
}, },
findMovie : function(movieId) {
return _.find(this.fullCollection.models, function(queueModel) {
return queueModel.get('movie').id === movieId;
});
},
sortMappings : { sortMappings : {
series : { series : {
sortValue : function(model, attr) { sortValue : function(model, attr) {

@ -17,7 +17,8 @@ module.exports = Marionette.Layout.extend({
events : { events : {
'click .x-import' : '_importMovies', 'click .x-import' : '_importMovies',
'click .x-add-new' : '_addMovies' 'click .x-add-new' : '_addMovies',
'click .x-show-existing' : '_toggleExisting'
}, },
attributes : { attributes : {
@ -31,13 +32,20 @@ module.exports = Marionette.Layout.extend({
}); });
}, },
_toggleExisting : function(e) {
var showExisting = e.target.checked;
vent.trigger(vent.Commands.ShowExistingCommand, {
showExisting: showExisting
});
},
onShow : function() { onShow : function() {
this.workspace.show(new AddMoviesView()); this.workspace.show(new AddMoviesView());
}, },
_folderSelected : function(options) { _folderSelected : function(options) {
vent.trigger(vent.Commands.CloseModalCommand); vent.trigger(vent.Commands.CloseModalCommand);
//TODO: Fix this shit.
this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); this.workspace.show(new ExistingMoviesCollectionView({ model : options.model }));
}, },

@ -9,6 +9,33 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-12">
<div class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" style="margin-bottom: 0px;">
<label class="col-sm-3 control-label">Display Existing Movies</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input class="x-show-existing" type="checkbox" checked="checked" name="showExisting"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-info" title="Should Radarr display movies already in your collection?"/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div id="add-movies-workspace"></div> <div id="add-movies-workspace"></div>

@ -27,6 +27,7 @@ module.exports = Marionette.Layout.extend({
initialize : function(options) { initialize : function(options) {
console.log(options); console.log(options);
this.isExisting = options.isExisting; this.isExisting = options.isExisting;
this.collection = new AddMoviesCollection(); this.collection = new AddMoviesCollection();
@ -159,7 +160,7 @@ module.exports = Marionette.Layout.extend({
this.searchResult.show(new NotFoundView({ term : this.collection.term })); this.searchResult.show(new NotFoundView({ term : this.collection.term }));
} else { } else {
this.searchResult.show(this.resultCollectionView); this.searchResult.show(this.resultCollectionView);
if (!this.showingAll && this.isExisting) { if (!this.showingAll) {
this.ui.loadMore.show(); this.ui.loadMore.show();
} }
} }

@ -1,6 +1,7 @@
var Marionette = require('marionette'); var Marionette = require('marionette');
var AddMoviesView = require('../AddMoviesView'); var AddMoviesView = require('../AddMoviesView');
var UnmappedFolderCollection = require('./UnmappedFolderCollection'); var UnmappedFolderCollection = require('./UnmappedFolderCollection');
var vent = require('vent');
module.exports = Marionette.CompositeView.extend({ module.exports = Marionette.CompositeView.extend({
itemView : AddMoviesView, itemView : AddMoviesView,

@ -48,7 +48,7 @@ var Layout = Marionette.Layout.extend({
var self = this; var self = this;
var newDir = new RootFolderModel({ var newDir = new RootFolderModel({
Path : this.ui.pathInput.val() Path : this.ui.pathInput.val(),
}); });
this.bindToModelValidation(newDir); this.bindToModelValidation(newDir);

@ -31,6 +31,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" data-dismiss="modal">Close</button> <button class="btn" data-dismiss="modal">Close</button>
</div> </div>
</div> </div>

@ -1,12 +1,25 @@
var Marionette = require('marionette'); var Marionette = require('marionette');
var SearchResultView = require('./SearchResultView'); var SearchResultView = require('./SearchResultView');
var MoviesCollection = require('../Movies/MoviesCollection');
var vent = require('vent');
module.exports = Marionette.CollectionView.extend({ module.exports = Marionette.CollectionView.extend({
itemView : SearchResultView, itemView : SearchResultView,
initialize : function(options) { initialize : function(options) {
this.showExisting = true;
this.isExisting = options.isExisting; this.isExisting = options.isExisting;
this.showing = 5;
if (this.isExisting) {
this.showing = 1; this.showing = 1;
}
vent.on(vent.Commands.ShowExistingCommand, this._onExistingToggle.bind(this));
},
_onExistingToggle : function(data) {
this.showExisting = data.showExisting;
this.render();
}, },
showAll : function() { showAll : function() {
@ -34,8 +47,19 @@ module.exports = Marionette.CollectionView.extend({
}, },
appendHtml : function(collectionView, itemView, index) { appendHtml : function(collectionView, itemView, index) {
if (!this.isExisting || index < this.showing || index === 0) { var tmdbId = itemView.model.get('tmdbId');
var existingMovies = MoviesCollection.where({ tmdbId: tmdbId });
if(existingMovies.length > 0) {
if(this.showExisting) {
if (index < this.showing || index === 0) {
collectionView.$el.append(itemView.el); collectionView.$el.append(itemView.el);
} }
} }
} else {
if (index < this.showing || index === 0) {
collectionView.$el.append(itemView.el);
}
}
}
}); });

@ -43,7 +43,7 @@ var view = Marionette.ItemView.extend({
throw 'model is required'; throw 'model is required';
} }
console.log(this.route); //console.log(this.route);
this.templateHelpers = {}; this.templateHelpers = {};
this._configureTemplateHelpers(); this._configureTemplateHelpers();
@ -92,14 +92,12 @@ var view = Marionette.ItemView.extend({
_configureTemplateHelpers : function() { _configureTemplateHelpers : function() {
var existingMovies = MoviesCollection.where({ tmdbId : this.model.get('tmdbId') }); var existingMovies = MoviesCollection.where({ tmdbId : this.model.get('tmdbId') });
console.log(existingMovies);
if (existingMovies.length > 0) { if (existingMovies.length > 0) {
this.templateHelpers.existing = existingMovies[0].toJSON(); this.templateHelpers.existing = existingMovies[0].toJSON();
} }
this.templateHelpers.profiles = Profiles.toJSON(); this.templateHelpers.profiles = Profiles.toJSON();
console.log(this.model); //console.log(this.templateHelpers.isExisting);
console.log(this.templateHelpers.existing);
if (!this.model.get('isExisting')) { if (!this.model.get('isExisting')) {
this.templateHelpers.rootFolders = RootFolders.toJSON(); this.templateHelpers.rootFolders = RootFolders.toJSON();
} }
@ -185,8 +183,8 @@ var view = Marionette.ItemView.extend({
var self = this; var self = this;
var promise = this.model.save(); var promise = this.model.save();
console.log(this.model.save); //console.log(this.model.save);
console.log(promise); //console.log(promise);
if (searchForMovie) { if (searchForMovie) {
this.ui.addSearchButton.spinForPromise(promise); this.ui.addSearchButton.spinForPromise(promise);

@ -1,5 +1,5 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
var EpisodeModel = require('../Series/EpisodeModel'); var EpisodeModel = require('../Movies/MovieModel');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/calendar', url : window.NzbDrone.ApiRoot + '/calendar',
@ -7,7 +7,7 @@ module.exports = Backbone.Collection.extend({
tableName : 'calendar', tableName : 'calendar',
comparator : function(model) { comparator : function(model) {
var date = new Date(model.get('airDateUtc')); var date = new Date(model.get('inCinemas'));
var time = date.getTime(); var time = date.getTime();
return time; return time;
} }

@ -23,24 +23,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-3 control-label">Season Premiers Only</label>
<div class="col-sm-4">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="premiersOnly" class="form-control x-premiersOnly"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">Tags</label> <label class="col-sm-3 control-label">Tags</label>

@ -10,13 +10,13 @@
<div id="x-calendar" class="calendar"/> <div id="x-calendar" class="calendar"/>
<div class="legend calendar"> <div class="legend calendar">
<ul class='legend-labels'> <ul class='legend-labels'>
<li class="legend-label"><span class="premiere" title="Premiere episode hasn't aired yet"></span>Unaired Premiere</li> <li class="legend-label"><span class="premiere" title="This Movie is still in cinemas and hasn't been released yet. Only poor qualities will be available"></span>In Cinemas</li>
<li class="legend-label"><span class="primary" title="Episode hasn't aired yet"></span>Unaired</li> <li class="legend-label"><span class="primary" title="This movie has only been announced yet."></span>Announced</li>
<li class="legend-label"><span class="warning" title="Episode is currently airing"></span>On Air</li> <!--<li class="legend-label"><span class="warning" title="Episode is currently airing"></span>On Air</li>-->
<li class="legend-label"><span class="purple" title="Episode is currently downloading"></span>Downloading</li> <li class="legend-label"><span class="purple" title="Movie is currently downloading"></span>Downloading</li>
<li class="legend-label"><span class="danger" title="Episode file has not been found"></span>Missing</li> <li class="legend-label"><span class="danger" title="Movie file has not been found"></span>Missing</li>
<li class="legend-label"><span class="success" title="Episode was downloaded and sorted"></span>Downloaded</li> <li class="legend-label"><span class="success" title="Movie was downloaded and sorted"></span>Downloaded</li>
<li class="legend-label"><span class="unmonitored" title="Episode is unmonitored"></span>Unmonitored</li> <li class="legend-label"><span class="unmonitored" title="Movie is unmonitored"></span>Unmonitored</li>
</ul> </ul>
</div> </div>
</div> </div>

@ -98,10 +98,6 @@ module.exports = Marionette.ItemView.extend({
else if (event.model.get('unverifiedSceneNumbering')) { else if (event.model.get('unverifiedSceneNumbering')) {
this._addStatusIcon(element, 'icon-sonarr-form-warning', 'Scene number hasn\'t been verified yet.'); this._addStatusIcon(element, 'icon-sonarr-form-warning', 'Scene number hasn\'t been verified yet.');
} }
else if (event.model.get('series').seriesType === 'anime' && event.model.get('seasonNumber') > 0 && !event.model.has('absoluteEpisodeNumber')) {
this._addStatusIcon(element, 'icon-sonarr-form-warning', 'Episode does not have an absolute episode number');
}
}, },
_eventAfterAllRender : function () { _eventAfterAllRender : function () {
@ -135,11 +131,11 @@ module.exports = Marionette.ItemView.extend({
end : end, end : end,
unmonitored : this.showUnmonitored unmonitored : this.showUnmonitored
}, },
success : this._setEventData.bind(this) success : this._setEventData.bind(this, new Date(start), new Date(end))
}); });
}, },
_setEventData : function(collection) { _setEventData : function(startD, endD, collection) {
if (collection.length === 0) { if (collection.length === 0) {
return; return;
} }
@ -148,20 +144,24 @@ module.exports = Marionette.ItemView.extend({
var self = this; var self = this;
collection.each(function(model) { collection.each(function(model) {
var seriesTitle = model.get('series').title; var seriesTitle = model.get('title');
var start = model.get('airDateUtc'); var start = model.get('inCinemas');
var runtime = model.get('series').runtime; var startDate = new Date(start);
if (!(startD <= startDate && startDate <= endD)) {
start = model.get("physicalRelease");
}
var runtime = model.get('runtime');
var end = moment(start).add('minutes', runtime).toISOString(); var end = moment(start).add('minutes', runtime).toISOString();
var event = { var event = {
title : seriesTitle, title : seriesTitle,
start : moment(start), start : moment(start),
end : moment(end), end : moment(end),
allDay : false, allDay : true,
statusLevel : self._getStatusLevel(model, end), statusLevel : self._getStatusLevel(model, end),
downloading : QueueCollection.findEpisode(model.get('id')), downloading : QueueCollection.findMovie(model.get('id')),
model : model, model : model,
sortOrder : (model.get('seasonNumber') === 0 ? 1000000 : model.get('seasonNumber') * 10000) + model.get('episodeNumber') sortOrder : 0
}; };
events.push(event); events.push(event);
@ -172,11 +172,12 @@ module.exports = Marionette.ItemView.extend({
_getStatusLevel : function(element, endTime) { _getStatusLevel : function(element, endTime) {
var hasFile = element.get('hasFile'); var hasFile = element.get('hasFile');
var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('grabbed'); var downloading = QueueCollection.findMovie(element.get('id')) || element.get('grabbed');
var currentTime = moment(); var currentTime = moment();
var start = moment(element.get('airDateUtc')); var start = moment(element.get('inCinemas'));
var status = element.getStatus();
var end = moment(endTime); var end = moment(endTime);
var monitored = element.get('series').monitored && element.get('monitored'); var monitored = element.get('monitored');
var statusLevel = 'primary'; var statusLevel = 'primary';
@ -192,16 +193,16 @@ module.exports = Marionette.ItemView.extend({
statusLevel = 'unmonitored'; statusLevel = 'unmonitored';
} }
else if (currentTime.isAfter(start) && currentTime.isBefore(end)) { else if (status == "inCinemas") {
statusLevel = 'warning'; statusLevel = 'premiere';
} }
else if (start.isBefore(currentTime) && !hasFile) { else if (status == "released") {
statusLevel = 'danger'; statusLevel = 'danger';
} }
else if (element.get('episodeNumber') === 1) { else if (status == "announced") {
statusLevel = 'premiere'; statusLevel = 'primary';
} }
if (end.isBefore(currentTime.startOf('day'))) { if (end.isBefore(currentTime.startOf('day'))) {
@ -213,12 +214,15 @@ module.exports = Marionette.ItemView.extend({
_reloadCalendarEvents : function() { _reloadCalendarEvents : function() {
this.$el.fullCalendar('removeEvents'); this.$el.fullCalendar('removeEvents');
this._setEventData(this.collection); var view = this.$el.fullCalendar('getView');
var start = moment(view.start.toISOString()).toISOString();
var end = moment(view.end.toISOString()).toISOString();
this._setEventData(new Date(start), new Date(end), this.collection);
}, },
_getOptions : function() { _getOptions : function() {
var options = { var options = {
allDayDefault : false, allDayDefault : true,
weekMode : 'variable', weekMode : 'variable',
firstDay : UiSettings.get('firstDayOfWeek'), firstDay : UiSettings.get('firstDayOfWeek'),
timeFormat : 'h(:mm)t', timeFormat : 'h(:mm)t',
@ -227,41 +231,38 @@ module.exports = Marionette.ItemView.extend({
eventAfterAllRender : this._eventAfterAllRender.bind(this), eventAfterAllRender : this._eventAfterAllRender.bind(this),
windowResize : this._windowResize.bind(this), windowResize : this._windowResize.bind(this),
eventClick : function(event) { eventClick : function(event) {
vent.trigger(vent.Commands.ShowEpisodeDetails, { episode : event.model }); //vent.trigger(vent.Commands.ShowMovieDetails, { movie : event.model });
window.location.href = "movies/"+event.model.get("titleSlug");
} }
}; };
if ($(window).width() < 768) { if ($(window).width() < 768) {
options.defaultView = Config.getValue(this.storageKey, 'basicDay'); options.defaultView = Config.getValue(this.storageKey, 'listYear');
options.header = { options.header = {
left : 'prev,next today', left : 'prev,next today',
center : 'title', center : 'title',
right : 'basicWeek,basicDay' right : 'listYear'
}; };
} }
else { else {
options.defaultView = Config.getValue(this.storageKey, 'basicWeek'); options.defaultView = Config.getValue(this.storageKey, 'month');
options.header = { options.header = {
left : 'prev,next today', left : 'prev,next today',
center : 'title', center : 'title',
right : 'month,basicWeek,basicDay' right : 'month,listYear'
}; };
} }
options.titleFormat = { options.titleFormat = "L";
month : 'MMMM YYYY',
week : UiSettings.get('shortDateFormat'),
day : UiSettings.get('longDateFormat')
};
options.columnFormat = { options.columnFormat = "L"/*{
month : 'ddd', month : 'ddd',
week : UiSettings.get('calendarWeekColumnHeader'), week : UiSettings.get('calendarWeekColumnHeader'),
day : 'dddd' day : 'dddd'
}; };*///For now ignore settings. TODO update that.
options.timeFormat = UiSettings.get('timeFormat'); options.timeFormat = UiSettings.get('timeFormat');

@ -1,17 +1,17 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
var moment = require('moment'); var moment = require('moment');
var EpisodeModel = require('../Series/EpisodeModel'); var EpisodeModel = require('../Movies/MovieModel');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/calendar', url : window.NzbDrone.ApiRoot + '/calendar',
model : EpisodeModel, model : EpisodeModel,
comparator : function(model1, model2) { comparator : function(model1, model2) {
var airDate1 = model1.get('airDateUtc'); var airDate1 = model1.get('inCinemas');
var date1 = moment(airDate1); var date1 = moment(airDate1);
var time1 = date1.unix(); var time1 = date1.unix();
var airDate2 = model2.get('airDateUtc'); var airDate2 = model2.get('inCinemas');
var date2 = moment(airDate2); var date2 = moment(airDate2);
var time2 = date2.unix(); var time2 = date2.unix();

@ -11,8 +11,8 @@ module.exports = Marionette.ItemView.extend({
}, },
initialize : function() { initialize : function() {
var start = this.model.get('airDateUtc'); var start = this.model.get('inCinemas');
var runtime = this.model.get('series').runtime; var runtime = this.model.get('runtime');
var end = moment(start).add('minutes', runtime); var end = moment(start).add('minutes', runtime);
this.model.set({ this.model.set({

@ -43,6 +43,16 @@
.past { .past {
opacity : 0.8; opacity : 0.8;
} }
.fc-title {
white-space: normal;
}
.fc-list-table {
.past {
opacity: 1.0;
}
}
} }
.event { .event {
@ -102,6 +112,7 @@
.danger { .danger {
border-color : @btn-danger-bg; border-color : @btn-danger-bg;
color: white;
} }
.success { .success {
@ -172,7 +183,7 @@
.danger { .danger {
border-color : @btn-danger-bg; border-color : @btn-danger-bg;
background-color : @btn-danger-bg; background-color : @btn-danger-bg;
color: white;
.color-impaired-background-gradient(90deg, @btn-danger-bg); .color-impaired-background-gradient(90deg, @btn-danger-bg);
} }

@ -8,4 +8,9 @@
{{#if imdbId}} {{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a> <a href="{{imdbUrl}}" class="label label-info">IMDB</a>
{{/if}} {{/if}}
{{#if youTubeTrailerId}}
<a href="{{youTubeTrailerUrl}}" class="label label-info">Trailer</a>
{{/if}}
</span> </span>

@ -0,0 +1,42 @@
var NzbDroneCell = require('./NzbDroneCell');
//used in Wanted tab
module.exports = NzbDroneCell.extend({
className : 'movie-status-text-cell',
render : function() {
this.$el.empty();
var monitored = this.model.get('monitored');
var status = this.model.get('status');
var inCinemas = this.model.get("inCinemas");
var date = new Date(inCinemas);
var timeSince = new Date().getTime() - date.getTime();
var numOfMonths = timeSince / 1000 / 60 / 60 / 24 / 30;
if (status === 'released') {
this.$el.html('<div class="released-banner"><i class="icon-sonarr-movie-released grid-icon" title=""></i>&nbsp;Released</div>');
this._setStatusWeight(3);
}
if (numOfMonths > 3) {
this.$el.html('<div class="released-banner"><i class="icon-sonarr-movie-released grid-icon" title=""></i>&nbsp;Released</div>');//TODO: Update for PreDB.me
this._setStatusWeight(2);
}
if (numOfMonths < 3) {
this.$el.html('<div class="cinemas-banner"><i class="icon-sonarr-movie-cinemas grid-icon" title=""></i>&nbsp;In Cinemas</div>');
this._setStatusWeight(2);
}
if (status === "announced") {
this.$el.html('<div class="announced-banner"><i class="icon-sonarr-movie-announced grid-icon" title=""></i>&nbsp;Announced</div>');
this._setStatusWeight(1);
}
return this;
},
_setStatusWeight : function(weight) {
this.model.set('statusWeight', weight, { silent : true });
}
});

@ -55,6 +55,10 @@
width : 150px; width : 150px;
} }
.movie-status-text-cell {
width : 150px;
}
.history-event-type-cell { .history-event-type-cell {
width : 10px; width : 10px;
} }

@ -1,7 +1,7 @@
/*! /*!
* FullCalendar v2.3.2 Stylesheet * FullCalendar v3.1.0 Stylesheet
* Docs & License: http://fullcalendar.io/ * Docs & License: http://fullcalendar.io/
* (c) 2015 Adam Shaw * (c) 2016 Adam Shaw
*/ */
@ -28,7 +28,10 @@ body .fc { /* extra precedence to overcome jqui */
.fc-unthemed tbody, .fc-unthemed tbody,
.fc-unthemed .fc-divider, .fc-unthemed .fc-divider,
.fc-unthemed .fc-row, .fc-unthemed .fc-row,
.fc-unthemed .fc-popover { .fc-unthemed .fc-content, /* for gutter border */
.fc-unthemed .fc-popover,
.fc-unthemed .fc-list-view,
.fc-unthemed .fc-list-heading td {
border-color: #ddd; border-color: #ddd;
} }
@ -37,7 +40,8 @@ body .fc { /* extra precedence to overcome jqui */
} }
.fc-unthemed .fc-divider, .fc-unthemed .fc-divider,
.fc-unthemed .fc-popover .fc-header { .fc-unthemed .fc-popover .fc-header,
.fc-unthemed .fc-list-heading td {
background: #eee; background: #eee;
} }
@ -45,20 +49,18 @@ body .fc { /* extra precedence to overcome jqui */
color: #666; color: #666;
} }
.fc-unthemed .fc-today { .fc-unthemed td.fc-today {
background: #fcf8e3; background: #fcf8e3;
} }
.fc-highlight { /* when user is selecting cells */ .fc-highlight { /* when user is selecting cells */
background: #bce8f1; background: #bce8f1;
opacity: .3; opacity: .3;
filter: alpha(opacity=30); /* for IE */
} }
.fc-bgevent { /* default look for background events */ .fc-bgevent { /* default look for background events */
background: rgb(143, 223, 130); background: rgb(143, 223, 130);
opacity: .3; opacity: .3;
filter: alpha(opacity=30); /* for IE */
} }
.fc-nonbusiness { /* default look for non-business-hours areas */ .fc-nonbusiness { /* default look for non-business-hours areas */
@ -72,7 +74,6 @@ body .fc { /* extra precedence to overcome jqui */
.fc-icon { .fc-icon {
display: inline-block; display: inline-block;
width: 1em;
height: 1em; height: 1em;
line-height: 1em; line-height: 1em;
font-size: 1em; font-size: 1em;
@ -99,7 +100,6 @@ NOTE: use percentage font sizes or else old IE chokes
.fc-icon:after { .fc-icon:after {
position: relative; position: relative;
margin: 0 -1em; /* ensures character will be centered, regardless of width */
} }
.fc-icon-left-single-arrow:after { .fc-icon-left-single-arrow:after {
@ -107,7 +107,6 @@ NOTE: use percentage font sizes or else old IE chokes
font-weight: bold; font-weight: bold;
font-size: 200%; font-size: 200%;
top: -7%; top: -7%;
left: 3%;
} }
.fc-icon-right-single-arrow:after { .fc-icon-right-single-arrow:after {
@ -115,7 +114,6 @@ NOTE: use percentage font sizes or else old IE chokes
font-weight: bold; font-weight: bold;
font-size: 200%; font-size: 200%;
top: -7%; top: -7%;
left: -3%;
} }
.fc-icon-left-double-arrow:after { .fc-icon-left-double-arrow:after {
@ -134,14 +132,12 @@ NOTE: use percentage font sizes or else old IE chokes
content: "\25C4"; content: "\25C4";
font-size: 125%; font-size: 125%;
top: 3%; top: 3%;
left: -2%;
} }
.fc-icon-right-triangle:after { .fc-icon-right-triangle:after {
content: "\25BA"; content: "\25BA";
font-size: 125%; font-size: 125%;
top: 3%; top: 3%;
left: 2%;
} }
.fc-icon-down-triangle:after { .fc-icon-down-triangle:after {
@ -252,7 +248,6 @@ NOTE: use percentage font sizes or else old IE chokes
cursor: default; cursor: default;
background-image: none; background-image: none;
opacity: 0.65; opacity: 0.65;
filter: alpha(opacity=65);
box-shadow: none; box-shadow: none;
} }
@ -372,6 +367,7 @@ hr.fc-divider {
.fc table { .fc table {
width: 100%; width: 100%;
box-sizing: border-box; /* fix scrollbar issue in firefox */
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
@ -395,6 +391,18 @@ hr.fc-divider {
} }
/* Internal Nav Links
--------------------------------------------------------------------------------------------------*/
a[data-goto] {
cursor: pointer;
}
a[data-goto]:hover {
text-decoration: underline;
}
/* Fake Table Rows /* Fake Table Rows
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
@ -491,15 +499,15 @@ temporary rendered events).
/* Scrolling Container /* Scrolling Container
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
.fc-scroller { /* this class goes on elements for guaranteed vertical scrollbars */ .fc-scroller {
overflow-y: scroll; -webkit-overflow-scrolling: touch;
overflow-x: hidden;
} }
.fc-scroller > * { /* we expect an immediate inner element */ /* TODO: move to agenda/basic */
.fc-scroller > .fc-day-grid,
.fc-scroller > .fc-time-grid {
position: relative; /* re-scope all positions */ position: relative; /* re-scope all positions */
width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */
overflow: hidden; /* don't let negative margins or absolute positioning create further scroll */
} }
@ -513,10 +521,14 @@ temporary rendered events).
line-height: 1.3; line-height: 1.3;
border-radius: 3px; border-radius: 3px;
border: 1px solid #3a87ad; /* default BORDER color */ border: 1px solid #3a87ad; /* default BORDER color */
background-color: #3a87ad; /* default BACKGROUND color */
font-weight: normal; /* undo jqui's ui-widget-header bold */ font-weight: normal; /* undo jqui's ui-widget-header bold */
} }
.fc-event,
.fc-event-dot {
background-color: #3a87ad; /* default BACKGROUND color */
}
/* overpower some of bootstrap's and jqui's styles on <a> tags */ /* overpower some of bootstrap's and jqui's styles on <a> tags */
.fc-event, .fc-event,
.fc-event:hover, .fc-event:hover,
@ -539,7 +551,6 @@ temporary rendered events).
z-index: 1; z-index: 1;
background: #fff; background: #fff;
opacity: .25; opacity: .25;
filter: alpha(opacity=25); /* for IE */
} }
.fc-event .fc-content { .fc-event .fc-content {
@ -547,15 +558,68 @@ temporary rendered events).
z-index: 2; z-index: 2;
} }
/* resizer (cursor AND touch devices) */
.fc-event .fc-resizer { .fc-event .fc-resizer {
position: absolute; position: absolute;
z-index: 3; z-index: 4;
}
/* resizer (touch devices) */
.fc-event .fc-resizer {
display: none;
}
.fc-event.fc-allow-mouse-resize .fc-resizer,
.fc-event.fc-selected .fc-resizer {
/* only show when hovering or selected (with touch) */
display: block;
}
/* hit area */
.fc-event.fc-selected .fc-resizer:before {
/* 40x40 touch area */
content: "";
position: absolute;
z-index: 9999; /* user of this util can scope within a lower z-index */
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin-left: -20px;
margin-top: -20px;
}
/* Event Selection (only for touch devices)
--------------------------------------------------------------------------------------------------*/
.fc-event.fc-selected {
z-index: 9999 !important; /* overcomes inline z-index */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.fc-event.fc-selected.fc-dragging {
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3);
} }
/* Horizontal Events /* Horizontal Events
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
/* bigger touch area when selected */
.fc-h-event.fc-selected:before {
content: "";
position: absolute;
z-index: 3; /* below resizers */
top: -10px;
bottom: -10px;
left: 0;
right: 0;
}
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ /* events that are continuing to/from another week. kill rounded corners and butt up against edge */
.fc-ltr .fc-h-event.fc-not-start, .fc-ltr .fc-h-event.fc-not-start,
@ -576,36 +640,56 @@ temporary rendered events).
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
/* resizer */ /* resizer (cursor AND touch devices) */
.fc-h-event .fc-resizer { /* positioned it to overcome the event's borders */
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
width: 5px;
}
/* left resizer */ /* left resizer */
.fc-ltr .fc-h-event .fc-start-resizer, .fc-ltr .fc-h-event .fc-start-resizer,
.fc-ltr .fc-h-event .fc-start-resizer:before, .fc-rtl .fc-h-event .fc-end-resizer {
.fc-ltr .fc-h-event .fc-start-resizer:after,
.fc-rtl .fc-h-event .fc-end-resizer,
.fc-rtl .fc-h-event .fc-end-resizer:before,
.fc-rtl .fc-h-event .fc-end-resizer:after {
right: auto; /* ignore the right and only use the left */
cursor: w-resize; cursor: w-resize;
left: -1px; /* overcome border */
} }
/* right resizer */ /* right resizer */
.fc-ltr .fc-h-event .fc-end-resizer, .fc-ltr .fc-h-event .fc-end-resizer,
.fc-ltr .fc-h-event .fc-end-resizer:before, .fc-rtl .fc-h-event .fc-start-resizer {
.fc-ltr .fc-h-event .fc-end-resizer:after,
.fc-rtl .fc-h-event .fc-start-resizer,
.fc-rtl .fc-h-event .fc-start-resizer:before,
.fc-rtl .fc-h-event .fc-start-resizer:after {
left: auto; /* ignore the left and only use the right */
cursor: e-resize; cursor: e-resize;
right: -1px; /* overcome border */
}
/* resizer (mouse devices) */
.fc-h-event.fc-allow-mouse-resize .fc-resizer {
width: 7px;
top: -1px; /* overcome top border */
bottom: -1px; /* overcome bottom border */
}
/* resizer (touch devices) */
.fc-h-event.fc-selected .fc-resizer {
/* 8x8 little dot */
border-radius: 4px;
border-width: 1px;
width: 6px;
height: 6px;
border-style: solid;
border-color: inherit;
background: #fff;
/* vertically center */
top: 50%;
margin-top: -4px;
}
/* left resizer */
.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,
.fc-rtl .fc-h-event.fc-selected .fc-end-resizer {
margin-left: -4px; /* centers the 8x8 dot on the left edge */
}
/* right resizer */
.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,
.fc-rtl .fc-h-event.fc-selected .fc-start-resizer {
margin-right: -4px; /* centers the 8x8 dot on the right edge */
} }
@ -620,6 +704,23 @@ be a descendant of the grid when it is being dragged.
padding: 0 1px; padding: 0 1px;
} }
tr:first-child > td > .fc-day-grid-event {
margin-top: 2px; /* a little bit more space before the first event */
}
.fc-day-grid-event.fc-selected:after {
content: "";
position: absolute;
z-index: 1; /* same z-index as fc-bg, behind text */
/* overcome the borders */
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
/* darkening effect */
background: #000;
opacity: .25;
}
.fc-day-grid-event .fc-content { /* force events to be one-line tall */ .fc-day-grid-event .fc-content { /* force events to be one-line tall */
white-space: nowrap; white-space: nowrap;
@ -630,10 +731,18 @@ be a descendant of the grid when it is being dragged.
font-weight: bold; font-weight: bold;
} }
.fc-day-grid-event .fc-resizer { /* enlarge the default hit area */ /* resizer (cursor devices) */
left: -3px;
right: -3px; /* left resizer */
width: 7px; .fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer {
margin-left: -2px; /* to the day cell's edge */
}
/* right resizer */
.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer {
margin-right: -2px; /* to the day cell's edge */
} }
@ -672,14 +781,46 @@ a.fc-more:hover {
padding: 10px; padding: 10px;
} }
/* Now Indicator
--------------------------------------------------------------------------------------------------*/
.fc-now-indicator {
position: absolute;
border: 0 solid red;
}
/* Utilities
--------------------------------------------------------------------------------------------------*/
.fc-unselectable {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Toolbar /* Toolbar
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
.fc-toolbar { .fc-toolbar {
text-align: center; text-align: center;
}
.fc-toolbar.fc-header-toolbar {
margin-bottom: 1em; margin-bottom: 1em;
} }
.fc-toolbar.fc-footer-toolbar {
margin-top: 1em;
}
.fc-toolbar .fc-left { .fc-toolbar .fc-left {
float: left; float: left;
} }
@ -753,6 +894,8 @@ a.fc-more:hover {
z-index: 1; z-index: 1;
} }
/* BasicView /* BasicView
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
@ -760,8 +903,7 @@ a.fc-more:hover {
.fc-basicWeek-view .fc-content-skeleton, .fc-basicWeek-view .fc-content-skeleton,
.fc-basicDay-view .fc-content-skeleton { .fc-basicDay-view .fc-content-skeleton {
/* we are sure there are no day numbers in these views, so... */ /* there may be week numbers in these views, so no padding-top */
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */
} }
@ -784,42 +926,45 @@ a.fc-more:hover {
/* week and day number styling */ /* week and day number styling */
.fc-day-top.fc-other-month {
opacity: 0.3;
}
.fc-basic-view .fc-week-number, .fc-basic-view .fc-week-number,
.fc-basic-view .fc-day-number { .fc-basic-view .fc-day-number {
padding: 0 2px; padding: 2px;
} }
.fc-basic-view td.fc-week-number span, .fc-basic-view th.fc-week-number,
.fc-basic-view td.fc-day-number { .fc-basic-view th.fc-day-number {
padding-top: 2px; padding: 0 2px; /* column headers can't have as much v space */
padding-bottom: 2px;
} }
.fc-basic-view .fc-week-number { .fc-ltr .fc-basic-view .fc-day-top .fc-day-number { float: right; }
.fc-rtl .fc-basic-view .fc-day-top .fc-day-number { float: left; }
.fc-ltr .fc-basic-view .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; }
.fc-rtl .fc-basic-view .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; }
.fc-basic-view .fc-day-top .fc-week-number {
min-width: 1.5em;
text-align: center; text-align: center;
background-color: #f2f2f2;
color: #808080;
} }
.fc-basic-view .fc-week-number span { /* when week/day number have own column */
/* work around the way we do column resizing and ensure a minimum width */
display: inline-block;
min-width: 1.25em;
}
.fc-ltr .fc-basic-view .fc-day-number { .fc-basic-view td.fc-week-number {
text-align: right; text-align: center;
} }
.fc-rtl .fc-basic-view .fc-day-number { .fc-basic-view td.fc-week-number > * {
text-align: left; /* work around the way we do column resizing and ensure a minimum width */
display: inline-block;
min-width: 1.25em;
} }
.fc-day-number.fc-other-month {
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
/* opacity with small font can sometimes look too faded
might want to set the 'color' property instead
making day-numbers bold also fixes the problem */
}
/* AgendaView all-day area /* AgendaView all-day area
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
@ -834,7 +979,6 @@ a.fc-more:hover {
} }
.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { .fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton {
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ padding-bottom: 1em; /* give space underneath events for clicking/selecting days */
} }
@ -888,27 +1032,46 @@ a.fc-more:hover {
z-index: 2; z-index: 2;
} }
.fc-time-grid .fc-bgevent-skeleton, .fc-time-grid .fc-content-col {
position: relative; /* because now-indicator lives directly inside */
}
.fc-time-grid .fc-content-skeleton { .fc-time-grid .fc-content-skeleton {
position: absolute; position: absolute;
z-index: 3;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
} }
.fc-time-grid .fc-bgevent-skeleton { /* divs within a cell within the fc-content-skeleton */
.fc-time-grid .fc-business-container {
position: relative;
z-index: 1;
}
.fc-time-grid .fc-bgevent-container {
position: relative;
z-index: 2;
}
.fc-time-grid .fc-highlight-container {
position: relative;
z-index: 3; z-index: 3;
} }
.fc-time-grid .fc-highlight-skeleton { .fc-time-grid .fc-event-container {
position: relative;
z-index: 4; z-index: 4;
} }
.fc-time-grid .fc-content-skeleton { .fc-time-grid .fc-now-indicator-line {
z-index: 5; z-index: 5;
} }
.fc-time-grid .fc-helper-skeleton { .fc-time-grid .fc-helper-container { /* also is fc-event-container */
position: relative;
z-index: 6; z-index: 6;
} }
@ -948,11 +1111,6 @@ a.fc-more:hover {
/* TimeGrid Event Containment /* TimeGrid Event Containment
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-event-container, /* a div within a cell within the fc-content-skeleton */
.fc-time-grid .fc-bgevent-container { /* a div within a cell within the fc-bgevent-skeleton */
position: relative;
}
.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ .fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */
margin: 0 2.5% 0 2px; margin: 0 2.5% 0 2px;
} }
@ -1008,6 +1166,20 @@ be a descendant of the grid when it is being dragged.
overflow: hidden; /* don't let the bg flow over rounded corners */ overflow: hidden; /* don't let the bg flow over rounded corners */
} }
.fc-time-grid-event.fc-selected {
/* need to allow touch resizers to extend outside event's bounding box */
/* common fc-selected styles hide the fc-bg, so don't need this anyway */
overflow: visible;
}
.fc-time-grid-event.fc-selected .fc-bg {
display: none; /* hide semi-white background, to appear darker */
}
.fc-time-grid-event .fc-content {
overflow: hidden; /* for when .fc-selected */
}
.fc-time-grid-event .fc-time, .fc-time-grid-event .fc-time,
.fc-time-grid-event .fc-title { .fc-time-grid-event .fc-title {
padding: 0 1px; padding: 0 1px;
@ -1049,9 +1221,9 @@ be a descendant of the grid when it is being dragged.
padding: 0; /* undo padding from above */ padding: 0; /* undo padding from above */
} }
/* resizer */ /* resizer (cursor device) */
.fc-time-grid-event .fc-resizer { .fc-time-grid-event.fc-allow-mouse-resize .fc-resizer {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
@ -1064,6 +1236,169 @@ be a descendant of the grid when it is being dragged.
cursor: s-resize; cursor: s-resize;
} }
.fc-time-grid-event .fc-resizer:after { .fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after {
content: "="; content: "=";
} }
/* resizer (touch device) */
.fc-time-grid-event.fc-selected .fc-resizer {
/* 10x10 dot */
border-radius: 5px;
border-width: 1px;
width: 8px;
height: 8px;
border-style: solid;
border-color: inherit;
background: #fff;
/* horizontally center */
left: 50%;
margin-left: -5px;
/* center on the bottom edge */
bottom: -5px;
}
/* Now Indicator
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-now-indicator-line {
border-top-width: 1px;
left: 0;
right: 0;
}
/* arrow on axis */
.fc-time-grid .fc-now-indicator-arrow {
margin-top: -5px; /* vertically center on top coordinate */
}
.fc-ltr .fc-time-grid .fc-now-indicator-arrow {
left: 0;
/* triangle pointing right... */
border-width: 5px 0 5px 6px;
border-top-color: transparent;
border-bottom-color: transparent;
}
.fc-rtl .fc-time-grid .fc-now-indicator-arrow {
right: 0;
/* triangle pointing left... */
border-width: 5px 6px 5px 0;
border-top-color: transparent;
border-bottom-color: transparent;
}
/* List View
--------------------------------------------------------------------------------------------------*/
/* possibly reusable */
.fc-event-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
/* view wrapper */
.fc-rtl .fc-list-view {
direction: rtl; /* unlike core views, leverage browser RTL */
}
.fc-list-view {
border-width: 1px;
border-style: solid;
}
/* table resets */
.fc .fc-list-table {
table-layout: auto; /* for shrinkwrapping cell content */
}
.fc-list-table td {
border-width: 1px 0 0;
padding: 8px 14px;
}
.fc-list-table tr:first-child td {
border-top-width: 0;
}
/* day headings with the list */
.fc-list-heading {
border-bottom-width: 1px;
}
.fc-list-heading td {
font-weight: bold;
}
.fc-ltr .fc-list-heading-main { float: left; }
.fc-ltr .fc-list-heading-alt { float: right; }
.fc-rtl .fc-list-heading-main { float: right; }
.fc-rtl .fc-list-heading-alt { float: left; }
/* event list items */
.fc-list-item.fc-has-url {
cursor: pointer; /* whole row will be clickable */
}
.fc-list-item:hover td {
background-color: #f5f5f5;
}
.fc-list-item-marker,
.fc-list-item-time {
white-space: nowrap;
width: 1px;
}
/* make the dot closer to the event title */
.fc-ltr .fc-list-item-marker { padding-right: 0; }
.fc-rtl .fc-list-item-marker { padding-left: 0; }
.fc-list-item-title a {
/* every event title cell has an <a> tag */
text-decoration: none;
color: inherit;
}
.fc-list-item-title a[href]:hover {
/* hover effect only on titles with hrefs */
text-decoration: underline;
}
/* message when no events */
.fc-list-empty-wrap2 {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.fc-list-empty-wrap1 {
width: 100%;
height: 100%;
display: table;
}
.fc-list-empty {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.fc-unthemed .fc-list-empty { /* theme will provide own background */
background-color: #eee;
}

@ -21,7 +21,7 @@ Handlebars.registerHelper('StatusLevel', function() {
var start = moment(this.airDateUtc); var start = moment(this.airDateUtc);
var end = moment(this.end); var end = moment(this.end);
var monitored = this.series.monitored && this.monitored; var monitored = this.series.monitored && this.monitored;
debugger;
if (hasFile) { if (hasFile) {
return 'success'; return 'success';
} }

@ -51,6 +51,10 @@ Handlebars.registerHelper('tmdbUrl', function() {
return 'https://www.themoviedb.org/movie/' + this.tmdbId; return 'https://www.themoviedb.org/movie/' + this.tmdbId;
}); });
Handlebars.registerHelper('youTubeTrailerUrl', function() {
return 'https://www.youtube.com/watch?v=' + this.youTubeTrailerId;
});
Handlebars.registerHelper('homepage', function() { Handlebars.registerHelper('homepage', function() {
return this.website; return this.website;
}); });

File diff suppressed because it is too large Load Diff

@ -6,6 +6,9 @@
<span class="label label-info">{{network}}</span> <span class="label label-info">{{network}}</span>
{{/if}} {{/if}}
{{#if studio}}
<span class="label label-info">{{studio}}</span>
{{/if}}
<span class="label label-info">{{runtime}} minutes</span> <span class="label label-info">{{runtime}} minutes</span>
<span class="label label-info">{{path}}</span> <span class="label label-info">{{path}}</span>
@ -33,6 +36,10 @@
{{#if imdbId}} {{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a> <a href="{{imdbUrl}}" class="label label-info">IMDB</a>
{{/if}} {{/if}}
{{#if youTubeTrailerId}}
<a href="{{youTubeTrailerUrl}}" class="label label-info">Trailer</a>
{{/if}}
</span> </span>
</div> </div>
</div> </div>

@ -11,9 +11,9 @@
Are you sure you want to update all files in the {{numberOfMovies}} selected movies? Are you sure you want to update all files in the {{numberOfMovies}} selected movies?
{{debug}}
<ul class="selected-series"> <ul class="selected-series">
{{#each movie}} {{#each movies}}
<li>{{title}}</li> <li>{{title}}</li>
{{/each}} {{/each}}
</ul> </ul>

@ -54,6 +54,10 @@
{{#if imdbId}} {{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a> <a href="{{imdbUrl}}" class="label label-info">IMDB</a>
{{/if}} {{/if}}
{{#if youTubeTrailerId}}
<a href="{{youTubeTrailerUrl}}" class="label label-info">Trailer</a>
{{/if}}
</span> </span>
</div> </div>
</div> </div>

@ -18,7 +18,7 @@
<div class="center"> <div class="center">
<div class="labels"> <div class="labels">
<span class="label label-{{DownloadedStatusColor}}" title="{{DownloadedQuality}}">{{DownloadedStatus}}</span>
{{#if website}} {{#if website}}
<a href="{{homepage}}" class="label label-info">Homepage</a> <a href="{{homepage}}" class="label label-info">Homepage</a>
{{/if}} {{/if}}
@ -26,7 +26,9 @@
{{#if imdbId}} {{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a> <a href="{{imdbUrl}}" class="label label-info">IMDB</a>
{{/if}} {{/if}}
{{#if youTubeTrailerId}}
<a href="{{youTubeTrailerUrl}}" class="label label-info">Trailer</a>
{{/if}}
</div> </div>
</div> </div>
</div> </div>

@ -126,7 +126,7 @@
.card; .card;
.clickable; .clickable;
margin-bottom : 20px; margin-bottom : 20px;
height : 324px; height : 363px;
.center { .center {
display : block; display : block;
@ -166,7 +166,7 @@
} }
@media (max-width: @screen-xs-max) { @media (max-width: @screen-xs-max) {
height : 235px; height : 283px;
margin : 5px; margin : 5px;
padding : 6px 5px; padding : 6px 5px;

@ -25,7 +25,7 @@
<li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-wanted"></i> Wanted</a></li> <li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-wanted"></i> Wanted</a></li>
<li><a href="{{UrlBase}}/settings" class="x-settings-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-settings"></i> Settings</a></li> <li><a href="{{UrlBase}}/settings" class="x-settings-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-settings"></i> Settings</a></li>
<li><a href="{{UrlBase}}/system" class="x-system-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-system"></i> System<span id="x-health" class="navbar-info"></span></a></li> <li><a href="{{UrlBase}}/system" class="x-system-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-system"></i> System<span id="x-health" class="navbar-info"></span></a></li>
<li><a href="https://sonarr.tv/donate" target="_blank"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-donate"></i> Donate</a></li> <li><a href="https://radarr.video/donate.html" target="_blank"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-donate"></i> Donate</a></li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="active screen-size"></li> <li class="active screen-size"></li>

@ -25,7 +25,9 @@ $.fn.bindSearch = function() {
minLength : 1 minLength : 1
}, { }, {
name : 'series', name : 'series',
displayKey : 'title', displayKey : function(series) {
return series.title + ' (' + series.year + ')';
},
source : substringMatcher() source : substringMatcher()
}); });

@ -1,43 +1,8 @@
// var Backbone = require('backbone');
// var RenamePreviewModel = require('./RenamePreviewModel');
// module.exports = Backbone.Collection.extend({
// url : window.NzbDrone.ApiRoot + '/rename',
// model : RenamePreviewModel,
// originalFetch : Backbone.Collection.prototype.fetch,
// initialize : function(options) {
// if (!options.seriesId) {
// throw 'seriesId is required';
// }
// this.seriesId = options.seriesId;
// this.seasonNumber = options.seasonNumber;
// },
// fetch : function(options) {
// if (!this.seriesId) {
// throw 'seriesId is required';
// }
// options = options || {};
// options.data = {};
// options.data.seriesId = this.seriesId;
// if (this.seasonNumber !== undefined) {
// options.data.seasonNumber = this.seasonNumber;
// }
// return this.originalFetch.call(this, options);
// }
// });
var Backbone = require('backbone'); var Backbone = require('backbone');
var RenamePreviewModel = require('./RenamePreviewModel'); var RenamePreviewModel = require('./RenamePreviewModel');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/rename', url : window.NzbDrone.ApiRoot + '/renameMovie',
model : RenamePreviewModel, model : RenamePreviewModel,
originalFetch : Backbone.Collection.prototype.fetch, originalFetch : Backbone.Collection.prototype.fetch,

@ -6,10 +6,10 @@ module.exports = Marionette.ItemView.extend({
template : 'Rename/RenamePreviewFormatViewTemplate', template : 'Rename/RenamePreviewFormatViewTemplate',
templateHelpers : function() { templateHelpers : function() {
var type = this.model.get('seriesType'); //var type = this.model.get('seriesType');
return { return {
rename : this.naming.get('renameEpisodes'), rename : this.naming.get('renameEpisodes'),
format : this.naming.get(type + 'EpisodeFormat') format : this.naming.get('standardMovieFormat')
}; };
}, },

@ -77,8 +77,8 @@
<div class="col-sm-2 col-sm-pull-1"> <div class="col-sm-2 col-sm-pull-1">
<select class="form-control" name="fileDate"> <select class="form-control" name="fileDate">
<option value="none">None</option> <option value="none">None</option>
<option value="localAirDate">Local Air Date</option> <option value="cinemas">In Cinemas Date</option>
<option value="utcAirDate">UTC Air Date</option> <option value="release">Physical Release Date</option>
</select> </select>
</div> </div>
</div> </div>

@ -72,6 +72,7 @@
{{> MediaInfoNamingPartial}} {{> MediaInfoNamingPartial}}
{{> ReleaseGroupNamingPartial}} {{> ReleaseGroupNamingPartial}}
{{> OriginalTitleNamingPartial}} {{> OriginalTitleNamingPartial}}
{{> ImdbIdNamingPartial}}
{{> SeparatorNamingPartial}} {{> SeparatorNamingPartial}}
</ul> </ul>
</div> </div>
@ -161,6 +162,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{{> MovieTitleNamingPartial}} {{> MovieTitleNamingPartial}}
{{> ReleaseYearNamingPartial}} {{> ReleaseYearNamingPartial}}
{{> ImdbIdNamingPartial}}
</ul> </ul>
</div> </div>
</div> </div>

@ -0,0 +1 @@
<li><a href="#" data-token="IMDb Id">IMDb Id</a></li>

@ -4,6 +4,7 @@
<li><a href="#" data-token="Movie Title">Movie Title</a></li> <li><a href="#" data-token="Movie Title">Movie Title</a></li>
<li><a href="#" data-token="Movie.Title">Movie.Title</a></li> <li><a href="#" data-token="Movie.Title">Movie.Title</a></li>
<li><a href="#" data-token="Movie_Title">Movie_Title</a></li> <li><a href="#" data-token="Movie_Title">Movie_Title</a></li>
<li><a href="#" data-token="Movie TitleThe">Movie Title, The</a></li>
<li><a href="#" data-token="Movie CleanTitle">Movie CleanTitle</a></li> <li><a href="#" data-token="Movie CleanTitle">Movie CleanTitle</a></li>
<li><a href="#" data-token="Movie.CleanTitle">Movie.CleanTitle</a></li> <li><a href="#" data-token="Movie.CleanTitle">Movie.CleanTitle</a></li>
<li><a href="#" data-token="Movie_CleanTitle">Movie_CleanTitle</a></li> <li><a href="#" data-token="Movie_CleanTitle">Movie_CleanTitle</a></li>

@ -6,7 +6,7 @@
<div class="row"> <div class="row">
<span class="col-md-2 col-sm-3">Quality</span> <span class="col-md-2 col-sm-3">Quality</span>
<span class="col-md-2 col-sm-3">Title</span> <span class="col-md-2 col-sm-3">Title</span>
<span class="col-md-4 col-sm-6">Size Limit <i class="icon-sonarr-info" title="Limits are automatically adjusted for the series runtime and number of episodes in the file." /></span> <span class="col-md-4 col-sm-6">Size Limit <i class="icon-sonarr-warning" title="Limits are automatically adjusted for the movie runtime." /></span>
</div> </div>
</div> </div>
<div class="rows x-rows"> <div class="rows x-rows">

@ -67,8 +67,8 @@ var view = Marionette.ItemView.extend({
var maxSize = this.model.get('maxSize') || null; var maxSize = this.model.get('maxSize') || null;
{ {
var minBytes = minSize * 1024 * 1024; var minBytes = minSize * 1024 * 1024;
var minThirty = FormatHelpers.bytes(minBytes * 30, 2); var minThirty = FormatHelpers.bytes(minBytes * 90, 2);
var minSixty = FormatHelpers.bytes(minBytes * 60, 2); var minSixty = FormatHelpers.bytes(minBytes * 140, 2);
this.ui.thirtyMinuteMinSize.html(minThirty); this.ui.thirtyMinuteMinSize.html(minThirty);
this.ui.sixtyMinuteMinSize.html(minSixty); this.ui.sixtyMinuteMinSize.html(minSixty);
@ -80,8 +80,8 @@ var view = Marionette.ItemView.extend({
this.ui.sixtyMinuteMaxSize.html('Unlimited'); this.ui.sixtyMinuteMaxSize.html('Unlimited');
} else { } else {
var maxBytes = maxSize * 1024 * 1024; var maxBytes = maxSize * 1024 * 1024;
var maxThirty = FormatHelpers.bytes(maxBytes * 30, 2); var maxThirty = FormatHelpers.bytes(maxBytes * 90, 2);
var maxSixty = FormatHelpers.bytes(maxBytes * 60, 2); var maxSixty = FormatHelpers.bytes(maxBytes * 140, 2);
this.ui.thirtyMinuteMaxSize.html(maxThirty); this.ui.thirtyMinuteMaxSize.html(maxThirty);
this.ui.sixtyMinuteMaxSize.html(maxSixty); this.ui.sixtyMinuteMaxSize.html(maxSixty);

@ -10,21 +10,21 @@
<div class="pull-left"> <div class="pull-left">
<span class="label label-warning x-min-thirty" <span class="label label-warning x-min-thirty"
name="thirtyMinuteMinSize" name="thirtyMinuteMinSize"
title="Minimum size for a 30 minute episode"> title="Minimum size for a 90 minute episode">
</span> </span>
<span class="label label-info x-min-sixty" <span class="label label-info x-min-sixty"
name="sixtyMinuteMinSize" name="sixtyMinuteMinSize"
title="Minimum size for a 60 minute episode"> title="Minimum size for a 140 minute episode">
</span> </span>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<span class="label label-warning x-max-thirty" <span class="label label-warning x-max-thirty"
name="thirtyMinuteMaxSize" name="thirtyMinuteMaxSize"
title="Maximum size for a 30 minute episode"> title="Maximum size for a 90 minute movie">
</span> </span>
<span class="label label-info x-max-sixty" <span class="label label-info x-max-sixty"
name="sixtyMinuteMaxSize" name="sixtyMinuteMaxSize"
title="Maximum size for a 60 minute episode"> title="Maximum size for a 140 minute movie">
</span> </span>
</div> </div>
</div> </div>

@ -7,10 +7,10 @@
<dt>Reddit</dt> <dt>Reddit</dt>
<dd><a href="https://www.reddit.com/r/radarr/">Radarr Subreddit</a> <dd><a href="https://www.reddit.com/r/radarr/">Radarr Subreddit</a>
{{!--<dt>Home page</dt> <dt>Home page</dt>
<dd><a href="https://radarr.tdb/">radarr.tdb</a></dd> <dd><a href="https://radarr.video/">radarr.video</a></dd>
<dt>Wiki</dt> {{!--<dt>Wiki</dt>
<dd><a href="https://wiki.radarr.tdb/">wiki.radarr.tdb</a></dd> <dd><a href="https://wiki.radarr.tdb/">wiki.radarr.tdb</a></dd>
<dt>Forums</dt> <dt>Forums</dt>

@ -1,5 +1,5 @@
var _ = require('underscore'); var _ = require('underscore');
var EpisodeModel = require('../../Series/EpisodeModel'); var MovieModel = require('../../Movies/MovieModel');
var PagableCollection = require('backbone.pageable'); var PagableCollection = require('backbone.pageable');
var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); var AsFilteredCollection = require('../../Mixins/AsFilteredCollection');
var AsSortedCollection = require('../../Mixins/AsSortedCollection'); var AsSortedCollection = require('../../Mixins/AsSortedCollection');
@ -7,13 +7,13 @@ var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollectio
var Collection = PagableCollection.extend({ var Collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + '/wanted/missing', url : window.NzbDrone.ApiRoot + '/wanted/missing',
model : EpisodeModel, model : MovieModel,
tableName : 'wanted.missing', tableName : 'wanted.missing',
state : { state : {
pageSize : 15, pageSize : 15,
sortKey : 'airDateUtc', sortKey : 'inCinemas',
order : 1 order : -1
}, },
queryParams : { queryParams : {
@ -39,10 +39,6 @@ var Collection = PagableCollection.extend({
] ]
}, },
sortMappings : {
'series' : { sortKey : 'series.sortTitle' }
},
parseState : function(resp) { parseState : function(resp) {
return { totalRecords : resp.totalRecords }; return { totalRecords : resp.totalRecords };
}, },

@ -5,11 +5,9 @@ var Marionette = require('marionette');
var Backgrid = require('backgrid'); var Backgrid = require('backgrid');
var MissingCollection = require('./MissingCollection'); var MissingCollection = require('./MissingCollection');
var SelectAllCell = require('../../Cells/SelectAllCell'); var SelectAllCell = require('../../Cells/SelectAllCell');
var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); var MovieTitleCell = require('../../Cells/MovieTitleCell');
var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell');
var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell');
var RelativeDateCell = require('../../Cells/RelativeDateCell'); var RelativeDateCell = require('../../Cells/RelativeDateCell');
var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); var MovieStatusWithTextCell = require('../../Cells/MovieStatusWithTextCell');
var GridPager = require('../../Shared/Grid/Pager'); var GridPager = require('../../Shared/Grid/Pager');
var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout');
var LoadingView = require('../../Shared/LoadingView'); var LoadingView = require('../../Shared/LoadingView');
@ -39,35 +37,29 @@ module.exports = Marionette.Layout.extend({
headerCell : 'select-all', headerCell : 'select-all',
sortable : false sortable : false
}, },
{
name : 'series',
label : 'Series Title',
cell : SeriesTitleCell,
sortValue : 'series.sortTitle'
},
{ {
name : 'this', name : 'this',
label : 'Episode', label : 'Movie Title',
cell : EpisodeNumberCell, cell : MovieTitleCell,
sortable : false sortable : false
}, },
{ {
name : 'this', name : 'inCinemas',
label : 'Episode Title', label : 'In Cinemas',
cell : EpisodeTitleCell, cell : RelativeDateCell
sortable : false
}, },
{ {
name : 'airDateUtc', name : 'physicalRelease',
label : 'Air Date', label : 'PhysicalRelease',
cell : RelativeDateCell cell : RelativeDateCell
}, },
{ {
name : 'status', name : 'status',
label : 'Status', label : 'Status',
cell : EpisodeStatusCell, cell : MovieStatusWithTextCell,
sortable : false sortable : false
} },
], ],
initialize : function() { initialize : function() {
@ -206,8 +198,8 @@ module.exports = Marionette.Layout.extend({
}); });
}, },
_searchMissing : function() { _searchMissing : function() {
if (window.confirm('Are you sure you want to search for {0} missing episodes? '.format(this.collection.state.totalRecords) + if (window.confirm('Are you sure you want to search for {0} missing movies? '.format(this.collection.state.totalRecords) +
'One API request to each indexer will be used for each episode. ' + 'This cannot be stopped once started.')) { 'One API request to each indexer will be used for each movie. ' + 'This cannot be stopped once started.')) {
CommandController.Execute('missingEpisodeSearch', { name : 'missingEpisodeSearch' }); CommandController.Execute('missingEpisodeSearch', { name : 'missingEpisodeSearch' });
} }
}, },

@ -29,7 +29,8 @@ vent.Commands = {
ShowFileBrowser : 'showFileBrowser', ShowFileBrowser : 'showFileBrowser',
CloseFileBrowser : 'closeFileBrowser', CloseFileBrowser : 'closeFileBrowser',
OpenControlPanelCommand : 'OpenControlPanelCommand', OpenControlPanelCommand : 'OpenControlPanelCommand',
CloseControlPanelCommand : 'CloseControlPanelCommand' CloseControlPanelCommand : 'CloseControlPanelCommand',
ShowExistingCommand : 'ShowExistingCommand'
}; };
vent.Hotkeys = { vent.Hotkeys = {

Loading…
Cancel
Save