diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 0687a1413..6d731620c 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Newtonsoft.Json.Linq; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Profiles; namespace NzbDrone.Api.ClientSchema { @@ -73,14 +76,14 @@ namespace NzbDrone.Api.ClientSchema if (propertyInfo.PropertyType == typeof(int)) { - var value = Convert.ToInt32(field.Value); - propertyInfo.SetValue(target, value, null); + var value = field.Value.ToString().ParseInt32(); + propertyInfo.SetValue(target, value ?? 0, null); } else if (propertyInfo.PropertyType == typeof(long)) { - var value = Convert.ToInt64(field.Value); - propertyInfo.SetValue(target, value, null); + var value = field.Value.ToString().ParseInt64(); + propertyInfo.SetValue(target, value ?? 0, null); } else if (propertyInfo.PropertyType == typeof(int?)) @@ -147,6 +150,18 @@ namespace NzbDrone.Api.ClientSchema private static List GetSelectOptions(Type selectOptions) { + if (selectOptions == typeof(Profile)) + { + return new List(); + } + + if (selectOptions == typeof(Quality)) + { + var qOptions = from Quality q in selectOptions.GetProperties(BindingFlags.Static | BindingFlags.Public) + select new SelectOption {Name = q.Name, Value = q.Id}; + return qOptions.OrderBy(o => o.Value).ToList(); + } + var options = from Enum e in Enum.GetValues(selectOptions) select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; diff --git a/src/NzbDrone.Api/Config/NetImportConfigModule.cs b/src/NzbDrone.Api/Config/NetImportConfigModule.cs new file mode 100644 index 000000000..f805e8c2d --- /dev/null +++ b/src/NzbDrone.Api/Config/NetImportConfigModule.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Api.Validation; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Config +{ + public class NetImportConfigModule : NzbDroneConfigModule + { + + public NetImportConfigModule(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.NetImportSyncInterval) + .IsValidNetImportSyncInterval(); + } + + protected override NetImportConfigResource ToResource(IConfigService model) + { + return NetImportConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NetImportConfigResource.cs b/src/NzbDrone.Api/Config/NetImportConfigResource.cs new file mode 100644 index 000000000..a1502375c --- /dev/null +++ b/src/NzbDrone.Api/Config/NetImportConfigResource.cs @@ -0,0 +1,21 @@ +using NzbDrone.Api.REST; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Config +{ + public class NetImportConfigResource : RestResource + { + public int NetImportSyncInterval { get; set; } + } + + public static class NetImportConfigResourceMapper + { + public static NetImportConfigResource ToResource(IConfigService model) + { + return new NetImportConfigResource + { + NetImportSyncInterval = model.NetImportSyncInterval + }; + } + } +} diff --git a/src/NzbDrone.Api/Movies/MovieFileModule.cs b/src/NzbDrone.Api/Movies/MovieFileModule.cs index a45fbefad..f8108a45c 100644 --- a/src/NzbDrone.Api/Movies/MovieFileModule.cs +++ b/src/NzbDrone.Api/Movies/MovieFileModule.cs @@ -35,21 +35,21 @@ namespace NzbDrone.Api.EpisodeFiles _seriesService = seriesService; _qualityUpgradableSpecification = qualityUpgradableSpecification; _logger = logger; - /*GetResourceById = GetEpisodeFile; - GetResourceAll = GetEpisodeFiles; + GetResourceById = GetMovieFile; + /*GetResourceAll = GetEpisodeFiles; UpdateResource = SetQuality;*/ + UpdateResource = SetQuality; DeleteResource = DeleteEpisodeFile; } - /*private EpisodeFileResource GetEpisodeFile(int id) + private MovieFileResource GetMovieFile(int id) { - var episodeFile = _mediaFileService.Get(id); - var series = _seriesService.GetSeries(episodeFile.SeriesId); + var episodeFile = _mediaFileService.GetMovie(id); - return episodeFile.ToResource(series, _qualityUpgradableSpecification); + return episodeFile.ToResource(); } - private List GetEpisodeFiles() + /*private List GetEpisodeFiles() { if (!Request.Query.SeriesId.HasValue) { @@ -62,13 +62,13 @@ namespace NzbDrone.Api.EpisodeFiles return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); } - - private void SetQuality(EpisodeFileResource episodeFileResource) + */ + private void SetQuality(MovieFileResource episodeFileResource) { - var episodeFile = _mediaFileService.Get(episodeFileResource.Id); + var episodeFile = _mediaFileService.GetMovie(episodeFileResource.Id); episodeFile.Quality = episodeFileResource.Quality; _mediaFileService.Update(episodeFile); - }*/ + } private void DeleteEpisodeFile(int id) { diff --git a/src/NzbDrone.Api/NetImport/ListImportModule.cs b/src/NzbDrone.Api/NetImport/ListImportModule.cs new file mode 100644 index 000000000..f1d81aefd --- /dev/null +++ b/src/NzbDrone.Api/NetImport/ListImportModule.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using Nancy.Extensions; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Movie; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.NetImport +{ + public class ListImportModule : NzbDroneApiModule + { + private readonly IMovieService _movieService; + private readonly ISearchForNewMovie _movieSearch; + + public ListImportModule(IMovieService movieService, ISearchForNewMovie movieSearch) + : base("/movie/import") + { + _movieService = movieService; + _movieSearch = movieSearch; + Put["/"] = Movie => SaveAll(); + } + + private Response SaveAll() + { + var resources = Request.Body.FromJson>(); + + var Movies = resources.Select(MovieResource => _movieSearch.MapMovieToTmdbMovie(MovieResource.ToModel())).Where(m => m != null).DistinctBy(m => m.TmdbId).ToList(); + + return _movieService.AddMovies(Movies).ToResource().AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/NzbDrone.Api/NetImport/NetImportModule.cs b/src/NzbDrone.Api/NetImport/NetImportModule.cs new file mode 100644 index 000000000..f56d1164a --- /dev/null +++ b/src/NzbDrone.Api/NetImport/NetImportModule.cs @@ -0,0 +1,44 @@ +using NzbDrone.Api.ClientSchema; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.Profiles; + +namespace NzbDrone.Api.NetImport +{ + public class NetImportModule : ProviderModuleBase + { + private readonly IProfileService _profileService; + public NetImportModule(NetImportFactory indexerFactory, IProfileService profileService) + : base(indexerFactory, "netimport") + { + _profileService = profileService; + } + + protected override void MapToResource(NetImportResource resource, NetImportDefinition definition) + { + base.MapToResource(resource, definition); + + resource.Enabled = definition.Enabled; + resource.EnableAuto = definition.EnableAuto; + resource.ProfileId = definition.ProfileId; + resource.RootFolderPath = definition.RootFolderPath; + resource.ShouldMonitor = definition.ShouldMonitor; + } + + protected override void MapToModel(NetImportDefinition definition, NetImportResource resource) + { + base.MapToModel(definition, resource); + + definition.Enabled = resource.Enabled; + definition.EnableAuto = resource.EnableAuto; + definition.ProfileId = resource.ProfileId; + definition.RootFolderPath = resource.RootFolderPath; + definition.ShouldMonitor = resource.ShouldMonitor; + } + + protected override void Validate(NetImportDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NetImport/NetImportResource.cs b/src/NzbDrone.Api/NetImport/NetImportResource.cs new file mode 100644 index 000000000..d46eec57e --- /dev/null +++ b/src/NzbDrone.Api/NetImport/NetImportResource.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.NetImport; + +namespace NzbDrone.Api.NetImport +{ + public class NetImportResource : ProviderResource + { + public bool Enabled { get; set; } + public bool EnableAuto { get; set; } + public bool ShouldMonitor { get; set; } + public string RootFolderPath { get; set; } + public int ProfileId { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index f2cc5280a..d467e397e 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -109,6 +109,8 @@ + + @@ -121,6 +123,9 @@ + + + @@ -234,6 +239,7 @@ + @@ -254,6 +260,7 @@ + @@ -295,4 +302,4 @@ --> - + \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index ee02bcb32..65e560b59 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Api.Profiles { public string Name { get; set; } public Quality Cutoff { get; set; } + public string PreferredTags { get; set; } public List Items { get; set; } public Language Language { get; set; } } @@ -33,6 +34,7 @@ namespace NzbDrone.Api.Profiles Name = model.Name, Cutoff = model.Cutoff, + PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "", Items = model.Items.ConvertAll(ToResource), Language = model.Language }; @@ -59,6 +61,7 @@ namespace NzbDrone.Api.Profiles Name = resource.Name, Cutoff = (Quality)resource.Cutoff.Id, + PreferredTags = resource.PreferredTags.Split(',').ToList(), Items = resource.Items.ConvertAll(ToModel), Language = resource.Language }; diff --git a/src/NzbDrone.Api/Series/FetchMovieListModule.cs b/src/NzbDrone.Api/Series/FetchMovieListModule.cs new file mode 100644 index 000000000..871ebd7bc --- /dev/null +++ b/src/NzbDrone.Api/Series/FetchMovieListModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using System.Linq; +using NzbDrone.Core.NetImport; + +namespace NzbDrone.Api.Movie +{ + public class FetchMovieListModule : NzbDroneRestModule + { + private readonly IFetchNetImport _fetchNetImport; + private readonly ISearchForNewMovie _movieSearch; + + public FetchMovieListModule(IFetchNetImport netImport, ISearchForNewMovie movieSearch) + : base("/netimport/movies") + { + _fetchNetImport = netImport; + _movieSearch = movieSearch; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var results = _fetchNetImport.FetchAndFilter((int) Request.Query.listId, false); + + List realResults = new List(); + + /*foreach (var movie in results) + { + var mapped = _movieSearch.MapMovieToTmdbMovie(movie); + + if (mapped != null) + { + realResults.Add(mapped); + } + }*/ + + return MapToResource(results).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable movies) + { + foreach (var currentSeries in movies) + { + var resource = currentSeries.ToResource(); + var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs b/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs new file mode 100644 index 000000000..b44b3f9e4 --- /dev/null +++ b/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs @@ -0,0 +1,34 @@ +using FluentValidation.Validators; + +namespace NzbDrone.Api.Validation +{ + public class NetImportSyncIntervalValidator : PropertyValidator + { + public NetImportSyncIntervalValidator() + : base("Must be between 10 and 1440 or 0 to disable") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + var value = (int)context.PropertyValue; + + if (value == 0) + { + return true; + } + + if (value >= 10 && value <= 1440) + { + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs index 01a3a4f75..4684d3f12 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs @@ -36,5 +36,10 @@ namespace NzbDrone.Api.Validation { return ruleBuilder.SetValidator(new RssSyncIntervalValidator()); } + + public static IRuleBuilderOptions IsValidNetImportSyncInterval(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator()); + } } } diff --git a/src/NzbDrone.Core.Test/Files/couchpotato_movie_list.json b/src/NzbDrone.Core.Test/Files/couchpotato_movie_list.json new file mode 100644 index 000000000..ba027936b --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/couchpotato_movie_list.json @@ -0,0 +1,449 @@ +{ + "movies": [ + { + "status": "active", + "info": { + "rating": { "imdb": [ 8.1, 228515 ] }, + "genres": [ "Action", "Adventure", "Fantasy", "Science Fiction", "Thriller", "War", "Sci-Fi" ], + "tmdb_id": 330459, + "plot": "A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.", + "tagline": "A Rebellion Built on Hope", + "release_date": { + "dvd": 1461016800, + "expires": 1486410729, + "theater": 1453417200, + "bluray": true + }, + "year": 2016, + "original_title": "Rogue One: A Star Wars Story", + "actor_roles": { + "Warwick Davis": "Bistan", + "Michael Giacchino": "Stormtrooper", + "Lex Lang": "Stormtrooper", + "Samuel Witwer": "Stormtrooper", + "Steen Young": "Vault Officer", + "Russell Balogh": "X-Wing Pilot", + "Alan Tudyk": "K-2SO", + "Angus Cook": "Mechanic", + "David Boat": "Stormtrooper", + "Kevin Hickman": "Stormtrooper", + "Aidan Cook": "Edrio Two Tubes", + "Valene Kane": "Lyra Erso", + "Simon Farnaby": "Blue Squadron", + "Donnie Yen": "Chirrut Imwe", + "Forest Whitaker": "Saw Gerrera", + "Jordan Stephens": "Corporal Tonc", + "Verona Blue": "Stormtrooper", + "David Sobolov": "Stormtrooper", + "Attila G. Kerekes": "Rebel Marine on Yavin", + "Ian McElhinney": "General Dodonna", + "John Gilroy": "Stormtrooper", + "Matthew Wood": "Stormtrooper", + "Jiang Wen": "Baze Malbus", + "Sharon Duncan-Brewster": "Senator Pamlo", + "Christopher Scarabosio": "Stormtrooper", + "Stephen Stanton": "Admiral Raddus (voice)", + "Andrew Zographos": "X-Wing Pilot", + "Ben Daniels": "General Merrick", + "James Arnold Taylor": "Stormtrooper", + "Robin Atkin Downes": "Stormtrooper", + "Guy Henry": "Grand Moff Tarkin", + "Mac Pietowski": "Commi Tech / Marine Soldier", + "James Earl Jones": "Darth Vader (voice)", + "Daniel Naprous": "Darth Vader", + "Geraldine James": "Blue Squadron", + "Eugene Byrd": "Stormtrooper", + "Michael Donovan": "Stormtrooper", + "Paul Kasey": "Admiral Raddus", + "Fred Tatasciore": "Stormtrooper", + "Vanessa Lengies": "Stormtrooper", + "Duncan Pow": "Sergeant Melshi", + "Dolly Gadsdon": "Younger Jyn (as Dolly Gadson)", + "David Acord": "Stormtrooper", + "Nick Kellington": "Bistan", + "Julian Stone": "Stormtrooper", + "Christian Simpson": "Stormtrooper", + "Alistair Petrie": "General Draven", + "Ariyon Bakare": "Blue Squadron", + "Drewe Henley": "Red Leader Garven Dreis", + "Ram Bergman": "Death Star technician", + "Anthony Daniels": "C-3PO", + "Derek Arnold": "Pao", + "Karen Huie": "Stormtrooper", + "Steve Bardrack": "Stormtrooper", + "Jonathan Aris": "Senator Jebel", + "Alexi Melvin": "Stormtroooper", + "Emeson Nwolie": "Personnel", + "Tyrone Love": "Rebel Marine Commander", + "John S. Schwartz": "Stormtrooper", + "Orly Schuchmacher": "Stormtrooper", + "Dave Filoni": "Stormtrooper", + "Yuri Lowenthal": "Stormtrooper", + "Mads Mikkelsen": "Galen Erso", + "Fares Fares": "Senator Vaspar", + "Ian Whyte": "Moroff", + "Genevieve O'Reilly": "Mon Mothma", + "Jorge Leon Martinez": "X-Wing Pilot", + "Beau Gadsdon": "Young Jyn", + "Katie Sheridan": "Stormtrooper", + "Michael Smiley": "Dr. Evazan", + "Babou Ceesay": "Lieutenant Sefla", + "Tom Harrison-Read": "Stormtrooper", + "Spencer Wilding": "Darth Vader", + "Tom Kane": "Stormtrooper", + "Riz Ahmed": "Bodhi Rook", + "Ingvild Deila": "Princess Leia", + "Tony Gilroy": "Stormtrooper", + "Felicity Jones": "Jyn Erso", + "Jonathan Dixon": "Stormtrooper", + "Angus MacInnes": "Gold Leader Dutch Vander", + "William M. Patrick": "Stormtroooper", + "Diego Luna": "Captain Cassian Andor", + "Sam Hanover": "Imperial Officer", + "Jimmy Smits": "Bail Organa", + "Ned Dennehy": "Prisoner", + "Rian Johnson": "Death Star Technician", + "Jimmy Vee": "R2-D2", + "David Cowgill": "Stormtrooper", + "Vanessa Marshall": "Stormtrooper", + "Terri Douglas": "Stormtrooper", + "David Ankrum": "Wedge Antilles", + "Flora Miller": "Stormtroooper", + "Steve Blum": "Stormtrooper", + "Ben Mendelsohn": "Director Orson Krennic" + }, + "via_imdb": true, + "images": { + "disc_art": [], + "poster": [ "https://images-na.ssl-images-amazon.com/images/M/MV5BMjEwMzMxODIzOV5BMl5BanBnXkFtZTgwNzg3OTAzMDI@._V1_SX300.jpg" ], + "backdrop": [ "https://image.tmdb.org/t/p/w1280/tZjVVIYXACV4IIIhXeIM59ytqwS.jpg" ], + "extra_thumbs": [], + "poster_original": [ "https://image.tmdb.org/t/p/original/qjiskwlV1qQzRCjpV0cL9pEMF9a.jpg" ], + "actors": { + "Warwick Davis": "https://image.tmdb.org/t/p/w185/5xBunTQJexQOuCmtlh8MNJerbaM.jpg", + "Michael Giacchino": "https://image.tmdb.org/t/p/w185/2YW8sSVvRhCwiQmsFCgtFsGkbv8.jpg", + "Michael Smiley": "https://image.tmdb.org/t/p/w185/muzJQpsKJ4srfVpyRa7qkrRYWSq.jpg", + "Babou Ceesay": "https://image.tmdb.org/t/p/w185/7HtIvbNxACa03ofJpN4EFQTNtRU.jpg", + "Julian Stone": "https://image.tmdb.org/t/p/w185/sNKqRYXFYHCz8lXExXl0DAl3iGD.jpg", + "Jordan Stephens": "https://image.tmdb.org/t/p/w185/oCQl5rkRExrDhGXNPeSxsmC5wvk.jpg", + "Alistair Petrie": "https://image.tmdb.org/t/p/w185/tC5CHVPnxAMqF0W0csTqcDAawwj.jpg", + "Samuel Witwer": "https://image.tmdb.org/t/p/w185/e4FRojd6SmiyRLo2nQQGUXwi16v.jpg", + "Ben Daniels": "https://image.tmdb.org/t/p/w185/x6MI4Fdz1XbERbNbXYoxTK6NAgv.jpg", + "Ariyon Bakare": "https://image.tmdb.org/t/p/w185/xjJlH9hU58Ocy6GxKfBlEvTif1p.jpg", + "James Arnold Taylor": "https://image.tmdb.org/t/p/w185/rAtyfY0diWt078qQIg0IX9xxG9F.jpg", + "Robin Atkin Downes": "https://image.tmdb.org/t/p/w185/pCnIQMMgrFc4hBOE4LJDdebqRZ4.jpg", + "Drewe Henley": "https://image.tmdb.org/t/p/w185/C28FmnpDyhI9BwD6YjagAe1U53.jpg", + "Spencer Wilding": "https://image.tmdb.org/t/p/w185/g3FJIpQZri7gG515rLehuo81T6W.jpg", + "Alan Tudyk": "https://image.tmdb.org/t/p/w185/6QuMtbD8kmhpwWhFKfNzEvHRLOu.jpg", + "Guy Henry": "https://image.tmdb.org/t/p/w185/zNjPC6BTZj7DZK4KFL0nMC1El2S.jpg", + "Angus Cook": "https://image.tmdb.org/t/p/w185/jPc794vF0h8bmslQ3sO8O3vUVIa.jpg", + "David Boat": "https://image.tmdb.org/t/p/w185/4ewxttZW0bhlta27oc5Tjrxel3p.jpg", + "Tom Kane": "https://image.tmdb.org/t/p/w185/hAyEHNuhD6PqbPdCNR7iUyM271I.jpg", + "Anthony Daniels": "https://image.tmdb.org/t/p/w185/cljvryjb3VwTsNR7fjQKjNPMaBB.jpg", + "Duncan Pow": "https://image.tmdb.org/t/p/w185/vJOzoMzxszyZGnySfql3KY9zR78.jpg", + "Fares Fares": "https://image.tmdb.org/t/p/w185/1BE5IG3hcFXfMjBuJJyKs2JpPjI.jpg", + "Tony Gilroy": "https://image.tmdb.org/t/p/w185/9HOtDgcO6F4Fa4BaIjt0t3Vbxrj.jpg", + "Felicity Jones": "https://image.tmdb.org/t/p/w185/9YekpRl6ndS7zpY0wwZAWcAXkl8.jpg", + "Eugene Byrd": "https://image.tmdb.org/t/p/w185/ab4zEcqdBSjpaz4CPQ2Z6q4rLmO.jpg", + "Jonathan Aris": "https://image.tmdb.org/t/p/w185/6RMuwGYfLLGq01LNGBydj9jpTWn.jpg", + "Valene Kane": "https://image.tmdb.org/t/p/w185/7TcV6HqGXjf28yjuSU42Z5XZRYb.jpg", + "Angus MacInnes": "https://image.tmdb.org/t/p/w185/qftkol8hj7yBBP3KCxRWYkhRyLC.jpg", + "James Earl Jones": "https://image.tmdb.org/t/p/w185/2ZuBf3ip2RXhkiQqGUjbUzAf4Nx.jpg", + "Emeson Nwolie": "https://image.tmdb.org/t/p/w185/dWCOK3qCOm1Vve567FXKhBp5x8B.jpg", + "Terri Douglas": "https://image.tmdb.org/t/p/w185/lECiABogAKm5Zl8Je6niNAoqz5N.jpg", + "Simon Farnaby": "https://image.tmdb.org/t/p/w185/3u1ObLUvaTyEMmpWQnkRg5Trlng.jpg", + "Donnie Yen": "https://image.tmdb.org/t/p/w185/vlKBbOc0htUsDGvcxeULcFXDMRo.jpg", + "Forest Whitaker": "https://image.tmdb.org/t/p/w185/4pMQkelS5lK661m9Kz3oIxLYiyS.jpg", + "Diego Luna": "https://image.tmdb.org/t/p/w185/9f1y0pLqohP8U3eEVCa4di1tESb.jpg", + "Dave Filoni": "https://image.tmdb.org/t/p/w185/1m7ijGgs29Emn3Sj08c1GwGTUm0.jpg", + "Jimmy Smits": "https://image.tmdb.org/t/p/w185/tZfr6EaIxzlT9MhY5T4C6cL3UjF.jpg", + "Yuri Lowenthal": "https://image.tmdb.org/t/p/w185/d5vbYEkrPYAiVdTee8e4xCm7Fg1.jpg", + "Verona Blue": "https://image.tmdb.org/t/p/w185/9UJiyVd65nGCVLsTuFjtF3ejCqa.jpg", + "David Sobolov": "https://image.tmdb.org/t/p/w185/lUXbnlyQPsfAGg0oinCtj6KlOkt.jpg", + "Ned Dennehy": "https://image.tmdb.org/t/p/w185/k4kgPvUND2eTrgmotrVWVJM0JUG.jpg", + "Ian McElhinney": "https://image.tmdb.org/t/p/w185/33RGircMDTbdvD6LUp8sLmQKWvA.jpg", + "Fred Tatasciore": "https://image.tmdb.org/t/p/w185/lNe4zn9fJ302GehQVaFk5BNcGGM.jpg", + "Mads Mikkelsen": "https://image.tmdb.org/t/p/w185/nJjN0bS6ssbOrXcnPJrNEIsbX9s.jpg", + "Paul Kasey": "https://image.tmdb.org/t/p/w185/56f0ouOg2ASKKKZlaywor8E5V3J.jpg", + "David Cowgill": "https://image.tmdb.org/t/p/w185/kcGjj4EuHfMp0VILRVoacoPqNFL.jpg", + "Ian Whyte": "https://image.tmdb.org/t/p/w185/6mRY7hTtHfDTGuTLmZmODOu9buF.jpg", + "Genevieve O'Reilly": "https://image.tmdb.org/t/p/w185/8NrrFxrGng88GU7lxwOyK3PZv05.jpg", + "Jorge Leon Martinez": "https://image.tmdb.org/t/p/w185/nWYveATaySCXosWAjcSS8VNPRe7.jpg", + "Katie Sheridan": "https://image.tmdb.org/t/p/w185/awNPsff9HU7NgAhG1qQ4Kh7pMmj.jpg", + "Vanessa Marshall": "https://image.tmdb.org/t/p/w185/wOXilt4TVOd0LuTw6RbWhe5DUy4.jpg", + "Vanessa Lengies": "https://image.tmdb.org/t/p/w185/vU4syqfb0PYE9efbBq9YZQu24cY.jpg", + "David Ankrum": "https://image.tmdb.org/t/p/w185/vo6JMA38exMSSbyQ3K0YCBwBrWT.jpg", + "Riz Ahmed": "https://image.tmdb.org/t/p/w185/yWjuIP634unLBCB4XjSgmJs5QGC.jpg", + "Steve Blum": "https://image.tmdb.org/t/p/w185/asCL6bWSZ7Xl2kSoRqrPB0CUUUU.jpg", + "Rian Johnson": "https://image.tmdb.org/t/p/w185/qWWRFkeMjTjQKoyEXhsV0QQp4qd.jpg", + "Matthew Wood": "https://image.tmdb.org/t/p/w185/oB9wVbEIg8fjY3ulDKjKsGn2A55.jpg", + "Jiang Wen": "https://image.tmdb.org/t/p/w185/sLLXxXg11VFdVYFthF9RB8wIQKv.jpg", + "Ben Mendelsohn": "https://image.tmdb.org/t/p/w185/nAeZkSUXh9CUAUq1cFAg77rZLIS.jpg", + "Geraldine James": "https://image.tmdb.org/t/p/w185/iHKFccX2qpSzMbhIBdfvr835MVg.jpg", + "Russell Balogh": "https://image.tmdb.org/t/p/w185/yCfE3Pf1npGB15Rw8GHt4nvgK6p.jpg" + }, + "backdrop_original": [ "https://image.tmdb.org/t/p/original/tZjVVIYXACV4IIIhXeIM59ytqwS.jpg" ], + "clear_art": [], + "logo": [], + "banner": [], + "landscape": [], + "extra_fanart": [] + }, + "directors": [ "Gareth Edwards" ], + "titles": [ "Rogue One: A Star Wars Story", "Rogue One", "Star Wars: Rogue One", "Star Wars Anthology: Rogue One", "Rogue One: Uma História Star Wars", "星際大戰外傳:俠盜一號", "Rogue One - A Star Wars Story", "星球大战外传:侠盗一号", "Rogue One: История от Междузвездни войни", "Star Wars - Rouge One" ], + "imdb": "tt3748528", + "mpaa": "PG-13", + "via_tmdb": true, + "actors": [ "Felicity Jones", "Diego Luna", "Alan Tudyk", "Donnie Yen" ], + "writers": [ "Chris Weitz (screenplay)", "Tony Gilroy (screenplay)", "John Knoll (story by)", "Gary Whitta (story by)", "George Lucas (based on characters created by)" ], + "runtime": 133, + "type": "movie", + "released": "16 Dec 2016" + }, + "_t": "media", + "releases": [], + "title": "Rogue One: A Star Wars Story", + "_rev": "00030f77", + "profile_id": "38699ec285c447bab0bc6267ffb2f3ad", + "_id": "d9d4e0ff9b0842518b9d5f5184a60f31", + "category_id": null, + "type": "movie", + "files": { "image_poster": [ "C:\\Users\\devin\\AppData\\Roaming\\CouchPotato\\cache\\2100049b45a923e858dd161ae28b1f4d.jpg" ] }, + "identifiers": { "imdb": "tt3748528" } + }, + { + "status": "active", + "info": { + "rating": { "imdb": [ 7.3, 16900 ] }, + "genres": [ "Animation", "Comedy", "Family", "Music", "Drama" ], + "tmdb_id": 335797, + "plot": "In a city of humanoid animals, a hustling theater impresario's attempt to save his theater with a singing competition becomes grander than he anticipates even as its finalists' find that their lives will never be the same.", + "tagline": "Auditions begin 2016.", + "release_date": { + "dvd": 1490997600, + "expires": 1485114888, + "theater": 1482274800, + "bluray": true + }, + "year": 2016, + "original_title": "Sing", + "actor_roles": { + "Taron Egerton": "Johnny (voice)", + "Catherine Cavadini": "Additional Voices (voice)", + "Beck Bennett": "Lance (voice)", + "Rhea Perlman": "Judith (voice)", + "Jon Robert Hall": "Frog (voice)", + "Abby Craden": "Additional Voices (voice)", + "Jim Cummings": "Additional Voices (voice)", + "Peter Serafinowicz": "Big Daddy (voice)", + "Bill Farmer": "News Reporter Dog (voice)", + "Jessica Rau": "Additional Voices (voice)", + "Townsend Coleman": "Additional Voices (voice)", + "Jen Faith Brown": "Singer (voice)", + "Brad Morris": "Baboon (voice)", + "Doug Burch": "Additional Voices (voice)", + "Jennifer Hudson": "Young Nana (voice)", + "Laura Dickinson": "Spider (voice)", + "Jeremy Maxwell": "Additional Voices (voice)", + "Asher Blinkoff": "Piglet (voice)", + "Reese Witherspoon": "Rosita (voice)", + "Scarlett Johansson": "Ash (voice)", + "Carlos Alazraqui": "Additional Voices (voice)", + "Edgar Wright": "Additional Voices (voice)", + "Asa Jennings": "Piglet (voice)", + "Nick Offerman": "Norman (voice)", + "Mickael Carreira": "Voice 3", + "Sara Mann": "Additional Voices (voice)", + "Jay Pharoah": "Meena's Grandfather (voice)", + "Adam Buxton": "Stan (voice)", + "Garth Jennings": "Miss Crawly / Additional Voices (voice)", + "Deolinda Kinzimba": "Voice 4", + "Jess Harnell": "Additional Voices (voice)", + "Bob Bergen": "Additional Voices (voice)", + "Leslie Jones": "Meena's Mother (voice)", + "Chris Renaud": "Additional Voices (voice)", + "Nick Kroll": "Gunter (voice)", + "Seth MacFarlane": "Mike (voice)", + "Marisa Liz": "Voice 2", + "Áurea": "Voice 1", + "Leo Jennings": "Piglet (voice)", + "Oscar Jennings": "Piglet (voice)", + "Tara Strong": "Additional Voices (voice)", + "John C. Reilly": "Eddie (voice)", + "Matthew McConaughey": "Buster Moon (voice)", + "Caspar Jennings": "Piglet (voice)", + "Daamen J. Krall": "Additional Voices (voice)", + "Tori Kelly": "Meena (voice)", + "Laraine Newman": "Meena's Grandmother / Additional Voices (voice)", + "Willow Geer": "Additional Voices (voice)", + "Wes Anderson": "Additional Voices (voice)", + "Jason Pace": "Additional Voices (voice)", + "Jennifer Saunders": "Nana (voice)", + "John DeMita": "Additional Voices (voice)" + }, + "via_imdb": true, + "images": { + "disc_art": [], + "poster": [ "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYzODYzODU2Ml5BMl5BanBnXkFtZTgwNTc1MTA2NzE@._V1_SX300.jpg" ], + "backdrop": [ "https://image.tmdb.org/t/p/w1280/fxDXp8un4qNY9b1dLd7SH6CKzC.jpg" ], + "extra_thumbs": [], + "poster_original": [ "https://image.tmdb.org/t/p/original/5XFchtGifv8mz4qlyT8PZ7ZsjfG.jpg" ], + "actors": { + "Taron Egerton": "https://image.tmdb.org/t/p/w185/bVsLVoO3BGoHRLjWoM4Gjav2hNb.jpg", + "Catherine Cavadini": "https://image.tmdb.org/t/p/w185/o2wULQltvbzCTCJitNeT72AjklR.jpg", + "Beck Bennett": "https://image.tmdb.org/t/p/w185/oblaqelpyBvtB5GaSgQpDrfka9M.jpg", + "Daamen J. Krall": "https://image.tmdb.org/t/p/w185/u0CORJ8e2vvw1dFARU4estHYS2I.jpg", + "Rhea Perlman": "https://image.tmdb.org/t/p/w185/cq7Cf4z3BHD9o58ki7MgCioty8q.jpg", + "Abby Craden": "https://image.tmdb.org/t/p/w185/biX1xErOEwsuRvidr8Pw6edEyK4.jpg", + "Jim Cummings": "https://image.tmdb.org/t/p/w185/i9frXvIJsGtoFikBEFVqE7uN8Bq.jpg", + "Peter Serafinowicz": "https://image.tmdb.org/t/p/w185/nfXHDKeetwO16agC0S7tDmLt1il.jpg", + "Bill Farmer": "https://image.tmdb.org/t/p/w185/4aDBlkt8nEkr1RkEhiKIbDWhpZB.jpg", + "Jessica Rau": "https://image.tmdb.org/t/p/w185/jBbIYc3UQf7JU8ggQVkfezpmgVZ.jpg", + "Townsend Coleman": "https://image.tmdb.org/t/p/w185/j7PvxQ7XuOQc1ggSRHWRP6CB8CU.jpg", + "Brad Morris": "https://image.tmdb.org/t/p/w185/qX6oVdAt7Vzzcnw28bdXFp05BBH.jpg", + "Doug Burch": "https://image.tmdb.org/t/p/w185/zwfqhPuIFrUL70bWPESdJZWXc7F.jpg", + "Jennifer Hudson": "https://image.tmdb.org/t/p/w185/zqTu7AANIUsVMAYz5rK1YPnvbWR.jpg", + "Asher Blinkoff": "https://image.tmdb.org/t/p/w185/780sIDWQoAIVVaUbAQex50Vam0V.jpg", + "Reese Witherspoon": "https://image.tmdb.org/t/p/w185/a3o8T1P6yy4KWL7wZG6HuDeuh5n.jpg", + "Scarlett Johansson": "https://image.tmdb.org/t/p/w185/f3c1rwcOoeU0v6Ak5loUvMyifR0.jpg", + "Carlos Alazraqui": "https://image.tmdb.org/t/p/w185/o62NevO1Vt9n1MdYsWOsDyhUt3A.jpg", + "Nick Offerman": "https://image.tmdb.org/t/p/w185/8rJOtmxL5GIfNdOfksVPzepQOy2.jpg", + "Sara Mann": "https://image.tmdb.org/t/p/w185/1TiV16ODOJtTZQrWmHRwOyQnMb0.jpg", + "Jay Pharoah": "https://image.tmdb.org/t/p/w185/yRD2vypRF0niEdoCCI0pNZENzvm.jpg", + "Tara Strong": "https://image.tmdb.org/t/p/w185/rFUZnJ4BaSaQVKW734xnUHSN9pm.jpg", + "Garth Jennings": "https://image.tmdb.org/t/p/w185/ahQh5uW5CXLe1LotxN4Y20aj5Gx.jpg", + "Jess Harnell": "https://image.tmdb.org/t/p/w185/k0BOzEyMkZ1CcoCaohjqTyQJjP1.jpg", + "Leslie Jones": "https://image.tmdb.org/t/p/w185/2cXrwJoX0QHGBtNMsMLqeF6bR3s.jpg", + "Chris Renaud": "https://image.tmdb.org/t/p/w185/yK3RxNsIEBljUe9jPG0iz53Iz6t.jpg", + "Nick Kroll": "https://image.tmdb.org/t/p/w185/puZov7sMmuVkvdqJvmlxtWcS1fU.jpg", + "Seth MacFarlane": "https://image.tmdb.org/t/p/w185/v4c6JhGYpjMRBwf95gtPxBnElNu.jpg", + "Bob Bergen": "https://image.tmdb.org/t/p/w185/kuWDjNTw6OVnc3q1ugMGBYpMMMa.jpg", + "Edgar Wright": "https://image.tmdb.org/t/p/w185/ypyH2s4egy5BkviuGDfeltpb19N.jpg", + "Matthew McConaughey": "https://image.tmdb.org/t/p/w185/jdRmHrG0TWXGhs4tO6TJNSoL25T.jpg", + "John C. Reilly": "https://image.tmdb.org/t/p/w185/kUo2TPQp4kOWWvijvkjLl0v9PQB.jpg", + "Adam Buxton": "https://image.tmdb.org/t/p/w185/zL31NlBBKL1NTjR48h610by5Rld.jpg", + "Tori Kelly": "https://image.tmdb.org/t/p/w185/dMyLOIOYqTMQtMEiK9DSxxHTz6F.jpg", + "Laraine Newman": "https://image.tmdb.org/t/p/w185/ApYftBOqDMBnVColOQwXIodOt5s.jpg", + "Willow Geer": "https://image.tmdb.org/t/p/w185/q2TjAxrQSpPPUiTUwFBXcLJ7qxc.jpg", + "Wes Anderson": "https://image.tmdb.org/t/p/w185/r6mr3gvbuocMznHXSlXVKDj7mEI.jpg", + "Jason Pace": "https://image.tmdb.org/t/p/w185/2q6KfNytYUiHuf8Rx9HyBGoD1T7.jpg", + "Jennifer Saunders": "https://image.tmdb.org/t/p/w185/nlxiFy0LUYGlICaFY3rF2DRovcc.jpg", + "John DeMita": "https://image.tmdb.org/t/p/w185/lzwHtcKVd5oenYtoFtJYeNddpwT.jpg" + }, + "backdrop_original": [ "https://image.tmdb.org/t/p/original/fxDXp8un4qNY9b1dLd7SH6CKzC.jpg" ], + "clear_art": [], + "logo": [], + "banner": [], + "landscape": [], + "extra_fanart": [] + }, + "directors": [ "Christophe Lourdelet", "Garth Jennings" ], + "titles": [ "Sing", "Welcome to the Auditions" ], + "imdb": "tt3470600", + "mpaa": "PG", + "via_tmdb": true, + "actors": [ "Matthew McConaughey", "Reese Witherspoon", "Seth MacFarlane", "Scarlett Johansson" ], + "writers": [ "Garth Jennings" ], + "runtime": 110, + "type": "movie", + "released": "21 Dec 2016" + }, + "_t": "media", + "releases": [], + "title": "Sing", + "_rev": "00031b86", + "profile_id": "38699ec285c447bab0bc6267ffb2f3ad", + "_id": "f12dc6bbff294daa85db0d839646442a", + "category_id": null, + "type": "movie", + "files": { "image_poster": [ "C:\\Users\\devin\\AppData\\Roaming\\CouchPotato\\cache\\2ad327d73e8ef4deab7a4b564d3b9cb4.jpg" ] }, + "identifiers": { "imdb": "tt3470600" } + }, + { + "status": "active", + "info": { + "rating": { "imdb": [ 6.4, 10027 ] }, + "genres": [ "Action", "Horror" ], + "tmdb_id": 346672, + "plot": "Vampire death dealer Selene fends off brutal attacks from both the Lycan clan and the Vampire faction that betrayed her. With her only allies, David and his father Thomas, she must stop the eternal war between Lycans and Vampires, even if it means she has to make the ultimate sacrifice.", + "tagline": "Protect the Bloodline", + "release_date": { + "dvd": 1493589600, + "expires": 1485114954, + "theater": 1483657200, + "bluray": true + }, + "year": 2016, + "original_title": "Underworld: Blood Wars", + "actor_roles": { + "India Eisley": "Eve", + "Kate Beckinsale": "Selene", + "Oliver Stark": "Gregor", + "Brian Caspe": "Hajna", + "Charles Dance": "Thomas", + "Alicia Vela-Bailey": "Safehouse Lycan", + "Bradley James": "Varga", + "David Bowles": "Grey Lycan", + "Theo James": "David", + "Lara Pulver": "Semira", + "Eva Larvoire": "Tech Lycan", + "Tobias Menzies": "Marius", + "Daisy Head": "Alexia", + "Trent Garrett": "Hybrid Michael" + }, + "via_imdb": true, + "images": { + "disc_art": [], + "poster": [ "https://images-na.ssl-images-amazon.com/images/M/MV5BMjI5Njk0NTIyNV5BMl5BanBnXkFtZTgwNjU4MjY5MDI@._V1_SX300.jpg" ], + "backdrop": [ "https://image.tmdb.org/t/p/w1280/PIXSMakrO3s2dqA7mCvAAoVR0E.jpg" ], + "extra_thumbs": [], + "poster_original": [ "https://image.tmdb.org/t/p/original/nHXiMnWUAUba2LZ0dFkNDVdvJ1o.jpg" ], + "actors": { + "India Eisley": "https://image.tmdb.org/t/p/w185/njL744BT8mz9jf2TxcZDnSOEZFb.jpg", + "Kate Beckinsale": "https://image.tmdb.org/t/p/w185/pTRtcZn9gWQZRiet36qWKh94urn.jpg", + "Oliver Stark": "https://image.tmdb.org/t/p/w185/5yULYfaUMymZdSLhk2W96hZIQBP.jpg", + "Brian Caspe": "https://image.tmdb.org/t/p/w185/1fDVsCwZOwp97Pdl7q743seHCMP.jpg", + "Charles Dance": "https://image.tmdb.org/t/p/w185/bLT03rnI29YmbYWjA1JJCl4xVXw.jpg", + "Alicia Vela-Bailey": "https://image.tmdb.org/t/p/w185/kVuyn6sS7ZSBlXVjjxq0LSE3k4I.jpg", + "Bradley James": "https://image.tmdb.org/t/p/w185/4XAtJsz67pmpIsCQ9SBKfqayk2d.jpg", + "Trent Garrett": "https://image.tmdb.org/t/p/w185/w9J2snV7QI71B5F7rCxfPqeS7GU.jpg", + "Theo James": "https://image.tmdb.org/t/p/w185/hLNSoQ3gc52X5VVb172yO3CuUEq.jpg", + "Eva Larvoire": "https://image.tmdb.org/t/p/w185/Aq96CWP3Pub2CdWSNbL5eaTwRt0.jpg", + "Tobias Menzies": "https://image.tmdb.org/t/p/w185/bXUpxFsIowySRyyqchaE1XprptI.jpg", + "Daisy Head": "https://image.tmdb.org/t/p/w185/33JAZTxDWj646mxdW1HksqHOsiY.jpg", + "Lara Pulver": "https://image.tmdb.org/t/p/w185/ve68vtNYVXmKjzn81zKhI7TWEvy.jpg" + }, + "backdrop_original": [ "https://image.tmdb.org/t/p/original/PIXSMakrO3s2dqA7mCvAAoVR0E.jpg" ], + "clear_art": [], + "logo": [], + "banner": [], + "landscape": [], + "extra_fanart": [] + }, + "directors": [ "Anna Foerster" ], + "titles": [ "Underworld: Blood Wars", "Inframundo: Guerras de Sangre", "Anjos da Noite: Guerras de Sangue", "Underworld Reboot", "Underworld: Next Generation", "決戰異世界:弒血之戰", "Інший світ 5: Кровна помста", "Інший світ 5", "Underworld 5 - Blood Wars" ], + "imdb": "tt3717252", + "mpaa": "R", + "via_tmdb": true, + "actors": [ "Kate Beckinsale", "Theo James", "Tobias Menzies", "Lara Pulver" ], + "writers": [ "Cory Goodman (screenplay)", "Kyle Ward (story by)", "Cory Goodman (story by)", "Kevin Grevioux (based on characters created by)", "Len Wiseman (based on characters created by)", "Danny McBride (based on characters created by)" ], + "runtime": 91, + "type": "movie", + "released": "06 Jan 2017" + }, + "_t": "media", + "releases": [], + "title": "Underworld: Blood Wars", + "_rev": "00037887", + "profile_id": "38699ec285c447bab0bc6267ffb2f3ad", + "_id": "4040237fdbd349629a51e29e8ff634f2", + "category_id": null, + "type": "movie", + "files": { "image_poster": [ "C:\\Users\\devin\\AppData\\Roaming\\CouchPotato\\cache\\e41f29a177dd6756dce94f24148c81fe.jpg" ] }, + "identifiers": { "imdb": "tt3717252" } + } + ], + "total": 3, + "empty": false, + "success": true +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml b/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml new file mode 100644 index 000000000..b49485a6c --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml @@ -0,0 +1,1760 @@ + + + + Movie Watchlist + http://www.imdb.com/list/ls005547488/ + + Fri, 15 Jul 2011 05:14:06 GMT + Tue, 25 Mar 2014 02:22:29 GMT + + Tue, 25 Mar 2014 02:22:29 GMT + Think Like a Man Too (2014) + http://www.imdb.com/title/tt2239832/ + http://www.imdb.com/title/tt2239832/ + + + + Tue, 25 Mar 2014 00:30:49 GMT + The Machine (2013) + http://www.imdb.com/title/tt2317225/ + http://www.imdb.com/title/tt2317225/ + + + + Sun, 23 Mar 2014 07:51:40 GMT + The Great Beauty (2013) + http://www.imdb.com/title/tt2358891/ + http://www.imdb.com/title/tt2358891/ + + + + Sun, 23 Mar 2014 07:51:03 GMT + A Touch of Sin (2013) + http://www.imdb.com/title/tt2852400/ + http://www.imdb.com/title/tt2852400/ + + + + Sun, 23 Mar 2014 07:49:12 GMT + All Is Lost (2013) + http://www.imdb.com/title/tt2017038/ + http://www.imdb.com/title/tt2017038/ + + + + Sat, 22 Mar 2014 05:07:32 GMT + Nymphomaniac: Vol. II (2013) + http://www.imdb.com/title/tt2382009/ + http://www.imdb.com/title/tt2382009/ + + + + Sat, 22 Mar 2014 05:07:18 GMT + The Maze Runner (2014) + http://www.imdb.com/title/tt1790864/ + http://www.imdb.com/title/tt1790864/ + + + + Thu, 16 Jan 2014 04:57:39 GMT + Winter's Tale (2014) + http://www.imdb.com/title/tt1837709/ + http://www.imdb.com/title/tt1837709/ + + + + Thu, 16 Jan 2014 04:50:58 GMT + Love at First Sight (2010 Short Film) + http://www.imdb.com/title/tt1735878/ + http://www.imdb.com/title/tt1735878/ + + + + Thu, 16 Jan 2014 04:47:51 GMT + Run & Jump (2013) + http://www.imdb.com/title/tt2343158/ + http://www.imdb.com/title/tt2343158/ + + + + Thu, 16 Jan 2014 04:45:23 GMT + The Railway Man (2013) + http://www.imdb.com/title/tt2058107/ + http://www.imdb.com/title/tt2058107/ + + + + Thu, 16 Jan 2014 04:41:47 GMT + Welcome to the Jungle (2013) + http://www.imdb.com/title/tt2193265/ + http://www.imdb.com/title/tt2193265/ + + + + Thu, 16 Jan 2014 04:38:26 GMT + Le Week-End (2013) + http://www.imdb.com/title/tt2392326/ + http://www.imdb.com/title/tt2392326/ + + + + Thu, 16 Jan 2014 04:31:57 GMT + Labor Day (2013) + http://www.imdb.com/title/tt1967545/ + http://www.imdb.com/title/tt1967545/ + + + + Thu, 16 Jan 2014 04:05:40 GMT + Grand Piano (2013) + http://www.imdb.com/title/tt2039345/ + http://www.imdb.com/title/tt2039345/ + + + + Thu, 16 Jan 2014 04:05:05 GMT + Gloria (2013) + http://www.imdb.com/title/tt2425486/ + http://www.imdb.com/title/tt2425486/ + + + + Thu, 16 Jan 2014 04:04:21 GMT + Gimme Shelter (2013) + http://www.imdb.com/title/tt1657510/ + http://www.imdb.com/title/tt1657510/ + + + + Thu, 16 Jan 2014 04:01:29 GMT + The Past (2013) + http://www.imdb.com/title/tt2404461/ + http://www.imdb.com/title/tt2404461/ + + + + Thu, 16 Jan 2014 04:00:49 GMT + Fading Gigolo (2013) + http://www.imdb.com/title/tt2258345/ + http://www.imdb.com/title/tt2258345/ + + + + Thu, 16 Jan 2014 04:00:18 GMT + Edge of Tomorrow (2014) + http://www.imdb.com/title/tt1631867/ + http://www.imdb.com/title/tt1631867/ + + + + Thu, 16 Jan 2014 03:58:29 GMT + Earth to Echo (2014) + http://www.imdb.com/title/tt2183034/ + http://www.imdb.com/title/tt2183034/ + + + + Thu, 16 Jan 2014 03:56:30 GMT + Drew: The Man Behind the Poster (2013 Documentary) + http://www.imdb.com/title/tt1486843/ + http://www.imdb.com/title/tt1486843/ + + + + Thu, 16 Jan 2014 03:55:16 GMT + Doomsdays (2013) + http://www.imdb.com/title/tt2395146/ + http://www.imdb.com/title/tt2395146/ + + + + Thu, 16 Jan 2014 03:52:31 GMT + Design Is One: The Vignellis (2012 Documentary) + http://www.imdb.com/title/tt2610862/ + http://www.imdb.com/title/tt2610862/ + + + + Thu, 16 Jan 2014 03:51:37 GMT + Eastern Promises (2007) + http://www.imdb.com/title/tt0765443/ + http://www.imdb.com/title/tt0765443/ + + + + Thu, 16 Jan 2014 03:50:43 GMT + The Machinist (2004) + http://www.imdb.com/title/tt0361862/ + http://www.imdb.com/title/tt0361862/ + + + + Thu, 16 Jan 2014 03:49:51 GMT + eXistenZ (1999) + http://www.imdb.com/title/tt0120907/ + http://www.imdb.com/title/tt0120907/ + + + + Thu, 16 Jan 2014 03:49:26 GMT + Courage Under Fire (1996) + http://www.imdb.com/title/tt0115956/ + http://www.imdb.com/title/tt0115956/ + + + + Thu, 16 Jan 2014 03:45:04 GMT + Cosmopolis (2012) + http://www.imdb.com/title/tt1480656/ + http://www.imdb.com/title/tt1480656/ + + + + Thu, 16 Jan 2014 03:44:27 GMT + Concussion (2013) + http://www.imdb.com/title/tt2296697/ + http://www.imdb.com/title/tt2296697/ + + + + Thu, 16 Jan 2014 03:43:05 GMT + Closed Curtain (2013) + http://www.imdb.com/title/tt2626926/ + http://www.imdb.com/title/tt2626926/ + + + + Thu, 16 Jan 2014 03:42:25 GMT + Charlie Countryman (2013) + http://www.imdb.com/title/tt1196948/ + http://www.imdb.com/title/tt1196948/ + + + + Thu, 16 Jan 2014 03:41:49 GMT + Captain America: The Winter Soldier (2014) + http://www.imdb.com/title/tt1843866/ + http://www.imdb.com/title/tt1843866/ + + + + Thu, 16 Jan 2014 03:40:59 GMT + Blue Is the Warmest Color (2013) + http://www.imdb.com/title/tt2278871/ + http://www.imdb.com/title/tt2278871/ + + + + Thu, 16 Jan 2014 03:39:37 GMT + Blind Detective (2013) + http://www.imdb.com/title/tt2332707/ + http://www.imdb.com/title/tt2332707/ + + + + Thu, 16 Jan 2014 03:38:05 GMT + Blended (2014) + http://www.imdb.com/title/tt1086772/ + http://www.imdb.com/title/tt1086772/ + + + + Thu, 16 Jan 2014 03:37:38 GMT + Big Bad Wolves (2013) + http://www.imdb.com/title/tt2309224/ + http://www.imdb.com/title/tt2309224/ + + + + Thu, 16 Jan 2014 03:36:35 GMT + Barefoot (2014) + http://www.imdb.com/title/tt2355495/ + http://www.imdb.com/title/tt2355495/ + + + + Thu, 16 Jan 2014 03:35:13 GMT + Bad Words (2013) + http://www.imdb.com/title/tt2170299/ + http://www.imdb.com/title/tt2170299/ + + + + Thu, 16 Jan 2014 03:34:27 GMT + A Fantastic Fear of Everything (2012) + http://www.imdb.com/title/tt2006040/ + http://www.imdb.com/title/tt2006040/ + + + + Thu, 16 Jan 2014 01:21:34 GMT + A Field in England (2013) + http://www.imdb.com/title/tt2375574/ + http://www.imdb.com/title/tt2375574/ + + + + Thu, 16 Jan 2014 01:21:14 GMT + Odd Thomas (2013) + http://www.imdb.com/title/tt1767354/ + http://www.imdb.com/title/tt1767354/ + + + + Thu, 16 Jan 2014 01:14:36 GMT + The Pretty One (2013) + http://www.imdb.com/title/tt2140577/ + http://www.imdb.com/title/tt2140577/ + + + + Thu, 16 Jan 2014 01:08:37 GMT + Awful Nice (2013) + http://www.imdb.com/title/tt1414449/ + http://www.imdb.com/title/tt1414449/ + + + + Wed, 15 Jan 2014 23:10:34 GMT + 50 to 1 (2014) + http://www.imdb.com/title/tt1777595/ + http://www.imdb.com/title/tt1777595/ + + + + Wed, 15 Jan 2014 23:09:57 GMT + $50K and a Call Girl: A Love Story (2014) + http://www.imdb.com/title/tt2106284/ + http://www.imdb.com/title/tt2106284/ + + + + Fri, 10 Jan 2014 04:48:44 GMT + Interstellar (2014) + http://www.imdb.com/title/tt0816692/ + http://www.imdb.com/title/tt0816692/ + + + + Fri, 10 Jan 2014 04:44:18 GMT + 3 Days to Kill (2014) + http://www.imdb.com/title/tt2172934/ + http://www.imdb.com/title/tt2172934/ + + + + Fri, 10 Jan 2014 04:40:50 GMT + Back in the Day (2014) + http://www.imdb.com/title/tt2246887/ + http://www.imdb.com/title/tt2246887/ + + + + Fri, 10 Jan 2014 04:36:30 GMT + 300: Rise of an Empire (2014) + http://www.imdb.com/title/tt1253863/ + http://www.imdb.com/title/tt1253863/ + + + + Fri, 10 Jan 2014 04:28:56 GMT + Small Time (2014) + http://www.imdb.com/title/tt2310109/ + http://www.imdb.com/title/tt2310109/ + + + + Fri, 10 Jan 2014 04:24:20 GMT + The Grand Budapest Hotel (2014) + http://www.imdb.com/title/tt2278388/ + http://www.imdb.com/title/tt2278388/ + + + + Fri, 10 Jan 2014 04:10:34 GMT + Dumbbells (2014) + http://www.imdb.com/title/tt1978428/ + http://www.imdb.com/title/tt1978428/ + + + + Fri, 10 Jan 2014 04:05:22 GMT + Dawn of the Planet of the Apes (2014) + http://www.imdb.com/title/tt2103281/ + http://www.imdb.com/title/tt2103281/ + + + + Fri, 22 Nov 2013 02:30:55 GMT + Beyond Outrage (2012) + http://www.imdb.com/title/tt1724962/ + http://www.imdb.com/title/tt1724962/ + + + + Fri, 22 Nov 2013 02:30:06 GMT + Belle (2013) + http://www.imdb.com/title/tt2404181/ + http://www.imdb.com/title/tt2404181/ + + + + Fri, 22 Nov 2013 02:29:41 GMT + A Simple Plan (1998) + http://www.imdb.com/title/tt0120324/ + http://www.imdb.com/title/tt0120324/ + + + + Fri, 22 Nov 2013 02:29:11 GMT + Approved for Adoption (2012) + http://www.imdb.com/title/tt1621766/ + http://www.imdb.com/title/tt1621766/ + + + + Fri, 22 Nov 2013 02:28:37 GMT + A Fierce Green Fire (2012 Documentary) + http://www.imdb.com/title/tt1539489/ + http://www.imdb.com/title/tt1539489/ + + + + Fri, 22 Nov 2013 02:28:01 GMT + Mother of George (2013) + http://www.imdb.com/title/tt2094890/ + http://www.imdb.com/title/tt2094890/ + + + + Tue, 20 Aug 2013 02:45:42 GMT + What Maisie Knew (2012) + http://www.imdb.com/title/tt1932767/ + http://www.imdb.com/title/tt1932767/ + + + + Tue, 20 Aug 2013 02:45:22 GMT + We're the Millers (2013) + http://www.imdb.com/title/tt1723121/ + http://www.imdb.com/title/tt1723121/ + + + + Tue, 20 Aug 2013 02:44:53 GMT + Visitors (2013 Documentary) + http://www.imdb.com/title/tt2936174/ + http://www.imdb.com/title/tt2936174/ + + + + Tue, 20 Aug 2013 02:43:58 GMT + Twenty Feet from Stardom (2013 Documentary) + http://www.imdb.com/title/tt2396566/ + http://www.imdb.com/title/tt2396566/ + + + + Tue, 20 Aug 2013 02:43:40 GMT + Trance (2013) + http://www.imdb.com/title/tt1924429/ + http://www.imdb.com/title/tt1924429/ + + + + Tue, 20 Aug 2013 02:42:19 GMT + This Is Martin Bonner (2013) + http://www.imdb.com/title/tt1798291/ + http://www.imdb.com/title/tt1798291/ + + + + Tue, 20 Aug 2013 02:41:50 GMT + The Purge (2013) + http://www.imdb.com/title/tt2184339/ + http://www.imdb.com/title/tt2184339/ + + + + Tue, 20 Aug 2013 02:41:27 GMT + The Place Beyond the Pines (2012) + http://www.imdb.com/title/tt1817273/ + http://www.imdb.com/title/tt1817273/ + + + + Tue, 20 Aug 2013 02:41:08 GMT + The Pervert's Guide to Ideology (2012 Documentary) + http://www.imdb.com/title/tt2152198/ + http://www.imdb.com/title/tt2152198/ + + + + Tue, 20 Aug 2013 02:40:36 GMT + The Monuments Men (2014) + http://www.imdb.com/title/tt2177771/ + http://www.imdb.com/title/tt2177771/ + + + + Tue, 20 Aug 2013 02:40:09 GMT + The Kids Are All Right (2010) + http://www.imdb.com/title/tt0842926/ + http://www.imdb.com/title/tt0842926/ + + + + Tue, 20 Aug 2013 02:39:46 GMT + The Internship (2013) + http://www.imdb.com/title/tt2234155/ + http://www.imdb.com/title/tt2234155/ + + + + Tue, 20 Aug 2013 02:39:26 GMT + The Incredible Burt Wonderstone (2013) + http://www.imdb.com/title/tt0790628/ + http://www.imdb.com/title/tt0790628/ + + + + Tue, 20 Aug 2013 02:39:03 GMT + The Company You Keep (2012) + http://www.imdb.com/title/tt1381404/ + http://www.imdb.com/title/tt1381404/ + + + + Tue, 20 Aug 2013 02:38:44 GMT + The Boxtrolls (2014) + http://www.imdb.com/title/tt0787474/ + http://www.imdb.com/title/tt0787474/ + + + + Tue, 20 Aug 2013 02:37:58 GMT + The Artist and the Model (2012) + http://www.imdb.com/title/tt1990217/ + http://www.imdb.com/title/tt1990217/ + + + + Tue, 20 Aug 2013 02:37:34 GMT + Spark: A Burning Man Story (2013 Documentary) + http://www.imdb.com/title/tt2554648/ + http://www.imdb.com/title/tt2554648/ + + + + Tue, 20 Aug 2013 02:36:42 GMT + Smash & Grab: The Story of the Pink Panthers (2013 Documentary) + http://www.imdb.com/title/tt2250032/ + http://www.imdb.com/title/tt2250032/ + + + + Tue, 20 Aug 2013 02:36:16 GMT + A Single Shot (2013) + http://www.imdb.com/title/tt1540741/ + http://www.imdb.com/title/tt1540741/ + + + + Tue, 20 Aug 2013 02:35:49 GMT + Side Effects (2013) + http://www.imdb.com/title/tt2053463/ + http://www.imdb.com/title/tt2053463/ + + + + Tue, 20 Aug 2013 02:34:43 GMT + Paradise (2013) + http://www.imdb.com/title/tt1262990/ + http://www.imdb.com/title/tt1262990/ + + + + Tue, 20 Aug 2013 02:34:00 GMT + Paperman (2012 Short Film) + http://www.imdb.com/title/tt2388725/ + http://www.imdb.com/title/tt2388725/ + + + + Tue, 20 Aug 2013 02:33:23 GMT + Once (2007) + http://www.imdb.com/title/tt0907657/ + http://www.imdb.com/title/tt0907657/ + + + + Tue, 20 Aug 2013 02:32:30 GMT + Mud (2012) + http://www.imdb.com/title/tt1935179/ + http://www.imdb.com/title/tt1935179/ + + + + Tue, 20 Aug 2013 02:31:52 GMT + Much Ado About Nothing (2012) + http://www.imdb.com/title/tt2094064/ + http://www.imdb.com/title/tt2094064/ + + + + Tue, 20 Aug 2013 02:31:32 GMT + Mama (2013) + http://www.imdb.com/title/tt2023587/ + http://www.imdb.com/title/tt2023587/ + + + + Tue, 20 Aug 2013 02:30:22 GMT + Ip Man: The Final Fight (2013) + http://www.imdb.com/title/tt2495118/ + http://www.imdb.com/title/tt2495118/ + + + + Tue, 20 Aug 2013 02:29:58 GMT + Intolerance: Love's Struggle Throughout the Ages (1916) + http://www.imdb.com/title/tt0006864/ + http://www.imdb.com/title/tt0006864/ + + + + Tue, 20 Aug 2013 02:29:26 GMT + Instructions Not Included (2013) + http://www.imdb.com/title/tt2378281/ + http://www.imdb.com/title/tt2378281/ + + + + Tue, 20 Aug 2013 02:29:02 GMT + Insidious: Chapter 2 (2013) + http://www.imdb.com/title/tt2226417/ + http://www.imdb.com/title/tt2226417/ + + + + Tue, 20 Aug 2013 02:27:50 GMT + Inequality for All (2013 Documentary) + http://www.imdb.com/title/tt2215151/ + http://www.imdb.com/title/tt2215151/ + + + + Tue, 20 Aug 2013 02:27:28 GMT + Her (2013) + http://www.imdb.com/title/tt1798709/ + http://www.imdb.com/title/tt1798709/ + + + + Tue, 20 Aug 2013 02:02:50 GMT + The Gatekeepers (2012 Documentary) + http://www.imdb.com/title/tt2309788/ + http://www.imdb.com/title/tt2309788/ + + + + Tue, 20 Aug 2013 02:02:32 GMT + Greetings from Tim Buckley (2012) + http://www.imdb.com/title/tt1823125/ + http://www.imdb.com/title/tt1823125/ + + + + Tue, 20 Aug 2013 02:02:16 GMT + Good Ol' Freda (2013 Documentary) + http://www.imdb.com/title/tt2505938/ + http://www.imdb.com/title/tt2505938/ + + + + Tue, 20 Aug 2013 02:01:56 GMT + Standing Up (2013) + http://www.imdb.com/title/tt1905042/ + http://www.imdb.com/title/tt1905042/ + + + + Tue, 20 Aug 2013 02:01:35 GMT + Gimme the Loot (2012) + http://www.imdb.com/title/tt2139919/ + http://www.imdb.com/title/tt2139919/ + + + + Tue, 20 Aug 2013 01:55:45 GMT + Frozen (2013) + http://www.imdb.com/title/tt2294629/ + http://www.imdb.com/title/tt2294629/ + + + + Tue, 20 Aug 2013 01:54:33 GMT + Enough Said (2013) + http://www.imdb.com/title/tt2390361/ + http://www.imdb.com/title/tt2390361/ + + + + Tue, 20 Aug 2013 01:53:53 GMT + Disconnect (2012) + http://www.imdb.com/title/tt1433811/ + http://www.imdb.com/title/tt1433811/ + + + + Tue, 20 Aug 2013 01:53:18 GMT + The Seventh Dwarf (2014) + http://www.imdb.com/title/tt2914892/ + http://www.imdb.com/title/tt2914892/ + + + + Tue, 20 Aug 2013 01:52:48 GMT + Delicatessen (1991) + http://www.imdb.com/title/tt0101700/ + http://www.imdb.com/title/tt0101700/ + + + + Tue, 20 Aug 2013 01:52:21 GMT + Cold Comes the Night (2013) + http://www.imdb.com/title/tt2511428/ + http://www.imdb.com/title/tt2511428/ + + + + Tue, 20 Aug 2013 01:51:51 GMT + CBGB (2013) + http://www.imdb.com/title/tt1786751/ + http://www.imdb.com/title/tt1786751/ + + + + Tue, 20 Aug 2013 01:51:25 GMT + C.O.G. (2013) + http://www.imdb.com/title/tt1650393/ + http://www.imdb.com/title/tt1650393/ + + + + Tue, 20 Aug 2013 01:50:38 GMT + Beyond the Hills (2012) + http://www.imdb.com/title/tt2258281/ + http://www.imdb.com/title/tt2258281/ + + + + Tue, 20 Aug 2013 01:49:52 GMT + Bears (2014 Documentary) + http://www.imdb.com/title/tt2458776/ + http://www.imdb.com/title/tt2458776/ + + + + Tue, 20 Aug 2013 01:47:45 GMT + A Teacher (2013) + http://www.imdb.com/title/tt2201548/ + http://www.imdb.com/title/tt2201548/ + + + + Tue, 20 Aug 2013 01:37:42 GMT + At Any Price (2012) + http://www.imdb.com/title/tt1937449/ + http://www.imdb.com/title/tt1937449/ + + + + Tue, 20 Aug 2013 01:37:18 GMT + A Strange Brand of Happy (2013) + http://www.imdb.com/title/tt2014168/ + http://www.imdb.com/title/tt2014168/ + + + + Tue, 20 Aug 2013 01:36:35 GMT + American Milkshake (2013) + http://www.imdb.com/title/tt2254364/ + http://www.imdb.com/title/tt2254364/ + + + + Tue, 20 Aug 2013 01:36:14 GMT + American Hustle (2013) + http://www.imdb.com/title/tt1800241/ + http://www.imdb.com/title/tt1800241/ + + + + Tue, 20 Aug 2013 01:33:58 GMT + Airplane! (1980) + http://www.imdb.com/title/tt0080339/ + http://www.imdb.com/title/tt0080339/ + + + + Tue, 20 Aug 2013 01:33:27 GMT + A.C.O.D. (2013) + http://www.imdb.com/title/tt1311060/ + http://www.imdb.com/title/tt1311060/ + + + + Tue, 20 Aug 2013 01:33:07 GMT + 12 O'Clock Boys (2013 Documentary) + http://www.imdb.com/title/tt2420006/ + http://www.imdb.com/title/tt2420006/ + + + + Tue, 20 Aug 2013 01:31:45 GMT + Unfinished Song (2012) + http://www.imdb.com/title/tt1047011/ + http://www.imdb.com/title/tt1047011/ + + + + Tue, 20 Aug 2013 01:31:25 GMT + The Sapphires (2012) + http://www.imdb.com/title/tt1673697/ + http://www.imdb.com/title/tt1673697/ + + + + Tue, 20 Aug 2013 01:30:59 GMT + Stories We Tell (2012 Documentary) + http://www.imdb.com/title/tt2366450/ + http://www.imdb.com/title/tt2366450/ + + + + Tue, 20 Aug 2013 01:30:29 GMT + Morning (2010) + http://www.imdb.com/title/tt1320103/ + http://www.imdb.com/title/tt1320103/ + + + + Tue, 20 Aug 2013 01:28:57 GMT + Kon-Tiki (2012) + http://www.imdb.com/title/tt1613750/ + http://www.imdb.com/title/tt1613750/ + + + + Tue, 20 Aug 2013 01:27:42 GMT + Kelly's Heroes (1970) + http://www.imdb.com/title/tt0065938/ + http://www.imdb.com/title/tt0065938/ + + + + Tue, 20 Aug 2013 01:20:13 GMT + Il Futuro (2013) + http://www.imdb.com/title/tt1992156/ + http://www.imdb.com/title/tt1992156/ + + + + Tue, 20 Aug 2013 01:18:48 GMT + Dear Zachary: A Letter to a Son About His Father (2008 Documentary) + http://www.imdb.com/title/tt1152758/ + http://www.imdb.com/title/tt1152758/ + + + + Tue, 20 Aug 2013 01:17:34 GMT + August: Osage County (2013) + http://www.imdb.com/title/tt1322269/ + http://www.imdb.com/title/tt1322269/ + + + + Tue, 20 Aug 2013 01:17:02 GMT + A Thousand Clowns (1965) + http://www.imdb.com/title/tt0059798/ + http://www.imdb.com/title/tt0059798/ + + + + Fri, 16 Aug 2013 05:39:41 GMT + The Naked Gun 2½: The Smell of Fear (1991) + http://www.imdb.com/title/tt0102510/ + http://www.imdb.com/title/tt0102510/ + + + + Fri, 16 Aug 2013 02:11:27 GMT + Blazing Saddles (1974) + http://www.imdb.com/title/tt0071230/ + http://www.imdb.com/title/tt0071230/ + + + + Wed, 14 Aug 2013 23:11:34 GMT + Super High Me (2007 Documentary) + http://www.imdb.com/title/tt1111833/ + http://www.imdb.com/title/tt1111833/ + + + + Fri, 26 Jul 2013 06:26:43 GMT + I Am Love (2009) + http://www.imdb.com/title/tt1226236/ + http://www.imdb.com/title/tt1226236/ + + + + Fri, 26 Jul 2013 06:26:20 GMT + The Wind Rises (2013) + http://www.imdb.com/title/tt2013293/ + http://www.imdb.com/title/tt2013293/ + + + + Fri, 26 Jul 2013 06:25:56 GMT + Melancholia (2011) + http://www.imdb.com/title/tt1527186/ + http://www.imdb.com/title/tt1527186/ + + + + Fri, 26 Jul 2013 06:14:53 GMT + The Patience Stone (2012) + http://www.imdb.com/title/tt1638353/ + http://www.imdb.com/title/tt1638353/ + + + + Fri, 26 Jul 2013 06:12:55 GMT + The Hunger Games (2012) + http://www.imdb.com/title/tt1392170/ + http://www.imdb.com/title/tt1392170/ + + + + Fri, 26 Jul 2013 06:10:37 GMT + Salinger (2013 Documentary) + http://www.imdb.com/title/tt1596753/ + http://www.imdb.com/title/tt1596753/ + + + + Fri, 26 Jul 2013 06:09:51 GMT + 47 Ronin (2013) + http://www.imdb.com/title/tt1335975/ + http://www.imdb.com/title/tt1335975/ + + + + Fri, 26 Jul 2013 06:06:53 GMT + Kick-Ass 2 (2013) + http://www.imdb.com/title/tt1650554/ + http://www.imdb.com/title/tt1650554/ + + + + Fri, 26 Jul 2013 06:05:54 GMT + Blackfish (2013 Documentary) + http://www.imdb.com/title/tt2545118/ + http://www.imdb.com/title/tt2545118/ + + + + Fri, 26 Jul 2013 06:05:32 GMT + Cockneys vs Zombies (2012) + http://www.imdb.com/title/tt1362058/ + http://www.imdb.com/title/tt1362058/ + + + + Fri, 26 Jul 2013 06:05:11 GMT + Blue Exorcist: The Movie (2012) + http://www.imdb.com/title/tt3028018/ + http://www.imdb.com/title/tt3028018/ + + + + Fri, 26 Jul 2013 06:04:31 GMT + Computer Chess (2013) + http://www.imdb.com/title/tt2007360/ + http://www.imdb.com/title/tt2007360/ + + + + Fri, 26 Jul 2013 06:03:22 GMT + Girl Most Likely (2012) + http://www.imdb.com/title/tt1698648/ + http://www.imdb.com/title/tt1698648/ + + + + Fri, 26 Jul 2013 05:31:00 GMT + Frankenweenie (2012) + http://www.imdb.com/title/tt1142977/ + http://www.imdb.com/title/tt1142977/ + + + + Thu, 18 Jul 2013 07:41:08 GMT + Nowhere Boy (2009) + http://www.imdb.com/title/tt1266029/ + http://www.imdb.com/title/tt1266029/ + + + + Thu, 18 Jul 2013 07:40:41 GMT + Amistad (1997) + http://www.imdb.com/title/tt0118607/ + http://www.imdb.com/title/tt0118607/ + + + + Thu, 18 Jul 2013 07:40:19 GMT + Angus, Thongs and Perfect Snogging (2008) + http://www.imdb.com/title/tt0963743/ + http://www.imdb.com/title/tt0963743/ + + + + Thu, 18 Jul 2013 07:31:50 GMT + Year One (2009) + http://www.imdb.com/title/tt1045778/ + http://www.imdb.com/title/tt1045778/ + + + + Thu, 18 Jul 2013 07:31:23 GMT + RocknRolla (2008) + http://www.imdb.com/title/tt1032755/ + http://www.imdb.com/title/tt1032755/ + + + + Thu, 18 Jul 2013 07:31:07 GMT + World War Z (2013) + http://www.imdb.com/title/tt0816711/ + http://www.imdb.com/title/tt0816711/ + + + + Thu, 18 Jul 2013 07:30:27 GMT + Welcome to the Punch (2013) + http://www.imdb.com/title/tt1684233/ + http://www.imdb.com/title/tt1684233/ + + + + Thu, 18 Jul 2013 07:30:01 GMT + Ways to Live Forever (2010) + http://www.imdb.com/title/tt1446208/ + http://www.imdb.com/title/tt1446208/ + + + + Thu, 18 Jul 2013 07:29:43 GMT + The Rise (2012) + http://www.imdb.com/title/tt1981140/ + http://www.imdb.com/title/tt1981140/ + + + + Thu, 18 Jul 2013 07:29:19 GMT + Warm Bodies (2013) + http://www.imdb.com/title/tt1588173/ + http://www.imdb.com/title/tt1588173/ + + + + Thu, 18 Jul 2013 07:27:30 GMT + Violet & Daisy (2011) + http://www.imdb.com/title/tt1634136/ + http://www.imdb.com/title/tt1634136/ + + + + Thu, 18 Jul 2013 07:24:58 GMT + Tiger Eyes (2012) + http://www.imdb.com/title/tt1748260/ + http://www.imdb.com/title/tt1748260/ + + + + Thu, 18 Jul 2013 07:24:37 GMT + This Is the End (2013) + http://www.imdb.com/title/tt1245492/ + http://www.imdb.com/title/tt1245492/ + + + + Thu, 18 Jul 2013 07:24:19 GMT + The Wolf of Wall Street (2013) + http://www.imdb.com/title/tt0993846/ + http://www.imdb.com/title/tt0993846/ + + + + Thu, 18 Jul 2013 07:24:01 GMT + The Way Way Back (2013) + http://www.imdb.com/title/tt1727388/ + http://www.imdb.com/title/tt1727388/ + + + + Thu, 18 Jul 2013 07:20:15 GMT + The Time Being (2012) + http://www.imdb.com/title/tt1916749/ + http://www.imdb.com/title/tt1916749/ + + + + Thu, 18 Jul 2013 07:19:57 GMT + The Sweeney (2012) + http://www.imdb.com/title/tt0857190/ + http://www.imdb.com/title/tt0857190/ + + + + Thu, 18 Jul 2013 07:19:26 GMT + The Spectacular Now (2013) + http://www.imdb.com/title/tt1714206/ + http://www.imdb.com/title/tt1714206/ + + + + Thu, 18 Jul 2013 07:18:41 GMT + Thérèse (2012) + http://www.imdb.com/title/tt1654829/ + http://www.imdb.com/title/tt1654829/ + + + + Thu, 18 Jul 2013 07:18:17 GMT + The Mortal Instruments: City of Bones (2013) + http://www.imdb.com/title/tt1538403/ + http://www.imdb.com/title/tt1538403/ + + + + Thu, 18 Jul 2013 07:17:15 GMT + The Lifeguard (2013) + http://www.imdb.com/title/tt2265534/ + http://www.imdb.com/title/tt2265534/ + + + + Thu, 18 Jul 2013 07:16:58 GMT + The Lego Movie (2014) + http://www.imdb.com/title/tt1490017/ + http://www.imdb.com/title/tt1490017/ + + + + Thu, 18 Jul 2013 07:05:06 GMT + The Hobbit: The Battle of the Five Armies (2014) + http://www.imdb.com/title/tt2310332/ + http://www.imdb.com/title/tt2310332/ + + + + Thu, 18 Jul 2013 07:04:28 GMT + The Hobbit: The Desolation of Smaug (2013) + http://www.imdb.com/title/tt1170358/ + http://www.imdb.com/title/tt1170358/ + + + + Thu, 18 Jul 2013 07:02:54 GMT + Silver Linings Playbook (2012) + http://www.imdb.com/title/tt1045658/ + http://www.imdb.com/title/tt1045658/ + + + + Thu, 18 Jul 2013 07:02:22 GMT + The Heat (2013) + http://www.imdb.com/title/tt2404463/ + http://www.imdb.com/title/tt2404463/ + + + + Thu, 18 Jul 2013 06:59:40 GMT + The Frozen Ground (2013) + http://www.imdb.com/title/tt2005374/ + http://www.imdb.com/title/tt2005374/ + + + + Thu, 18 Jul 2013 06:59:19 GMT + The Fifth Estate (2013) + http://www.imdb.com/title/tt1837703/ + http://www.imdb.com/title/tt1837703/ + + + + Thu, 18 Jul 2013 06:58:18 GMT + The Counselor (2013) + http://www.imdb.com/title/tt2193215/ + http://www.imdb.com/title/tt2193215/ + + + + Thu, 18 Jul 2013 06:57:39 GMT + The Conjuring (2013) + http://www.imdb.com/title/tt1457767/ + http://www.imdb.com/title/tt1457767/ + + + + Thu, 18 Jul 2013 06:56:31 GMT + The Act of Killing (2012 Documentary) + http://www.imdb.com/title/tt2375605/ + http://www.imdb.com/title/tt2375605/ + + + + Thu, 18 Jul 2013 06:56:11 GMT + Thanks for Sharing (2012) + http://www.imdb.com/title/tt1932718/ + http://www.imdb.com/title/tt1932718/ + + + + Thu, 18 Jul 2013 06:55:46 GMT + Stuck in Love (2012) + http://www.imdb.com/title/tt2205697/ + http://www.imdb.com/title/tt2205697/ + + + + Thu, 18 Jul 2013 06:54:11 GMT + Some Girl(s) (2013) + http://www.imdb.com/title/tt2201221/ + http://www.imdb.com/title/tt2201221/ + + + + Thu, 18 Jul 2013 06:53:27 GMT + Snowpiercer (2013) + http://www.imdb.com/title/tt1706620/ + http://www.imdb.com/title/tt1706620/ + + + + Thu, 18 Jul 2013 06:51:58 GMT + Arbitrage (2012) + http://www.imdb.com/title/tt1764183/ + http://www.imdb.com/title/tt1764183/ + + + + Thu, 18 Jul 2013 06:39:19 GMT + Seventh Son (2014) + http://www.imdb.com/title/tt1121096/ + http://www.imdb.com/title/tt1121096/ + + + + Thu, 18 Jul 2013 06:38:57 GMT + Saving Mr. Banks (2013) + http://www.imdb.com/title/tt2140373/ + http://www.imdb.com/title/tt2140373/ + + + + Thu, 18 Jul 2013 06:38:14 GMT + Runner Runner (2013) + http://www.imdb.com/title/tt2364841/ + http://www.imdb.com/title/tt2364841/ + + + + Thu, 18 Jul 2013 06:37:47 GMT + Rigor Mortis (2013) + http://www.imdb.com/title/tt2771800/ + http://www.imdb.com/title/tt2771800/ + + + + Thu, 18 Jul 2013 06:37:24 GMT + Ride Along (2014) + http://www.imdb.com/title/tt1408253/ + http://www.imdb.com/title/tt1408253/ + + + + Thu, 18 Jul 2013 06:35:37 GMT + Rush (2013) + http://www.imdb.com/title/tt1979320/ + http://www.imdb.com/title/tt1979320/ + + + + Thu, 18 Jul 2013 06:35:07 GMT + Prisoners (2013) + http://www.imdb.com/title/tt1392214/ + http://www.imdb.com/title/tt1392214/ + + + + Thu, 18 Jul 2013 06:34:50 GMT + Prince Avalanche (2013) + http://www.imdb.com/title/tt2195548/ + http://www.imdb.com/title/tt2195548/ + + + + Thu, 18 Jul 2013 06:34:28 GMT + Populaire (2012) + http://www.imdb.com/title/tt2070776/ + http://www.imdb.com/title/tt2070776/ + + + + Thu, 18 Jul 2013 06:34:06 GMT + Pitch Perfect (2012) + http://www.imdb.com/title/tt1981677/ + http://www.imdb.com/title/tt1981677/ + + + + Thu, 18 Jul 2013 06:33:17 GMT + Percy Jackson: Sea of Monsters (2013) + http://www.imdb.com/title/tt1854564/ + http://www.imdb.com/title/tt1854564/ + + + + Thu, 18 Jul 2013 06:33:00 GMT + Percy Jackson & the Olympians: The Lightning Thief (2010) + http://www.imdb.com/title/tt0814255/ + http://www.imdb.com/title/tt0814255/ + + + + Thu, 18 Jul 2013 06:32:39 GMT + Pawn Shop Chronicles (2013) + http://www.imdb.com/title/tt1741243/ + http://www.imdb.com/title/tt1741243/ + + + + Thu, 18 Jul 2013 06:32:04 GMT + Pacific Rim (2013) + http://www.imdb.com/title/tt1663662/ + http://www.imdb.com/title/tt1663662/ + + + + Thu, 18 Jul 2013 06:31:41 GMT + Oz the Great and Powerful (2013) + http://www.imdb.com/title/tt1623205/ + http://www.imdb.com/title/tt1623205/ + + + + Thu, 18 Jul 2013 06:31:21 GMT + Out of the Furnace (2013) + http://www.imdb.com/title/tt1206543/ + http://www.imdb.com/title/tt1206543/ + + + + Thu, 18 Jul 2013 06:30:54 GMT + Anchorman: The Legend of Ron Burgundy (2004) + http://www.imdb.com/title/tt0357413/ + http://www.imdb.com/title/tt0357413/ + + + + Thu, 18 Jul 2013 06:29:59 GMT + Now You See Me (2013) + http://www.imdb.com/title/tt1670345/ + http://www.imdb.com/title/tt1670345/ + + + + Thu, 18 Jul 2013 06:29:33 GMT + No (2012) + http://www.imdb.com/title/tt2059255/ + http://www.imdb.com/title/tt2059255/ + + + + Thu, 18 Jul 2013 06:28:06 GMT + Monsters University (2013) + http://www.imdb.com/title/tt1453405/ + http://www.imdb.com/title/tt1453405/ + + + + Thu, 18 Jul 2013 06:26:52 GMT + Magic Magic (2013) + http://www.imdb.com/title/tt1929308/ + http://www.imdb.com/title/tt1929308/ + + + + Thu, 18 Jul 2013 06:25:38 GMT + Like Someone in Love (2012) + http://www.imdb.com/title/tt1843287/ + http://www.imdb.com/title/tt1843287/ + + + + Thu, 18 Jul 2013 06:24:48 GMT + Jug Face (2013) + http://www.imdb.com/title/tt2620736/ + http://www.imdb.com/title/tt2620736/ + + + + Thu, 18 Jul 2013 06:24:25 GMT + Inside Llewyn Davis (2013) + http://www.imdb.com/title/tt2042568/ + http://www.imdb.com/title/tt2042568/ + + + + Thu, 18 Jul 2013 06:23:39 GMT + I Give It a Year (2013) + http://www.imdb.com/title/tt2244901/ + http://www.imdb.com/title/tt2244901/ + + + + Thu, 18 Jul 2013 06:23:14 GMT + I Declare War (2012) + http://www.imdb.com/title/tt2133239/ + http://www.imdb.com/title/tt2133239/ + + + + Thu, 18 Jul 2013 06:22:51 GMT + How to Train Your Dragon 2 (2014) + http://www.imdb.com/title/tt1646971/ + http://www.imdb.com/title/tt1646971/ + + + + Thu, 18 Jul 2013 06:22:32 GMT + How to Make Money Selling Drugs (2012 Documentary) + http://www.imdb.com/title/tt1276962/ + http://www.imdb.com/title/tt1276962/ + + + + Thu, 18 Jul 2013 06:22:07 GMT + Hell Baby (2013) + http://www.imdb.com/title/tt2318527/ + http://www.imdb.com/title/tt2318527/ + + + + Thu, 18 Jul 2013 06:16:54 GMT + Hannah Arendt (2012) + http://www.imdb.com/title/tt1674773/ + http://www.imdb.com/title/tt1674773/ + + + + Thu, 18 Jul 2013 06:16:01 GMT + Gravity (2013) + http://www.imdb.com/title/tt1454468/ + http://www.imdb.com/title/tt1454468/ + + + + Thu, 18 Jul 2013 06:15:42 GMT + Getaway (2013) + http://www.imdb.com/title/tt2167202/ + http://www.imdb.com/title/tt2167202/ + + + + Thu, 18 Jul 2013 06:15:24 GMT + Generation Um... (2012) + http://www.imdb.com/title/tt1718158/ + http://www.imdb.com/title/tt1718158/ + + + + Thu, 18 Jul 2013 06:14:29 GMT + Fruitvale Station (2013) + http://www.imdb.com/title/tt2334649/ + http://www.imdb.com/title/tt2334649/ + + + + Thu, 18 Jul 2013 06:13:55 GMT + Free Birds (2013) + http://www.imdb.com/title/tt1621039/ + http://www.imdb.com/title/tt1621039/ + + + + Thu, 18 Jul 2013 06:13:32 GMT + Billy Elliot (2000) + http://www.imdb.com/title/tt0249462/ + http://www.imdb.com/title/tt0249462/ + + + + Thu, 18 Jul 2013 06:13:03 GMT + Filth (2013) + http://www.imdb.com/title/tt1450321/ + http://www.imdb.com/title/tt1450321/ + + + + Thu, 18 Jul 2013 06:12:44 GMT + Ferris Bueller's Day Off (1986) + http://www.imdb.com/title/tt0091042/ + http://www.imdb.com/title/tt0091042/ + + + + Thu, 18 Jul 2013 06:12:22 GMT + Fast & Furious 6 (2013) + http://www.imdb.com/title/tt1905041/ + http://www.imdb.com/title/tt1905041/ + + + + Thu, 18 Jul 2013 06:11:49 GMT + Extraction (2013) + http://www.imdb.com/title/tt2823574/ + http://www.imdb.com/title/tt2823574/ + + + + Thu, 18 Jul 2013 06:11:13 GMT + Europa Report (2013) + http://www.imdb.com/title/tt2051879/ + http://www.imdb.com/title/tt2051879/ + + + + Thu, 18 Jul 2013 06:10:52 GMT + Escape Plan (2013) + http://www.imdb.com/title/tt1211956/ + http://www.imdb.com/title/tt1211956/ + + + + Thu, 18 Jul 2013 06:10:30 GMT + Epic (2013) + http://www.imdb.com/title/tt0848537/ + http://www.imdb.com/title/tt0848537/ + + + + Thu, 18 Jul 2013 06:09:42 GMT + Elysium (2013) + http://www.imdb.com/title/tt1535108/ + http://www.imdb.com/title/tt1535108/ + + + + Thu, 18 Jul 2013 06:09:19 GMT + Drift (2013) + http://www.imdb.com/title/tt1714833/ + http://www.imdb.com/title/tt1714833/ + + + + Thu, 18 Jul 2013 06:08:49 GMT + Dragon (2011) + http://www.imdb.com/title/tt1718199/ + http://www.imdb.com/title/tt1718199/ + + + + Thu, 18 Jul 2013 06:08:46 GMT + Dragon (2011) + http://www.imdb.com/title/tt1718199/ + http://www.imdb.com/title/tt1718199/ + + + + Thu, 18 Jul 2013 06:07:34 GMT + Don Jon (2013) + http://www.imdb.com/title/tt2229499/ + http://www.imdb.com/title/tt2229499/ + + + + Thu, 18 Jul 2013 06:07:01 GMT + Despicable Me 2 (2013) + http://www.imdb.com/title/tt1690953/ + http://www.imdb.com/title/tt1690953/ + + + + Thu, 18 Jul 2013 05:55:51 GMT + All the Real Girls (2003) + http://www.imdb.com/title/tt0299458/ + http://www.imdb.com/title/tt0299458/ + + + + Thu, 18 Jul 2013 05:55:35 GMT + The Assassination of Jesse James by the Coward Robert Ford (2007) + http://www.imdb.com/title/tt0443680/ + http://www.imdb.com/title/tt0443680/ + + + + Thu, 18 Jul 2013 05:55:29 GMT + Lars and the Real Girl (2007) + http://www.imdb.com/title/tt0805564/ + http://www.imdb.com/title/tt0805564/ + + + + Thu, 18 Jul 2013 05:48:45 GMT + Cutie and the Boxer (2013 Documentary) + http://www.imdb.com/title/tt2355540/ + http://www.imdb.com/title/tt2355540/ + + + + Thu, 18 Jul 2013 05:48:23 GMT + Superbad (2007) + http://www.imdb.com/title/tt0829482/ + http://www.imdb.com/title/tt0829482/ + + + + Thu, 18 Jul 2013 05:48:03 GMT + Crystal Fairy & the Magical Cactus (2013) + http://www.imdb.com/title/tt2332579/ + http://www.imdb.com/title/tt2332579/ + + + + Thu, 18 Jul 2013 05:47:45 GMT + Cloudy with a Chance of Meatballs 2 (2013) + http://www.imdb.com/title/tt1985966/ + http://www.imdb.com/title/tt1985966/ + + + + Thu, 18 Jul 2013 05:47:26 GMT + Cloudy with a Chance of Meatballs (2009) + http://www.imdb.com/title/tt0844471/ + http://www.imdb.com/title/tt0844471/ + + + + Thu, 18 Jul 2013 05:47:03 GMT + Captain Phillips (2013) + http://www.imdb.com/title/tt1535109/ + http://www.imdb.com/title/tt1535109/ + + + + Thu, 18 Jul 2013 05:46:03 GMT + Byzantium (2012) + http://www.imdb.com/title/tt1531901/ + http://www.imdb.com/title/tt1531901/ + + + + Thu, 18 Jul 2013 05:45:36 GMT + Broken (2012) + http://www.imdb.com/title/tt1441940/ + http://www.imdb.com/title/tt1441940/ + + + + Thu, 18 Jul 2013 05:45:13 GMT + Blue Jasmine (2013) + http://www.imdb.com/title/tt2334873/ + http://www.imdb.com/title/tt2334873/ + + + + Thu, 18 Jul 2013 05:44:53 GMT + Before Midnight (2013) + http://www.imdb.com/title/tt2209418/ + http://www.imdb.com/title/tt2209418/ + + + + Thu, 18 Jul 2013 05:44:21 GMT + Dirty Pretty Things (2002) + http://www.imdb.com/title/tt0301199/ + http://www.imdb.com/title/tt0301199/ + + + + Thu, 18 Jul 2013 05:43:52 GMT + Inside Man (2006) + http://www.imdb.com/title/tt0454848/ + http://www.imdb.com/title/tt0454848/ + + + + Thu, 18 Jul 2013 05:43:40 GMT + About Time (2013) + http://www.imdb.com/title/tt2194499/ + http://www.imdb.com/title/tt2194499/ + + + + Thu, 18 Jul 2013 05:43:26 GMT + Adore (2013) + http://www.imdb.com/title/tt2103267/ + http://www.imdb.com/title/tt2103267/ + + + + Thu, 18 Jul 2013 05:43:07 GMT + After Earth (2013) + http://www.imdb.com/title/tt1815862/ + http://www.imdb.com/title/tt1815862/ + + + + Thu, 18 Jul 2013 05:42:45 GMT + The Kings of Summer (2013) + http://www.imdb.com/title/tt2179116/ + http://www.imdb.com/title/tt2179116/ + + + + Thu, 18 Jul 2013 05:42:37 GMT + Afternoon Delight (2013) + http://www.imdb.com/title/tt2312890/ + http://www.imdb.com/title/tt2312890/ + + + + Thu, 18 Jul 2013 05:42:29 GMT + Ain't Them Bodies Saints (2013) + http://www.imdb.com/title/tt2388637/ + http://www.imdb.com/title/tt2388637/ + + + + Thu, 18 Jul 2013 05:42:21 GMT + Alan Partridge (2013) + http://www.imdb.com/title/tt0469021/ + http://www.imdb.com/title/tt0469021/ + + + + Thu, 18 Jul 2013 05:42:12 GMT + And Now a Word from Our Sponsor (2013) + http://www.imdb.com/title/tt2094762/ + http://www.imdb.com/title/tt2094762/ + + + + diff --git a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs index b2819434d..802744c96 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs @@ -45,6 +45,7 @@ namespace NzbDrone.Core.Test.IndexerTests return new IndexerResponse(new IndexerRequest(httpRequest), httpResponse); } + [Test] public void should_handle_relative_url() { diff --git a/src/NzbDrone.Core.Test/NetImport/CouchPotato/CouchPotatoParserFixture.cs b/src/NzbDrone.Core.Test/NetImport/CouchPotato/CouchPotatoParserFixture.cs new file mode 100644 index 000000000..cfea7ae36 --- /dev/null +++ b/src/NzbDrone.Core.Test/NetImport/CouchPotato/CouchPotatoParserFixture.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.CouchPotato; +using NzbDrone.Core.NetImport.RSSImport; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NetImport.CouchPotato +{ + public class CouchPotatoTest : CoreTest + { + private NetImportResponse CreateResponse(string url, string content) + { + var httpRequest = new HttpRequest(url); + var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content)); + + return new NetImportResponse(new NetImportRequest(httpRequest), httpResponse); + } + + + [Test] + public void should_parse_json_of_couchpotato() + { + var json = ReadAllText("Files/couchpotato_movie_list.json"); + + var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", json)); + + result.First().Title.Should().Be("Rogue One: A Star Wars Story"); + result.First().ImdbId.Should().Be("tt3748528"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs new file mode 100644 index 000000000..de13c40bf --- /dev/null +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.RSSImport; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NetImport +{ + [TestFixture] + public class RSSImportFixture : CoreTest + { + + [SetUp] + public void Setup() + { + Subject.Definition = Subject.DefaultDefinitions.First(); + } + private void GivenRecentFeedResponse(string rssXmlFile) + { + var recentFeed = ReadAllText(@"Files/" + rssXmlFile); + + Mocker.GetMock() + .Setup(o => o.Execute(It.IsAny())) + .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); + } + + [Test] + public void should_fetch_imdb_list() + { + GivenRecentFeedResponse("imdb_watchlist.xml"); + + var result = Subject.Fetch(); + + result.First().Title.Should().Be("Think Like a Man Too"); + result.First().ImdbId.Should().Be("tt2239832"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs new file mode 100644 index 000000000..cde97c653 --- /dev/null +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs @@ -0,0 +1,36 @@ +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.RSSImport; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NetImport +{ + public class RSSImportTest : CoreTest + { + private NetImportResponse CreateResponse(string url, string content) + { + var httpRequest = new HttpRequest(url); + var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content)); + + return new NetImportResponse(new NetImportRequest(httpRequest), httpResponse); + } + + + [Test] + public void should_parse_xml_of_imdb() + { + var xml = ReadAllText("Files/imdb_watchlist.xml"); + + var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", xml)); + + result.First().Title.Should().Be("Think Like a Man Too"); + result.First().ImdbId.Should().Be("tt2239832"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 70e548681..8cf473015 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -190,6 +190,9 @@ + + Always + Always @@ -284,6 +287,9 @@ + + + @@ -409,6 +415,9 @@ sqlite3.dll Always + + Always + Always diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index d19cddd67..639bb69d6 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -105,6 +105,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("RssSyncInterval", value); } } + public int NetImportSyncInterval + { + get { return GetValueInt("NetImportSyncInterval", 60); } + + set { SetValue("NetImportSyncInterval", value); } + } + public int MinimumAge { get { return GetValueInt("MinimumAge", 0); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index e17d8d6dc..a2d56e778 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -46,6 +46,8 @@ namespace NzbDrone.Core.Configuration int RssSyncInterval { get; set; } int MinimumAge { get; set; } + int NetImportSyncInterval { get; set; } + //UI int FirstDayOfWeek { get; set; } string CalendarWeekColumnHeader { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/123_create_netimport_table.cs b/src/NzbDrone.Core/Datastore/Migration/123_create_netimport_table.cs new file mode 100644 index 000000000..09eb67992 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/123_create_netimport_table.cs @@ -0,0 +1,27 @@ +using FluentMigrator; +using FluentMigrator.Expressions; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(123)] + public class create_netimport_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + if (!this.Schema.Schema("dbo").Table("NetImport").Exists()) + { + Create.TableForModel("NetImport") + .WithColumn("Enabled").AsBoolean() + .WithColumn("Name").AsString().Unique() + .WithColumn("Implementation").AsString() + .WithColumn("ConfigContract").AsString().Nullable() + .WithColumn("Settings").AsString().Nullable() + .WithColumn("EnableAuto").AsInt32() + .WithColumn("RootFolderPath").AsString() + .WithColumn("ShouldMonitor").AsInt32() + .WithColumn("ProfileId").AsInt32(); + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs b/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs new file mode 100644 index 000000000..531af0eb6 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs @@ -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(124)] + public class add_preferred_tags_to_profile : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Profiles").AddColumn("PreferredTags").AsString().Nullable(); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/125_fix_imdb_unique.cs b/src/NzbDrone.Core/Datastore/Migration/125_fix_imdb_unique.cs new file mode 100644 index 000000000..407ad06c4 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/125_fix_imdb_unique.cs @@ -0,0 +1,31 @@ +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(125)] + public class fix_imdb_unique : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(DeleteUniqueIndex); + } + + private void DeleteUniqueIndex(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"DROP INDEX 'IX_Movies_ImdbId'"; + + getSeriesCmd.ExecuteNonQuery(); + } + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 30c0b038f..28bf15948 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -34,6 +34,7 @@ using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.NetImport; namespace NzbDrone.Core.Datastore { @@ -55,6 +56,11 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.SupportsRss) .Ignore(i => i.SupportsSearch); + Mapper.Entity().RegisterDefinition("NetImport") + .Ignore(i => i.Enable) + .Relationship() + .HasOne(n => n.Profile, n => n.ProfileId); + Mapper.Entity().RegisterDefinition("Notifications") .Ignore(i => i.SupportsOnGrab) .Ignore(i => i.SupportsOnDownload) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index aba427cbf..fdb45f6d4 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.DecisionEngine var comparers = new List { CompareQuality, + ComparePreferredWords, CompareProtocol, ComparePeersIfTorrent, CompareAgeIfUsenet, @@ -65,6 +66,26 @@ namespace NzbDrone.Core.DecisionEngine CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version)); } + private int ComparePreferredWords(DownloadDecision x, DownloadDecision y) + { + return CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => + { + var title = remoteMovie.Release.Title; + remoteMovie.Movie.Profile.LazyLoad(); + var preferredWords = remoteMovie.Movie.Profile.Value.PreferredTags; + + if (preferredWords == null) + { + return 0; + } + + var num = preferredWords.AsEnumerable().Count(w => title.ToLower().Contains(w.ToLower())); + + return num; + + }); +; } + private int CompareProtocol(DownloadDecision x, DownloadDecision y) { diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index a7175dff6..679f422b4 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge public DelugeSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.MovieCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index 5291c9515..f66dbb365 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public HadoukenSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.Username).NotEmpty() .WithMessage("Username must not be empty."); diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 749ef9d04..211ba229d 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex public NzbVortexSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.ApiKey).NotEmpty() .WithMessage("API Key is required"); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index aff3f27ff..8b7a0c31d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public NzbgetSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password)); RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username)); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index c02619f2f..b5127293d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public QBittorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 0c1dc8221..00a8ef6b8 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public SabnzbdSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.ApiKey).NotEmpty() .WithMessage("API Key is required when username/password are not configured") diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 9d0a860ec..aeb8887c4 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public TransmissionSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.UrlBase).ValidUrlBase(); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 4965c9a78..b978ec721 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -10,10 +10,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent public RTorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.MovieCategory).NotEmpty() .WithMessage("A category is recommended") - .AsWarning(); + .AsWarning(); } } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 394fc53b4..8bf4c4ccb 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public UTorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.TvCategory).NotEmpty(); } } diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 70aba1c41..88dfcb164 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download public ProcessedDecisions ProcessDecisions(List decisions) { //var qualifiedReports = GetQualifiedReports(decisions); - var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisionsForMovies(decisions); var grabbed = new List(); var pending = new List(); diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs index 5185433a5..1574d53e0 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs @@ -27,4 +27,4 @@ namespace NzbDrone.Core.Indexers.HDBits return new HDBitsParser(Settings); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs index 9bb6d624b..aba22a1f2 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs @@ -129,4 +129,4 @@ namespace NzbDrone.Core.Indexers.HDBits ImdbImportFail = 8, ImdbTvNotAllowed = 9 } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index c5a6dfa4a..0a183ff77 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -87,4 +87,4 @@ namespace NzbDrone.Core.Indexers.HDBits } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 84fef6bb8..cd1ff7ccb 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; @@ -45,17 +44,34 @@ namespace NzbDrone.Core.Indexers.HDBits return new IndexerPageableRequestChain(); } - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); var queryBase = new TorrentQuery(); - var query = queryBase.Clone(); - query.ImdbInfo.Id = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); - pageableRequests.Add(GetRequest(query)); + + if (TryAddSearchParameters(queryBase, searchCriteria)) + { + var query = queryBase.Clone(); + query.ImdbInfo.Id = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); + pageableRequests.Add(GetRequest(query)); + } + return pageableRequests; } + private bool TryAddSearchParameters(TorrentQuery query, SearchCriteriaBase searchCriteria) + { + var imdbId = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); + + if (imdbId != 0) + { + query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo(); + query.ImdbInfo.Id = imdbId; + return true; + } + return false; + } + private IEnumerable GetRequest(TorrentQuery query) { var request = new HttpRequestBuilder(Settings.BaseUrl) @@ -75,4 +91,4 @@ namespace NzbDrone.Core.Indexers.HDBits yield return new IndexerRequest(request); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index c912291fa..a29e9a9b8 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -306,6 +306,8 @@ namespace NzbDrone.Core.Indexers request.HttpRequest.RateLimit = RateLimit; } + request.HttpRequest.AllowAutoRedirect = true; + return new IndexerResponse(request, _httpClient.Execute(request.HttpRequest)); } diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index d5303a97c..c2739b506 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Lifecycle; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.NetImport; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Update.Commands; @@ -85,6 +86,12 @@ namespace NzbDrone.Core.Jobs TypeName = typeof(RssSyncCommand).FullName }, + new ScheduledTask + { + Interval = GetNetImportSyncInterval(), + TypeName = typeof(NetImportSyncCommand).FullName + }, + new ScheduledTask { Interval = _configService.DownloadedEpisodesScanInterval, @@ -138,6 +145,23 @@ namespace NzbDrone.Core.Jobs return interval; } + private int GetNetImportSyncInterval() + { + var interval = _configService.NetImportSyncInterval; + + if (interval > 0 && interval < 10) + { + return 10; + } + + if (interval < 0) + { + return 0; + } + + return interval; + } + public void Handle(CommandExecutedEvent message) { var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.Body.GetType().FullName); @@ -157,7 +181,10 @@ namespace NzbDrone.Core.Jobs var downloadedEpisodes = _scheduledTaskRepository.GetDefinition(typeof(DownloadedEpisodesScanCommand)); downloadedEpisodes.Interval = _configService.DownloadedEpisodesScanInterval; - _scheduledTaskRepository.UpdateMany(new List { rss, downloadedEpisodes }); + var netImport = _scheduledTaskRepository.GetDefinition(typeof(NetImportSyncCommand)); + netImport.Interval = _configService.NetImportSyncInterval; + + _scheduledTaskRepository.UpdateMany(new List { rss, downloadedEpisodes, netImport }); } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index e1f095791..9821e8419 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -128,7 +128,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport var current = localMovie.Quality; var qualityName = current.Quality.Name.ToLower(); QualityModel updated = null; - if (width > 1400) + if (width > 2000) + { + if (qualityName.Contains("bluray")) + { + updated = new QualityModel(Quality.Bluray2160p); + } + + else if (qualityName.Contains("webdl")) + { + updated = new QualityModel(Quality.WEBDL2160p); + } + + else if (qualityName.Contains("hdtv")) + { + updated = new QualityModel(Quality.HDTV2160p); + } + + else + { + var def = _qualitiesService.Get(Quality.HDTV2160p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.HDTV2160p); + } + def = _qualitiesService.Get(Quality.WEBDL2160p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.WEBDL2160p); + } + def = _qualitiesService.Get(Quality.Bluray2160p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.Bluray2160p); + } + if (updated == null) + { + updated = new QualityModel(Quality.Bluray2160p); + } + } + + } + else if (width > 1400) { if (qualityName.Contains("bluray")) { @@ -147,7 +188,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport else { - var def = _qualitiesService.Get(Quality.HDTV1080p); if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) { diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 65d0706e2..371d43d35 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index b0fde064d..fb00c5c89 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.MediaFiles } else { - _logger.Warn("The existing movie file was not lazy loaded."); + //_logger.Warn("The existing movie file was not lazy loaded."); } diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs index d895075f9..bda58e7af 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs @@ -6,5 +6,7 @@ namespace NzbDrone.Core.MetadataSource public interface ISearchForNewMovie { List SearchForNewMovie(string title); + + Movie MapMovieToTmdbMovie(Movie movie); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs index 469e72776..182992c38 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs @@ -42,6 +42,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public bool adult { get; set; } public string backdrop_path { get; set; } public Belongs_To_Collection belongs_to_collection { get; set; } + public int? status_code { get; set; } + public string status_message { get; set; } public int budget { get; set; } public Genre[] genres { get; set; } public string homepage { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index a6ad60957..909881773 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -88,6 +88,18 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var response = _httpClient.Get(request); var resource = response.Resource; + if (resource.status_message != null) + { + if (resource.status_code == 34) + { + _logger.Warn("Movie with TmdbId {0} could not be found. This is probably the case when the movie was deleted from TMDB.", TmdbId); + return null; + } + + _logger.Warn(resource.status_message); + return null; + } + var movie = new Movie(); foreach (var alternativeTitle in resource.alternative_titles.titles) @@ -577,5 +589,40 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return value; } + + public Movie MapMovieToTmdbMovie(Movie movie) + { + Movie newMovie = movie; + if (movie.TmdbId > 0) + { + newMovie = GetMovieInfo(movie.TmdbId); + } + else if (movie.ImdbId.IsNotNullOrWhiteSpace()) + { + newMovie = GetMovieInfo(movie.ImdbId); + } + else + { + var yearStr = ""; + if (movie.Year > 1900) + { + yearStr = $" {movie.Year}"; + } + newMovie = SearchForNewMovie(movie.Title + yearStr).FirstOrDefault(); + } + + if (newMovie == null) + { + _logger.Warn("Couldn't map movie {0} to a movie on The Movie DB. It will not be added :(", movie.Title); + return null; + } + + newMovie.Path = movie.Path; + newMovie.RootFolderPath = movie.RootFolderPath; + newMovie.ProfileId = movie.ProfileId; + newMovie.Monitored = movie.Monitored; + + return newMovie; + } } } diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs new file mode 100644 index 000000000..4982e02da --- /dev/null +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.CouchPotato +{ + public class CouchPotatoResponse + { + public Movie[] movies { get; set; } + public int total { get; set; } + public bool empty { get; set; } + public bool success { get; set; } + } + + public class Movie + { + public string status { get; set; } + public Info info { get; set; } + public string _t { get; set; } + public List releases { get; set; } + public string title { get; set; } + public string _rev { get; set; } + public string profile_id { get; set; } + public string _id { get; set; } + public object category_id { get; set; } + public string type { get; set; } + } + + public class Info + { + public string[] genres { get; set; } + public int? tmdb_id { get; set; } + public string plot { get; set; } + public string tagline { get; set; } + public int? year { get; set; } + public string original_title { get; set; } + public bool? via_imdb { get; set; } + public string[] directors { get; set; } + public string[] titles { get; set; } + public string imdb { get; set; } + public string mpaa { get; set; } + public bool? via_tmdb { get; set; } + public string[] actors { get; set; } + public string[] writers { get; set; } + public int? runtime { get; set; } + public string type { get; set; } + public string released { get; set; } + } + + public class ReleaseInfo + { + public double? size { get; set; } + public int? seeders { get; set; } + public string protocol { get; set; } + public string description { get; set; } + public string url { get; set; } + public int? age { get; set; } + public string id { get; set; } + public int? leechers { get; set; } + public int? score { get; set; } + public string provider { get; set; } + public int? seed_time { get; set; } + public string provider_extra { get; set; } + public string detail_url { get; set; } + public string type { get; set; } + public double? seed_ratio { get; set; } + public string name { get; set; } + } + + public class DownloadInfo + { + public bool? status_support { get; set; } + public string id { get; set; } + public string downloader { get; set; } + } + + public class Release + { + public string status { get; set; } + public ReleaseInfo info { get; set; } + public DownloadInfo download_info { get; set; } + public string _id { get; set; } + public string media_id { get; set; } + public string _rev { get; set; } + public string _t { get; set; } + public bool? is_3d { get; set; } + public int? last_edit { get; set; } + public string identifier { get; set; } + public string quality { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs new file mode 100644 index 000000000..969b927db --- /dev/null +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.CouchPotato +{ + public class CouchPotatoImport : HttpNetImportBase + { + public override string Name => "CouchPotato"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public CouchPotatoImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + /*public new virtual IEnumerable DefaultDefinitions + { + get + { + var config = (CouchPotatoSettings)new CouchPotatoSettings(); + config.Link = "http://localhost"; + config.Port = "5050"; + + yield return new NetImportDefinition + { + Name = "Localhost", + Enabled = config.Validate().IsValid && Enabled, + Implementation = GetType().Name, + Settings = config + }; + } + }*/ + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new CouchPotatoRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new CouchPotatoParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs new file mode 100644 index 000000000..48b7b76d7 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs @@ -0,0 +1,109 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.CouchPotato +{ + public class CouchPotatoParser : IParseNetImportResponse + { + private readonly CouchPotatoSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + private static readonly Regex ReplaceEntities = new Regex("&[a-z]+;", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public CouchPotatoParser(CouchPotatoSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = JsonConvert.DeserializeObject(_importResponse.Content); + + // no movies were return + if (jsonResponse.total == 0) + { + return movies; + } + + var responseData = jsonResponse.movies; + + foreach (var item in responseData) + { + int tmdbid = item.info.tmdb_id ?? 0; + + // if there are no releases at all the movie wasn't found on CP, so return movies + if (!item.releases.Any() && item.type == "movie") + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = item.title, + ImdbId = item.info.imdb, + TmdbId = tmdbid + }); + } + else + { + // snatched,missing,available,downloaded + // done,seeding + bool isCompleted = item.releases.Any(rel => (rel.status == "done" || rel.status == "seeding")); + if (!isCompleted) + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = item.title, + ImdbId = item.info.imdb, + TmdbId = tmdbid, + Monitored = false + }); + } + } + } + + return movies; + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoRequestGenerator.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoRequestGenerator.cs new file mode 100644 index 000000000..0416ec5e8 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoRequestGenerator.cs @@ -0,0 +1,41 @@ +using NzbDrone.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.NetImport.CouchPotato +{ + public class CouchPotatoRequestGenerator : INetImportRequestGenerator + { + public CouchPotatoSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + private IEnumerable GetMovies(string searchParameters) + { + var urlBase = ""; + if (!string.IsNullOrWhiteSpace(Settings.UrlBase)) + { + urlBase = Settings.UrlBase.StartsWith("/") ? Settings.UrlBase : $"/{Settings.UrlBase}"; + } + + var status = ""; + + if (Settings.OnlyActive) + { + status = "?status=active"; + } + + var request = new NetImportRequest($"{Settings.Link.Trim()}:{Settings.Port}{urlBase}/api/{Settings.ApiKey}/movie.list/{status}", HttpAccept.Json); + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs new file mode 100644 index 000000000..5fb9c332f --- /dev/null +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs @@ -0,0 +1,48 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.CouchPotato +{ + + public class CouchPotatoSettingsValidator : AbstractValidator + { + public CouchPotatoSettingsValidator() + { + RuleFor(c => c.Link).ValidRootUrl(); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); + RuleFor(c => c.ApiKey).NotEmpty(); + } + } + + public class CouchPotatoSettings : NetImportBaseSettings + { + public CouchPotatoSettings() + { + Link = "http://localhost"; + Port = 5050; + UrlBase = ""; + OnlyActive = true; + } + + [FieldDefinition(0, Label = "CouchPotato URL", HelpText = "Link to your CoouchPootato.")] + public new string Link { get; set; } + + [FieldDefinition(1, Label = "CouchPotato Port", HelpText = "Port your CoouchPootato uses.")] + public int Port { get; set; } + + [FieldDefinition(2, Label = "CouchPotato Url Base", + HelpText = "UrlBase your CoouchPootato uses, leave blank for none")] + public string UrlBase { get; set; } + + [FieldDefinition(3, Label = "CouchPotato API Key", HelpText = "CoouchPootato API Key.")] + public string ApiKey { get; set; } + + [FieldDefinition(4, Label = "Only Wanted", HelpText = "Only add wanted movies.", Type = FieldType.Checkbox)] + public bool OnlyActive { get; set; } + + } + +} diff --git a/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs b/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs new file mode 100644 index 000000000..d3444d991 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.NetImport.Exceptions +{ + public class NetImportException : NzbDroneException + { + private readonly NetImportResponse _netImportResponse; + + public NetImportException(NetImportResponse response, string message, params object[] args) + : base(message, args) + { + _netImportResponse = response; + } + + public NetImportException(NetImportResponse response, string message) + : base(message) + { + _netImportResponse = response; + } + + public NetImportResponse Response => _netImportResponse; + } +} diff --git a/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs new file mode 100644 index 000000000..4454b2317 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Http.CloudFlare; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public abstract class HttpNetImportBase : NetImportBase + where TSettings : IProviderConfig, new() + { + protected const int MaxNumResultsPerQuery = 1000; + + protected readonly IHttpClient _httpClient; + + public override bool Enabled => true; + + public bool SupportsPaging => PageSize > 0; + + public virtual int PageSize => 0; + + public virtual TimeSpan RateLimit => TimeSpan.FromSeconds(2); + + public abstract INetImportRequestGenerator GetRequestGenerator(); + public abstract IParseNetImportResponse GetParser(); + + public HttpNetImportBase(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(configService, parsingService, logger) + { + _httpClient = httpClient; + } + + public override IList Fetch() + { + var generator = GetRequestGenerator(); + + return FetchMovies(generator.GetMovies()); + } + + protected virtual IList FetchMovies(NetImportPageableRequestChain pageableRequestChain, bool isRecent = false) + { + var movies = new List(); + var url = string.Empty; + + var parser = GetParser(); + + try + { + var fullyUpdated = false; + Movie lastMovie = null; + if (isRecent) + { + //lastReleaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id); + } + + for (int i = 0; i < pageableRequestChain.Tiers; i++) + { + var pageableRequests = pageableRequestChain.GetTier(i); + + foreach (var pageableRequest in pageableRequests) + { + var pagedReleases = new List(); + + foreach (var request in pageableRequest) + { + url = request.Url.FullUri; + + var page = FetchPage(request, parser); + + pagedReleases.AddRange(page); + + if (isRecent && page.Any()) + { + if (lastMovie == null) + { + fullyUpdated = true; + break; + }/* + var oldestReleaseDate = page.Select(v => v.PublishDate).Min(); + if (oldestReleaseDate < lastReleaseInfo.PublishDate || page.Any(v => v.DownloadUrl == lastReleaseInfo.DownloadUrl)) + { + fullyUpdated = true; + break; + } + + if (pagedReleases.Count >= MaxNumResultsPerQuery && + oldestReleaseDate < DateTime.UtcNow - TimeSpan.FromHours(24)) + { + fullyUpdated = false; + break; + }*///update later + } + else if (pagedReleases.Count >= MaxNumResultsPerQuery) + { + break; + } + + if (!IsFullPage(page)) + { + break; + } + } + + movies.AddRange(pagedReleases); + } + + if (movies.Any()) + { + break; + } + } + + if (isRecent && !movies.Empty()) + { + var ordered = movies.OrderByDescending(v => v.Title).ToList(); + + lastMovie = ordered.First(); + //_indexerStatusService.UpdateRssSyncStatus(Definition.Id, lastReleaseInfo); + } + + //_indexerStatusService.RecordSuccess(Definition.Id); + } + catch (WebException webException) + { + if (webException.Status == WebExceptionStatus.NameResolutionFailure || + webException.Status == WebExceptionStatus.ConnectFailure) + { + //_indexerStatusService.RecordConnectionFailure(Definition.Id); + } + else + { + //_indexerStatusService.RecordFailure(Definition.Id); + } + + if (webException.Message.Contains("502") || webException.Message.Contains("503") || + webException.Message.Contains("timed out")) + { + _logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message); + } + else + { + _logger.Warn("{0} {1} {2}", this, url, webException.Message); + } + } + catch (HttpException httpException) + { + if ((int)httpException.Response.StatusCode == 429) + { + //_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); + _logger.Warn("API Request Limit reached for {0}", this); + } + else + { + //_indexerStatusService.RecordFailure(Definition.Id); + _logger.Warn("{0} {1}", this, httpException.Message); + } + } + catch (RequestLimitReachedException) + { + //_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); + _logger.Warn("API Request Limit reached for {0}", this); + } + catch (ApiKeyException) + { + //_indexerStatusService.RecordFailure(Definition.Id); + _logger.Warn("Invalid API Key for {0} {1}", this, url); + } + catch (CloudFlareCaptchaException ex) + { + //_indexerStatusService.RecordFailure(Definition.Id); + if (ex.IsExpired) + { + _logger.Error(ex, "Expired CAPTCHA token for {0}, please refresh in indexer settings.", this); + } + else + { + _logger.Error(ex, "CAPTCHA token required for {0}, check indexer settings.", this); + } + } + catch (IndexerException ex) + { + //_indexerStatusService.RecordFailure(Definition.Id); + var message = string.Format("{0} - {1}", ex.Message, url); + _logger.Warn(ex, message); + } + catch (Exception feedEx) + { + //_indexerStatusService.RecordFailure(Definition.Id); + feedEx.Data.Add("FeedUrl", url); + _logger.Error(feedEx, "An error occurred while processing feed. " + url); + } + + return movies; + } + + protected virtual bool IsFullPage(IList page) + { + return PageSize != 0 && page.Count >= PageSize; + } + + protected virtual IList FetchPage(NetImportRequest request, IParseNetImportResponse parser) + { + var response = FetchIndexerResponse(request); + + return parser.ParseResponse(response).ToList().Select(m => + { + m.RootFolderPath = ((NetImportDefinition) Definition).RootFolderPath; + m.ProfileId = ((NetImportDefinition) Definition).ProfileId; + m.Monitored = ((NetImportDefinition) Definition).ShouldMonitor; + return m; + }).ToList(); + } + + protected virtual NetImportResponse FetchIndexerResponse(NetImportRequest request) + { + _logger.Debug("Downloading List " + request.HttpRequest.ToString(false)); + + if (request.HttpRequest.RateLimit < RateLimit) + { + request.HttpRequest.RateLimit = RateLimit; + } + + request.HttpRequest.AllowAutoRedirect = true; + + return new NetImportResponse(request, _httpClient.Execute(request.HttpRequest)); + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + } + + protected virtual ValidationFailure TestConnection() + { + try + { + var parser = GetParser(); + var generator = GetRequestGenerator(); + var releases = FetchPage(generator.GetMovies().GetAllTiers().First().First(), parser); + + if (releases.Empty()) + { + return new ValidationFailure(string.Empty, "No results were returned from your list, please check your settings."); + } + } + catch (ApiKeyException) + { + _logger.Warn("List returned result for RSS URL, API Key appears to be invalid"); + + return new ValidationFailure("ApiKey", "Invalid API Key"); + } + catch (RequestLimitReachedException) + { + _logger.Warn("Request limit reached"); + } + catch (CloudFlareCaptchaException ex) + { + if (ex.IsExpired) + { + return new ValidationFailure("CaptchaToken", "CloudFlare CAPTCHA token expired, please Refresh."); + } + else + { + return new ValidationFailure("CaptchaToken", "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required."); + } + } + catch (UnsupportedFeedException ex) + { + _logger.Warn(ex, "List feed is not supported"); + + return new ValidationFailure(string.Empty, "List feed is not supported: " + ex.Message); + } + catch (NetImportException ex) + { + _logger.Warn(ex, "Unable to connect to list"); + + return new ValidationFailure(string.Empty, "Unable to connect to indexer. " + ex.Message); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to list"); + + return new ValidationFailure(string.Empty, "Unable to connect to list, check the log for more details"); + } + + return null; + } + } + + +} diff --git a/src/NzbDrone.Core/NetImport/INetImport.cs b/src/NzbDrone.Core/NetImport/INetImport.cs new file mode 100644 index 000000000..7f5819fd1 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/INetImport.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public interface INetImport : IProvider + { + bool Enabled { get; } + bool EnableAuto { get; } + + IList Fetch(); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs b/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs new file mode 100644 index 000000000..d2c8107a4 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportRequestGenerator + { + NetImportPageableRequestChain GetMovies(); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs b/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs new file mode 100644 index 000000000..4776f551e --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public interface IParseNetImportResponse + { + IList ParseResponse(NetImportResponse netMovieImporterResponse); + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportBase.cs b/src/NzbDrone.Core/NetImport/NetImportBase.cs new file mode 100644 index 000000000..c08aaa3be --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportBase.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public abstract class NetImportBase : INetImport + where TSettings : IProviderConfig, new() + { + protected readonly IConfigService _configService; + protected readonly IParsingService _parsingService; + protected readonly Logger _logger; + + public abstract string Name { get; } + + public abstract bool Enabled { get; } + public abstract bool EnableAuto { get; } + + public NetImportBase(IConfigService configService, IParsingService parsingService, Logger logger) + { + _configService = configService; + _parsingService = parsingService; + _logger = logger; + } + + public Type ConfigContract => typeof(TSettings); + + public virtual ProviderMessage Message => null; + + public virtual IEnumerable DefaultDefinitions + { + get + { + var config = (IProviderConfig)new TSettings(); + + yield return new NetImportDefinition + { + Name = this.Name, + Enabled = config.Validate().IsValid && Enabled, + EnableAuto = true, + ProfileId = 1, + Implementation = GetType().Name, + Settings = config + }; + } + } + + public virtual ProviderDefinition Definition { get; set; } + + public virtual object RequestAction(string action, IDictionary query) { return null; } + + protected TSettings Settings => (TSettings)Definition.Settings; + + public abstract IList Fetch(); + + public ValidationResult Test() + { + var failures = new List(); + + try + { + Test(failures); + } + catch (Exception ex) + { + _logger.Error(ex, "Test aborted due to exception"); + failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); + } + + return new ValidationResult(failures); + } + + protected abstract void Test(List failures); + + public override string ToString() + { + return Definition.Name; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs b/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs new file mode 100644 index 000000000..0c9365151 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportBaseSettingsValidator : AbstractValidator + { + public NetImportBaseSettingsValidator() + { + RuleFor(c => c.Link).NotEmpty(); + } + } + + public class NetImportBaseSettings : IProviderConfig + { + private static readonly NetImportBaseSettingsValidator Validator = new NetImportBaseSettingsValidator(); + + public NetImportBaseSettings() + { + Link = "http://rss.imdb.com/list/"; + } + + [FieldDefinition(0, Label = "Link", HelpText = "Link to the list of movies.")] + public string Link { get; set; } + + public bool IsValid => !string.IsNullOrWhiteSpace(Link); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportDefinition.cs b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs new file mode 100644 index 000000000..20b6d2312 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs @@ -0,0 +1,17 @@ +using Marr.Data; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportDefinition : ProviderDefinition + { + public bool Enabled { get; set; } + public bool EnableAuto { get; set; } + public bool ShouldMonitor { get; set; } + public int ProfileId { get; set; } + public LazyLoaded Profile { get; set; } + public string RootFolderPath { get; set; } + public override bool Enable => Enabled; + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportFactory.cs b/src/NzbDrone.Core/NetImport/NetImportFactory.cs new file mode 100644 index 000000000..1e6ebf344 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportFactory.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Composition; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportFactory : IProviderFactory + { + List Enabled(); + } + + public class NetImportFactory : ProviderFactory, INetImportFactory + { + private readonly INetImportRepository _providerRepository; + private readonly Logger _logger; + + public NetImportFactory(INetImportRepository providerRepository, + IEnumerable providers, + IContainer container, + IEventAggregator eventAggregator, + Logger logger) + : base(providerRepository, providers, container, eventAggregator, logger) + { + _providerRepository = providerRepository; + _logger = logger; + } + + protected override List Active() + { + return base.Active().Where(c => c.Enabled).ToList(); + } + + public override void SetProviderCharacteristics(INetImport provider, NetImportDefinition definition) + { + base.SetProviderCharacteristics(provider, definition); + } + + public List Enabled() + { + var enabledImporters = GetAvailableProviders().Where(n => ((NetImportDefinition)n.Definition).Enabled); + var indexers = FilterBlockedIndexers(enabledImporters); + return indexers.ToList(); + } + + private IEnumerable FilterBlockedIndexers(IEnumerable importers) + { + foreach (var importer in importers) + { + yield return importer; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs b/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs new file mode 100644 index 000000000..50a43fce9 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportPageableRequest : IEnumerable + { + private readonly IEnumerable _enumerable; + + public NetImportPageableRequest(IEnumerable enumerable) + { + _enumerable = enumerable; + } + + public IEnumerator GetEnumerator() + { + return _enumerable.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerable.GetEnumerator(); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs b/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs new file mode 100644 index 000000000..080b8727a --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportPageableRequestChain + { + private List> _chains; + + public NetImportPageableRequestChain() + { + _chains = new List>(); + _chains.Add(new List()); + } + + public int Tiers => _chains.Count; + + public IEnumerable GetAllTiers() + { + return _chains.SelectMany(v => v); + } + + public IEnumerable GetTier(int index) + { + return _chains[index]; + } + + public void Add(IEnumerable request) + { + if (request == null) return; + + _chains.Last().Add(new NetImportPageableRequest(request)); + } + + public void AddTier(IEnumerable request) + { + AddTier(); + Add(request); + } + + public void AddTier() + { + if (_chains.Last().Count == 0) return; + + _chains.Add(new List()); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportRepository.cs b/src/NzbDrone.Core/NetImport/NetImportRepository.cs new file mode 100644 index 000000000..8efa8a4a8 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportRepository.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportRepository : IProviderRepository + { + + } + + public class NetImportRepository : ProviderRepository, INetImportRepository + { + public NetImportRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportRequest.cs b/src/NzbDrone.Core/NetImport/NetImportRequest.cs new file mode 100644 index 000000000..e00fe316f --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportRequest.cs @@ -0,0 +1,21 @@ +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportRequest + { + public HttpRequest HttpRequest { get; private set; } + + public NetImportRequest(string url, HttpAccept httpAccept) + { + HttpRequest = new HttpRequest(url, httpAccept); + } + + public NetImportRequest(HttpRequest httpRequest) + { + HttpRequest = httpRequest; + } + + public HttpUri Url => HttpRequest.Url; + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportResponse.cs b/src/NzbDrone.Core/NetImport/NetImportResponse.cs new file mode 100644 index 000000000..3174b0775 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportResponse.cs @@ -0,0 +1,24 @@ +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportResponse + { + private readonly NetImportRequest _netImport; + private readonly HttpResponse _httpResponse; + + public NetImportResponse(NetImportRequest netImport, HttpResponse httpResponse) + { + _netImport = netImport; + _httpResponse = httpResponse; + } + + public NetImportRequest Request => _netImport; + + public HttpRequest HttpRequest => _httpResponse.Request; + + public HttpResponse HttpResponse => _httpResponse; + + public string Content => _httpResponse.Content; + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs new file mode 100644 index 000000000..e119d8b36 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public interface IFetchNetImport + { + List Fetch(int listId, bool onlyEnableAuto); + List FetchAndFilter(int listId, bool onlyEnableAuto); + } + + public class NetImportSearchService : IFetchNetImport, IExecute + { + private readonly Logger _logger; + private readonly INetImportFactory _netImportFactory; + private readonly IMovieService _movieService; + private readonly ISearchForNewMovie _movieSearch; + private readonly IRootFolderService _rootFolder; + + public NetImportSearchService(INetImportFactory netImportFactory, IMovieService movieService, + ISearchForNewMovie movieSearch, IRootFolderService rootFolder, Logger logger) + { + _netImportFactory = netImportFactory; + _movieService = movieService; + _movieSearch = movieSearch; + _rootFolder = rootFolder; + _logger = logger; + } + + + public List Fetch(int listId, bool onlyEnableAuto = false) + { + return MovieListSearch(listId, onlyEnableAuto); + } + + public List FetchAndFilter(int listId, bool onlyEnableAuto) + { + var movies = MovieListSearch(listId, onlyEnableAuto); + + return movies.Where(x => !_movieService.MovieExists(x)).ToList(); + } + + public List MovieListSearch(int listId, bool onlyEnableAuto = false) + { + var movies = new List(); + + var importLists = _netImportFactory.GetAvailableProviders(); + + var lists = listId == 0 ? importLists : importLists.Where(n => ((NetImportDefinition)n.Definition).Id == listId); + + if (onlyEnableAuto) + { + lists = importLists.Where(a => ((NetImportDefinition)a.Definition).EnableAuto); + } + + foreach (var list in lists) + { + movies.AddRange(list.Fetch()); + } + + _logger.Debug("Found {0} movies from list(s) {1}", movies.Count, string.Join(", ", lists.Select(l => l.Definition.Name))); + + return movies; + } + + public void Execute(NetImportSyncCommand message) + { + var movies = FetchAndFilter(0, true); + + _logger.Debug("Found {0} movies on your auto enabled lists not in your library", movies.Count); + + foreach (var movie in movies) + { + var mapped = _movieSearch.MapMovieToTmdbMovie(movie); + + if (mapped != null) + { + _movieService.AddMovie(mapped); + } + } + } + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportSyncCommand.cs b/src/NzbDrone.Core/NetImport/NetImportSyncCommand.cs new file mode 100644 index 000000000..67d258fc6 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportSyncCommand.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportSyncCommand : Command + { + + public override bool SendUpdatesToClient => true; + + public int listId = 0; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs new file mode 100644 index 000000000..a68b4190d --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + public class RSSImport : HttpNetImportBase + { + public override string Name => "RSSList"; + public override bool Enabled => true; + public override bool EnableAuto => true; + + public RSSImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override IEnumerable DefaultDefinitions + { + get + { + foreach (var def in base.DefaultDefinitions) + { + yield return def; + } + yield return new NetImportDefinition + { + Name = "IMDb Watchlist", + Enabled = Enabled, + EnableAuto = true, + ProfileId = 1, + Implementation = GetType().Name, + Settings = new RSSImportSettings { Link = "http://rss.imdb.com/list/YOURLISTID" }, + }; + } + } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new RSSImportRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new RSSImportParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs new file mode 100644 index 000000000..f3244beec --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs @@ -0,0 +1,236 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + public class RSSImportParser : IParseNetImportResponse + { + private readonly RSSImportSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + private static readonly Regex ReplaceEntities = new Regex("&[a-z]+;", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public RSSImportParser(RSSImportSettings settings) + { + _settings = settings; + } + + public virtual IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(importResponse)) + { + return movies; + } + + var document = LoadXmlDocument(importResponse); + var items = GetItems(document); + + foreach (var item in items) + { + try + { + var reportInfo = ProcessItem(item); + + movies.AddIfNotNull(reportInfo); + } + catch (Exception itemEx) + { + //itemEx.Data.Add("Item", item.Title()); + _logger.Error(itemEx, "An error occurred while processing feed item from " + importResponse.Request.Url); + } + } + + return movies; + } + + protected virtual XDocument LoadXmlDocument(NetImportResponse indexerResponse) + { + try + { + var content = indexerResponse.Content; + content = ReplaceEntities.Replace(content, ReplaceEntity); + + using (var xmlTextReader = XmlReader.Create(new StringReader(content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true })) + { + return XDocument.Load(xmlTextReader); + } + } + catch (XmlException ex) + { + var contentSample = indexerResponse.Content.Substring(0, Math.Min(indexerResponse.Content.Length, 512)); + _logger.Debug("Truncated response content (originally {0} characters): {1}", indexerResponse.Content.Length, contentSample); + + ex.Data.Add("ContentLength", indexerResponse.Content.Length); + ex.Data.Add("ContentSample", contentSample); + + throw; + } + } + + protected virtual string ReplaceEntity(Match match) + { + try + { + var character = WebUtility.HtmlDecode(match.Value); + return string.Concat("&#", (int)character[0], ";"); + } + catch + { + return match.Value; + } + } + + protected virtual Movie CreateNewMovie() + { + return new Movie(); + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/html") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/html")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + protected Movie ProcessItem(XElement item) + { + var releaseInfo = CreateNewMovie(); + + releaseInfo = ProcessItem(item, releaseInfo); + + //_logger.Trace("Parsed: {0}", releaseInfo.Title); + + return PostProcess(item, releaseInfo); + } + + protected virtual Movie ProcessItem(XElement item, Movie releaseInfo) + { + var result = Parser.Parser.ParseMovieTitle(GetTitle(item)); + + releaseInfo.Title = GetTitle(item); + + if (result != null) + { + releaseInfo.Title = result.MovieTitle; + releaseInfo.Year = result.Year; + releaseInfo.ImdbId = result.ImdbId; + } + + try + { + if (releaseInfo.ImdbId.IsNullOrWhiteSpace()) + { + releaseInfo.ImdbId = GetImdbId(item); + } + + } + catch (Exception) + { + _logger.Debug("Unable to extract Imdb Id :(."); + } + + return releaseInfo; + } + + protected virtual Movie PostProcess(XElement item, Movie releaseInfo) + { + return releaseInfo; + } + + protected virtual string GetTitle(XElement item) + { + return item.TryGetValue("title", "Unknown"); + } + + protected virtual DateTime GetPublishDate(XElement item) + { + var dateString = item.TryGetValue("pubDate"); + + if (dateString.IsNullOrWhiteSpace()) + { + throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date."); + } + + return XElementExtensions.ParseDate(dateString); + } + + protected virtual string GetImdbId(XElement item) + { + var url = item.TryGetValue("link"); + if (url.IsNullOrWhiteSpace()) + { + return ""; + } + return Parser.Parser.ParseImdbId(url); + } + + protected IEnumerable GetItems(XDocument document) + { + var root = document.Root; + + if (root == null) + { + return Enumerable.Empty(); + } + + var channel = root.Element("channel"); + + if (channel == null) + { + return Enumerable.Empty(); + } + + return channel.Elements("item"); + } + + protected virtual string ParseUrl(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + var url = _importResponse.HttpRequest.Url + new HttpUri(value); + + return url.FullUri; + } + catch (Exception ex) + { + _logger.Debug(ex, string.Format("Failed to parse Url {0}, ignoring.", value)); + return null; + } + } + } +} diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs new file mode 100644 index 000000000..534f2cda1 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + public class RSSImportRequestGenerator : INetImportRequestGenerator + { + public RSSImportSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + //public NetImportPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + //{ + // return new NetImportPageableRequestChain(); + //} + + private IEnumerable GetMovies(string searchParameters) + { + var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Rss); + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs new file mode 100644 index 000000000..af39fe2ae --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + + public class RSSImportSettings : NetImportBaseSettings + { + //private const string helpLink = "https://imdb.com"; + + public RSSImportSettings() + { + Link = "http://rss.yoursite.com"; + } + + [FieldDefinition(0, Label = "RSS Link", HelpText = "Link to the rss feed of movies.")] + public new string Link { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs new file mode 100644 index 000000000..1dedc8719 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuResponse + { + public string title { get; set; } + public string imdb_id { get; set; } + public string poster_url { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs new file mode 100644 index 000000000..95c1d9b9e --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuImport : HttpNetImportBase + { + public override string Name => "StevenLu"; + public override bool Enabled => true; + public override bool EnableAuto => true; + + public StevenLuImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new StevenLuRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new StevenLuParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs new file mode 100644 index 000000000..9c032c162 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuParser : IParseNetImportResponse + { + private readonly StevenLuSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + public StevenLuParser(StevenLuSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + + // no movies were return + if (jsonResponse == null) + { + return movies; + } + + foreach (var item in jsonResponse) + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = item.title, + ImdbId = item.imdb_id + }); + } + + return movies; + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs new file mode 100644 index 000000000..9e573bcf4 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs @@ -0,0 +1,28 @@ +using NzbDrone.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuRequestGenerator : INetImportRequestGenerator + { + public StevenLuSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + private IEnumerable GetMovies(string searchParameters) + { + var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Json); + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs new file mode 100644 index 000000000..a0829c868 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + + public class StevenLuSettings : NetImportBaseSettings + { + public StevenLuSettings() + { + Link = "https://s3.amazonaws.com/popular-movies/movies.json"; + } + + [FieldDefinition(0, Label = "URL", HelpText = "Don't change this unless you know what you are doing.")] + public new string Link { get; set; } + + } + +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs new file mode 100644 index 000000000..e09ce89be --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class Ids + { + public int trakt { get; set; } + public string slug { get; set; } + public string imdb { get; set; } + public int tmdb { get; set; } + } + + public class Movie + { + public string title { get; set; } + public int? year { get; set; } + public Ids ids { get; set; } + } + + public class TraktResponse + { + public int rank { get; set; } + public string listed_at { get; set; } + public string type { get; set; } + public Movie movie { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs new file mode 100644 index 000000000..2add8de16 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktImport : HttpNetImportBase + { + public override string Name => "Trakt List"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public TraktImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new TraktRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new TraktParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs new file mode 100644 index 000000000..bf818d06a --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.NetImport.Trakt +{ + public enum TraktListType + { + WatchList = 0, + Watched = 1, + CustomList = 2 + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs new file mode 100644 index 000000000..f6e80bad2 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs @@ -0,0 +1,84 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktParser : IParseNetImportResponse + { + private readonly TraktSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + public TraktParser(TraktSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + + // no movies were return + if (jsonResponse == null) + { + return movies; + } + + foreach (var movie in jsonResponse) + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = movie.movie.title, + ImdbId = movie.movie.ids.imdb, + TmdbId = movie.movie.ids.tmdb, + Year = (movie.movie.year ?? 0) + }); + } + + return movies; + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs new file mode 100644 index 000000000..61166f8b3 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs @@ -0,0 +1,46 @@ +using NzbDrone.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktRequestGenerator : INetImportRequestGenerator + { + public TraktSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + private IEnumerable GetMovies(string searchParameters) + { + var link = $"{Settings.Link.Trim()}{Settings.Username.Trim()}"; + + switch (Settings.ListType) + { + case (int)TraktListType.CustomList: + link = link + $"/lists/{Settings.Listname.Trim()}/items/movies"; + break; + case (int)TraktListType.WatchList: + link = link + "/watchlist/movies"; + break; + case (int)TraktListType.Watched: + link = link + "/watched/movies"; + break; + } + + var request = new NetImportRequest($"{link}", HttpAccept.Json); + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", "657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0"); + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs new file mode 100644 index 000000000..8af9bd25f --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs @@ -0,0 +1,42 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.Trakt +{ + + public class TraktSettingsValidator : AbstractValidator + { + public TraktSettingsValidator() + { + RuleFor(c => c.Link).ValidRootUrl(); + RuleFor(c => c.Username).NotEmpty(); + } + } + + public class TraktSettings : NetImportBaseSettings + { + public TraktSettings() + { + Link = "https://api.trakt.tv/users/"; + Username = ""; + Listname = ""; + } + + [FieldDefinition(0, Label = "Trakt API URL", HelpText = "Link to to Trakt API URL, do not change unless you know what you are doing.")] + public new string Link { get; set; } + + [FieldDefinition(1, Label = "Trakt List Type", Type = FieldType.Select, SelectOptions = typeof(TraktListType), HelpText = "Trakt list type, custom or watchlist")] + public int ListType { get; set; } + + [FieldDefinition(2, Label = "Trakt Username", HelpText = "Trakt Username the list belongs to.")] + public string Username { get; set; } + + [FieldDefinition(3, Label = "Trakt List Name", HelpText = "Required for Custom List")] + public string Listname { get; set; } + + } + +} diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index d89f3aa42..8ff95a672 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -56,6 +56,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Radarr_EventType", "Download"); environmentVariables.Add("Radarr_Movie_Id", movie.Id.ToString()); environmentVariables.Add("Radarr_Movie_Title", movie.Title); + environmentVariables.Add("Radarr_Movie_Path", movie.Path); environmentVariables.Add("Radarr_Movie_ImdbId", movie.ImdbId.ToString()); environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); environmentVariables.Add("Radarr_MovieFile_RelativePath", movieFile.RelativePath); diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index a8c1a9851..2af0ed9b8 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Email public EmailSettingsValidator() { RuleFor(c => c.Server).NotEmpty(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.From).NotEmpty(); RuleFor(c => c.To).NotEmpty(); } diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs index 3c484dec7..55682003d 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Growl public GrowlSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index b47385736..f293a6ecd 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -37,14 +37,18 @@ namespace NzbDrone.Core.Notifications.MediaBrowser if (Settings.UpdateLibrary) { - _mediaBrowserService.Update(Settings, message.Series); + _mediaBrowserService.UpdateMovies(Settings, message.Movie); } } public override void OnMovieRename(Movie movie) { + if (Settings.UpdateLibrary) + { + _mediaBrowserService.UpdateMovies(Settings, movie); + } } - + public override void OnRename(Series series) { if (Settings.UpdateLibrary) diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 251488d87..dafccb99a 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -40,6 +40,16 @@ namespace NzbDrone.Core.Notifications.MediaBrowser ProcessRequest(request, settings); } + + public void UpdateMovies(MediaBrowserSettings settings, string imdbid) + { + var path = string.Format("/Library/Movies/Updated?ImdbId={0}", imdbid); + var request = BuildRequest(path, settings); + request.Headers.Add("Content-Length", "0"); + + ProcessRequest(request, settings); + } + private string ProcessRequest(HttpRequest request, MediaBrowserSettings settings) { request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs index 748d2a67f..9c76145cd 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser { void Notify(MediaBrowserSettings settings, string title, string message); void Update(MediaBrowserSettings settings, Series series); + void UpdateMovies(MediaBrowserSettings settings, Movie movie); ValidationFailure Test(MediaBrowserSettings settings); } @@ -35,6 +36,13 @@ namespace NzbDrone.Core.Notifications.MediaBrowser _proxy.Update(settings, series.TvdbId); } + + public void UpdateMovies(MediaBrowserSettings settings, Movie movie) + { + _proxy.UpdateMovies(settings, movie.ImdbId); + } + + public ValidationFailure Test(MediaBrowserSettings settings) { try diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index eefef7653..9b0c80f3d 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -80,8 +80,9 @@ namespace NzbDrone.Core.Notifications qualityString += " Proper"; } - return string.Format("{0} [{1}]", + return string.Format("{0} ({1}) [{2}]", movie.Title, + movie.Year, qualityString); } @@ -210,10 +211,12 @@ namespace NzbDrone.Core.Notifications public void Handle(MovieDownloadedEvent message) { var downloadMessage = new DownloadMessage(); - downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality); + downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality); downloadMessage.Series = null; downloadMessage.EpisodeFile = null; + downloadMessage.MovieFile = message.MovieFile; downloadMessage.Movie = message.Movie.Movie; + downloadMessage.OldFiles = null; downloadMessage.OldMovieFiles = message.OldFiles; downloadMessage.SourcePath = message.Movie.Path; diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs index 34e9e4b75..d10993d79 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Plex public PlexClientSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index a0c49452f..aa9e660fd 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Notifications.Plex public interface IPlexServerProxy { List GetTvSections(PlexServerSettings settings); - List GetMovieSections(PlexServerSettings settings); + List GetMovieSections(PlexServerSettings settings); void Update(int sectionId, PlexServerSettings settings); void UpdateSeries(int metadataId, PlexServerSettings settings); string Version(PlexServerSettings settings); @@ -81,12 +81,12 @@ namespace NzbDrone.Core.Notifications.Plex return Json.Deserialize(response.Content) .Sections .Where(d => d.Type == "movie") - .Select(s => new PlexSection - { - Id = s.Id, - Language = s.Language, - Locations = s.Locations, - Type = s.Type + .Select(s => new PlexSection + { + Id = s.Id, + Language = s.Language, + Locations = s.Locations, + Type = s.Type }) .ToList(); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs index e792392ab..9a5d0587c 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Plex public PlexServerSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 03b74c27f..13e69f5a0 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -86,8 +86,8 @@ namespace NzbDrone.Core.Notifications.Slack }; NotifySlack(payload); - } - + } + public override void OnRename(Series series) { var payload = new SlackPayload diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index c406252fc..a05eab45c 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -27,15 +27,15 @@ namespace NzbDrone.Core.Notifications.Synology { if (Settings.UpdateLibrary) { - foreach (var oldFile in message.OldFiles) + foreach (var oldFile in message.OldMovieFiles) { - var fullPath = Path.Combine(message.Series.Path, oldFile.RelativePath); + var fullPath = Path.Combine(message.Movie.Path, oldFile.RelativePath); _indexerProxy.DeleteFile(fullPath); } { - var fullPath = Path.Combine(message.Series.Path, message.EpisodeFile.RelativePath); + var fullPath = Path.Combine(message.Movie.Path, message.MovieFile.RelativePath); _indexerProxy.AddFile(fullPath); } @@ -44,8 +44,12 @@ namespace NzbDrone.Core.Notifications.Synology public override void OnMovieRename(Movie movie) { + if (Settings.UpdateLibrary) + { + _indexerProxy.UpdateFolder(movie.Path); + } } - + public override void OnRename(Series series) { if (Settings.UpdateLibrary) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs index 76f2bc91f..528728cdf 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs @@ -51,6 +51,24 @@ namespace NzbDrone.Core.Notifications.Xbmc UpdateLibrary(settings, series); } + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + if (!settings.AlwaysUpdate) + { + _logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address); + var activePlayers = GetActivePlayers(settings); + + if (activePlayers.Any(a => a.Type.Equals("video"))) + { + _logger.Debug("Video is currently playing, skipping library update"); + return; + } + } + + UpdateMovieLibrary(settings, movie); + } + + public void Clean(XbmcSettings settings) { const string cleanVideoLibrary = "CleanLibrary(video)"; @@ -167,6 +185,37 @@ namespace NzbDrone.Core.Notifications.Xbmc } } + private void UpdateMovieLibrary(XbmcSettings settings, Movie movie) + { + try + { + //_logger.Debug("Sending Update DB Request to XBMC Host: {0}", settings.Address); + //var xbmcSeriesPath = GetSeriesPath(settings, series); + + ////If the path is found update it, else update the whole library + //if (!string.IsNullOrEmpty(xbmcSeriesPath)) + //{ + // _logger.Debug("Updating series [{0}] on XBMC host: {1}", series, settings.Address); + // var command = BuildExecBuiltInCommand(string.Format("UpdateLibrary(video,{0})", xbmcSeriesPath)); + // SendCommand(settings, command); + //} + + //else + //{ + //Update the entire library + _logger.Debug("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", movie, settings.Address); + var command = BuildExecBuiltInCommand("UpdateLibrary(video)"); + SendCommand(settings, command); + //} + } + + catch (Exception ex) + { + _logger.Debug(ex, ex.Message); + } + } + + private string SendCommand(XbmcSettings settings, string command) { var url = string.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", settings.Address, command); diff --git a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs index bf250edc3..94bf80862 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Notifications.Xbmc { void Notify(XbmcSettings settings, string title, string message); void Update(XbmcSettings settings, Series series); + void UpdateMovie(XbmcSettings settings, Movie movie); void Clean(XbmcSettings settings); bool CanHandle(XbmcVersion version); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index 1a0674908..378bb0774 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -44,7 +44,25 @@ namespace NzbDrone.Core.Notifications.Xbmc UpdateLibrary(settings, series); } - + + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + if (!settings.AlwaysUpdate) + { + _logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address); + var activePlayers = _proxy.GetActivePlayers(settings); + + if (activePlayers.Any(a => a.Type.Equals("video"))) + { + _logger.Debug("Video is currently playing, skipping library update"); + return; + } + } + + UpdateMovieLibrary(settings, movie); + } + + public void Clean(XbmcSettings settings) { _proxy.CleanLibrary(settings); @@ -108,5 +126,23 @@ namespace NzbDrone.Core.Notifications.Xbmc _logger.Debug(ex, ex.Message); } } + + private void UpdateMovieLibrary(XbmcSettings settings, Movie movie) + { + try + { + var response = _proxy.UpdateLibrary(settings, null); + + if (!response.Equals("OK", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Failed to update library for: {0}", settings.Address); + } + } + + catch (Exception ex) + { + _logger.Debug(ex, ex.Message); + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 4939fbe3d..890e0516d 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -33,13 +33,14 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); - UpdateAndClean(message.Series, message.OldFiles.Any()); + UpdateAndCleanMovie(message.Movie, message.OldMovieFiles.Any()); } public override void OnMovieRename(Movie movie) { + UpdateAndCleanMovie(movie); } - + public override void OnRename(Series series) { UpdateAndClean(series); @@ -92,5 +93,26 @@ namespace NzbDrone.Core.Notifications.Xbmc _logger.Debug(ex, logMessage); } } + + private void UpdateAndCleanMovie(Movie movie, bool clean = true) + { + try + { + if (Settings.UpdateLibrary) + { + _xbmcService.UpdateMovie(Settings, movie); + } + + if (clean && Settings.CleanLibrary) + { + _xbmcService.Clean(Settings); + } + } + catch (SocketException ex) + { + var logMessage = string.Format("Unable to connect to XBMC Host: {0}:{1}", Settings.Host, Settings.Port); + _logger.Debug(ex, logMessage); + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 84127f69f..85dbc99c5 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Xbmc { void Notify(XbmcSettings settings, string title, string message); void Update(XbmcSettings settings, Series series); + void UpdateMovie(XbmcSettings settings, Movie movie); void Clean(XbmcSettings settings); ValidationFailure Test(XbmcSettings settings, string message); } @@ -51,6 +52,12 @@ namespace NzbDrone.Core.Notifications.Xbmc provider.Update(settings, series); } + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + var provider = GetApiProvider(settings); + provider.UpdateMovie(settings, movie); + } + public void Clean(XbmcSettings settings) { var provider = GetApiProvider(settings); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 60767b6ea..8d74efc50 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -122,6 +122,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -184,6 +220,8 @@ + + @@ -863,6 +901,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c247555e6..32422092e 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -138,7 +138,7 @@ namespace NzbDrone.Core.Organizer AddEpisodeFileTokens(tokenHandlers, episodeFile); AddQualityTokens(tokenHandlers, series, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile); - + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); @@ -230,10 +230,10 @@ namespace NzbDrone.Core.Organizer } var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); @@ -297,7 +297,7 @@ namespace NzbDrone.Core.Organizer public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) { - if(namingConfig == null) + if (namingConfig == null) { namingConfig = _namingConfigService.GetConfig(); } @@ -443,7 +443,7 @@ namespace NzbDrone.Core.Organizer var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; string formatPattern; - switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) { case MultiEpisodeStyle.Duplicate: @@ -466,14 +466,14 @@ namespace NzbDrone.Core.Organizer case MultiEpisodeStyle.Range: case MultiEpisodeStyle.PrefixedRange: formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - var eps = new List {episodes.First()}; + var eps = new List { episodes.First() }; if (episodes.Count > 1) eps.Add(episodes.Last()); absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); break; - //MultiEpisodeStyle.Extend + //MultiEpisodeStyle.Extend default: formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); @@ -921,7 +921,7 @@ namespace NzbDrone.Core.Organizer private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) { - return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() .Select(match => new AbsoluteEpisodeFormat { Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", @@ -1076,4 +1076,4 @@ namespace NzbDrone.Core.Organizer Range = 4, PrefixedRange = 5 } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 0d525ebe6..6b2e6e705 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -266,7 +266,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex ReportImdbId = new Regex(@"(?tt\d{9})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ReportImdbId = new Regex(@"(?tt\d{7})", RegexOptions.IgnoreCase | RegexOptions.Compiled); 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); @@ -454,7 +454,7 @@ namespace NzbDrone.Core.Parser { if (match.Groups["imdbid"].Value != null) { - if (match.Groups["imdbid"].Length == 11) + if (match.Groups["imdbid"].Length == 9) { return match.Groups["imdbid"].Value; } diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs index 6215e9474..d25104fb6 100644 --- a/src/NzbDrone.Core/Profiles/Profile.cs +++ b/src/NzbDrone.Core/Profiles/Profile.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Profiles public string Name { get; set; } public Quality Cutoff { get; set; } public List Items { get; set; } + public List PreferredTags { get; set; } public Language Language { get; set; } public Quality LastAllowedQuality() diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index 0c64aa994..74b77410d 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.ThingiProvider { var definition = provider.DefaultDefinitions .OfType() - .FirstOrDefault(v => v.Name == null || v.Name == provider.GetType().Name); + .FirstOrDefault(v => v.Name == null || v.Name == provider.Name); if (definition == null) { @@ -70,7 +70,7 @@ namespace NzbDrone.Core.ThingiProvider var definitions = provider.DefaultDefinitions .OfType() - .Where(v => v.Name != null && v.Name != provider.GetType().Name) + .Where(v => v.Name != null && v.Name != provider.Name) .ToList(); return definitions; diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs index d0c86f7aa..e9cd70af7 100644 --- a/src/NzbDrone.Core/Tv/MovieRepository.cs +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Tv Movie FindByTitle(string cleanTitle); Movie FindByTitle(string cleanTitle, int year); Movie FindByImdbId(string imdbid); + Movie FindByTmdbId(int tmdbid); Movie FindByTitleSlug(string slug); List MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); List MoviesWithFiles(int movieId); @@ -100,9 +101,46 @@ namespace NzbDrone.Core.Tv { cleanTitle = cleanTitle.ToLowerInvariant(); - return Query.Where(s => s.CleanTitle == cleanTitle) - .AndWhere(s => s.Year == year) - .SingleOrDefault(); + var cleanRoman = cleanTitle; + + var cleanNum = cleanTitle; + + foreach (KeyValuePair entry in romanNumeralsMapper) + { + string num = entry.Key; + string roman = entry.Value.ToLower(); + + cleanRoman = cleanRoman.Replace(num, roman); + + cleanNum = cleanNum.Replace(roman, num); + } + + var results = Query.Where(s => s.CleanTitle == cleanTitle); + + if (results == null) + { + results = Query.Where(s => s.CleanTitle == cleanNum).OrWhere(s => s.CleanTitle == cleanRoman); + + if (results == null) + { + var movies = this.All(); + + var listResults = movies.Where(m => m.AlternativeTitles.Any(t => Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanTitle || + Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanRoman || + Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanNum)); + + return listResults.Where(m => m.Year == year).FirstOrDefault(); + } + else + { + return results.Where(m => m.Year == year).FirstOrDefault(); + } + + } + else + { + return results.Where(m => m.Year == year).FirstOrDefault(); + } } public Movie FindByImdbId(string imdbid) @@ -122,7 +160,7 @@ namespace NzbDrone.Core.Tv public Movie FindByTitleSlug(string slug) { - return Query.Where(m => m.TitleSlug == slug).FirstOrDefault(); + return Query.FirstOrDefault(m => m.TitleSlug == slug); } public List MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) @@ -161,5 +199,10 @@ namespace NzbDrone.Core.Tv .Skip(pagingSpec.PagingOffset()) .Take(pagingSpec.PageSize); } + + public Movie FindByTmdbId(int tmdbid) + { + return Query.Where(m => m.TmdbId == tmdbid).FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index 24e1d19dd..9c33e1ecd 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -21,11 +21,13 @@ namespace NzbDrone.Core.Tv Movie GetMovie(int movieId); List GetMovies(IEnumerable movieIds); Movie AddMovie(Movie newMovie); + List AddMovies(List newMovies); Movie FindByImdbId(string imdbid); Movie FindByTitle(string title); Movie FindByTitle(string title, int year); Movie FindByTitleInexact(string title); Movie FindByTitleSlug(string slug); + bool MovieExists(Movie movie); Movie GetMovieByFileId(int fileId); List GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); PagingSpec MoviesWithoutFiles(PagingSpec pagingSpec); @@ -91,6 +93,35 @@ namespace NzbDrone.Core.Tv return newMovie; } + public List AddMovies(List newMovies) + { + _logger.Debug("Adding {0} movies", newMovies.Count); + + newMovies.ForEach(m => Ensure.That(m, () => m).IsNotNull()); + + newMovies.ForEach(m => + { + if (string.IsNullOrWhiteSpace(m.Path)) + { + var folderName = _fileNameBuilder.GetMovieFolder(m); + m.Path = Path.Combine(m.RootFolderPath, folderName); + } + + m.CleanTitle = m.Title.CleanSeriesTitle(); + m.SortTitle = MovieTitleNormalizer.Normalize(m.Title, m.TmdbId); + m.Added = DateTime.UtcNow; + }); + + _movieRepository.InsertMany(newMovies); + + newMovies.ForEach(m => + { + _eventAggregator.PublishEvent(new MovieAddedEvent(m)); + }); + + return newMovies; + } + public Movie FindByTitle(string title) { return _movieRepository.FindByTitle(title.CleanSeriesTitle()); @@ -247,5 +278,39 @@ namespace NzbDrone.Core.Tv return movieResult; } + + public bool MovieExists(Movie movie) + { + Movie result = null; + + if (movie.TmdbId != 0) + { + result = _movieRepository.FindByTmdbId(movie.TmdbId); + if (result != null) + { + return true; + } + } + + if (movie.ImdbId.IsNotNullOrWhiteSpace()) + { + result = _movieRepository.FindByImdbId(movie.ImdbId); + if (result != null) + { + return true; + } + } + + if (movie.Year > 1850) + { + result = _movieRepository.FindByTitle(movie.Title.CleanSeriesTitle(), movie.Year); + if (result != null) + { + return true; + } + } + + return false; + } } } diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js index 30cbc74b3..a219833e5 100644 --- a/src/UI/AddMovies/AddMoviesLayout.js +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -5,57 +5,72 @@ var RootFolderLayout = require('./RootFolders/RootFolderLayout'); var ExistingMoviesCollectionView = require('./Existing/AddExistingMovieCollectionView'); var AddMoviesView = require('./AddMoviesView'); var ProfileCollection = require('../Profile/ProfileCollection'); +var AddFromListView = require("./List/AddFromListView"); var RootFolderCollection = require('./RootFolders/RootFolderCollection'); require('../Movies/MoviesCollection'); module.exports = Marionette.Layout.extend({ - template : 'AddMovies/AddMoviesLayoutTemplate', - - regions : { - workspace : '#add-movies-workspace' - }, - - events : { - 'click .x-import' : '_importMovies', - 'click .x-add-new' : '_addMovies', - 'click .x-show-existing' : '_toggleExisting' - }, - - attributes : { - id : 'add-movies-screen' - }, - - initialize : function() { - ProfileCollection.fetch(); - RootFolderCollection.fetch().done(function() { - RootFolderCollection.synced = true; - }); - }, - - _toggleExisting : function(e) { - var showExisting = e.target.checked; - - vent.trigger(vent.Commands.ShowExistingCommand, { - showExisting: showExisting - }); - }, - - onShow : function() { - this.workspace.show(new AddMoviesView()); - }, - - _folderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); - }, - - _importMovies : function() { - this.rootFolderLayout = new RootFolderLayout(); - this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); - AppLayout.modalRegion.show(this.rootFolderLayout); - }, - - _addMovies : function() { - this.workspace.show(new AddMoviesView()); - } + template : 'AddMovies/AddMoviesLayoutTemplate', + + regions : { + workspace : '#add-movies-workspace', + }, + + ui : { + $existing : '#show-existing-movies-toggle' + }, + + events : { + 'click .x-import' : '_importMovies', + 'click .x-add-new' : '_addMovies', + "click .x-add-lists" : "_addFromList", + 'click .x-show-existing' : '_toggleExisting' + }, + + attributes : { + id : 'add-movies-screen' + }, + + initialize : function() { + ProfileCollection.fetch(); + RootFolderCollection.fetch().done(function() { + RootFolderCollection.synced = true; + }); + }, + + _toggleExisting : function(e) { + var showExisting = e.target.checked; + + vent.trigger(vent.Commands.ShowExistingCommand, { + showExisting: showExisting + }); + }, + + onShow : function() { + + this.workspace.show(new AddMoviesView()); + this.ui.$existing.hide(); + }, + + + _folderSelected : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.ui.$existing.show(); + this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); + }, + + _importMovies : function() { + this.rootFolderLayout = new RootFolderLayout(); + this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); + AppLayout.modalRegion.show(this.rootFolderLayout); + }, + + _addMovies : function() { + this.workspace.show(new AddMoviesView()); + }, + + _addFromList : function() { + this.ui.$existing.hide(); + this.workspace.show(new AddFromListView()); + } }); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs index c2f9ce419..48667d34c 100644 --- a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -1,43 +1,46 @@
-
-
- - -
-
+
+
+ + + +
+
-
-
-
- +
+
+
+
+ -
-
-
+ + + +
+
+
+
+
+
-
-
-
+
+
+
diff --git a/src/UI/AddMovies/List/AddFromListCollection.js b/src/UI/AddMovies/List/AddFromListCollection.js new file mode 100644 index 000000000..12f5cb7f0 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollection.js @@ -0,0 +1,18 @@ +var Backbone = require('backbone'); +var MovieModel = require('../../Movies/MovieModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/netimport/movies', + model : MovieModel, + + parse : function(response) { + var self = this; + + _.each(response, function(model) { + model.id = undefined; + }); + + return response; + } +}); diff --git a/src/UI/AddMovies/List/AddFromListCollectionView.js b/src/UI/AddMovies/List/AddFromListCollectionView.js new file mode 100644 index 000000000..91a963601 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollectionView.js @@ -0,0 +1,47 @@ +var Marionette = require('marionette'); +var ListItemView = require('./ListItemView'); +var vent = require('vent'); + +module.exports = Marionette.CollectionView.extend({ + itemView : ListItemView, + + ui : { + loadingList : '.x-loading-list' + }, + + initialize : function() { + + }, + + showCollection : function() { + }, + // + // appendHtml : function(collectionView, itemView, index) { + // collectionView.ui.loadingFolders.before(itemView.el); + // }, + // + // _showAndSearch : function(index) { + // var self = this; + // var model = this.collection.at(index); + // + // if (model) { + // var currentIndex = index; + // var folderName = model.get('folder').name; + // this.addItemView(model, this.getItemView(), index); + // this.children.findByModel(model).search({ term : folderName }).always(function() { + // if (!self.isClosed) { + // self._showAndSearch(currentIndex + 1); + // } + // }); + // } + // + // else { + // this.ui.loadingFolders.hide(); + // } + // }, + // + // itemViewOptions : { + // isExisting : true + // } + +}); diff --git a/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs new file mode 100644 index 000000000..34a766b7a --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/UI/AddMovies/List/AddFromListView.js b/src/UI/AddMovies/List/AddFromListView.js new file mode 100644 index 000000000..335380f94 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListView.js @@ -0,0 +1,254 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var AddFromListCollection = require('./AddFromListCollection'); +var AddFromListCollectionView = require('./AddFromListCollectionView'); +var AddListView = require("../../Settings/NetImport/Add/NetImportAddItemView"); +var EmptyView = require('../EmptyView'); +var NotFoundView = require('../NotFoundView'); +var ListCollection = require("../../Settings/NetImport/NetImportCollection"); +var ErrorView = require('../ErrorView'); +var LoadingView = require('../../Shared/LoadingView'); +var AppLayout = require('../../AppLayout'); +var InCinemasCell = require('../../Cells/InCinemasCell'); +var MovieTitleCell = require('../../Cells/MovieListTitleCell'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var ProfileCell = require('../../Cells/ProfileCell'); +var MovieLinksCell = require('../../Cells/MovieLinksCell'); +var MovieActionCell = require('../../Cells/MovieActionCell'); +var MovieStatusCell = require('../../Cells/MovieStatusCell'); +var MovieDownloadStatusCell = require('../../Cells/MovieDownloadStatusCell'); +var DownloadedQualityCell = require('../../Cells/DownloadedQualityCell'); +var MoviesCollection = require('../../Movies/MoviesCollection'); +var Messenger = require('../../Shared/Messenger'); +require('jquery.dotdotdot'); +var SchemaModal = require('../../Settings/NetImport/Add/NetImportSchemaModal'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/List/AddFromListViewTemplate', + + regions : { + fetchResult : '#fetch-result' + }, + + ui : { + moviesSearch : '.x-movies-search', + listSelection : ".x-list-selection", + importSelected : ".x-import-selected" + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false + }, + { + name : 'title', + label : 'Title', + cell : MovieTitleCell, + cellValue : 'this', + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell + }, + { + name : 'this', + label : 'Links', + cell : MovieLinksCell, + className : "movie-links-cell", + sortable : false, + } + ], + + events : { + 'click .x-load-more' : '_onLoadMore', + "change .x-list-selection" : "_listSelected", + "click .x-fetch-list" : "_fetchList", + "click .x-import-selected" : "_importSelected" + }, + + initialize : function(options) { + console.log(options); + + this.isExisting = options.isExisting; + //this.collection = new AddFromListCollection(); + + this.templateHelpers = {} + this.listCollection = new ListCollection(); + this.templateHelpers.lists = this.listCollection.toJSON(); + + this.listenTo(this.listCollection, 'all', this._listsUpdated); + this.listCollection.fetch(); + + this.collection = new AddFromListCollection(); + + this.listenTo(this.collection, 'sync', this._showResults); + + /*this.listenTo(this.collection, 'sync', this._showResults); + + this.resultCollectionView = new SearchResultCollectionView({ + collection : this.collection, + isExisting : this.isExisting + });*/ + + //this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); + }, + + onRender : function() { + var self = this; + this.ui.importSelected.hide(); + }, + + onShow : function() { + this.ui.moviesSearch.focus(); + + }, + + search : function(options) { + var self = this; + + this.collection.reset(); + + if (!options.term || options.term === this.collection.term) { + return Marionette.$.Deferred().resolve(); + } + + this.searchResult.show(new LoadingView()); + this.collection.term = options.term; + this.currentSearchPromise = this.collection.fetch({ + data : { term : options.term } + }); + + this.currentSearchPromise.fail(function() { + self._showError(); + }); + + return this.currentSearchPromise; + }, + + _onMoviesAdded : function(options) { + if (this.isExisting && options.movie.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.resultCollectionView.setExisting(options.movie.get('tmdbId')); + /*this.collection.term = ''; + this.collection.reset(); + this._clearResults(); + this.ui.moviesSearch.val(''); + this.ui.moviesSearch.focus();*/ //TODO: Maybe add option wheter to clear search result. + } + }, + + _onLoadMore : function() { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + + _listSelected : function() { + var rootFolderValue = this.ui.listSelection.val(); + if (rootFolderValue === 'addNew') { + //var rootFolderLayout = new SchemaModal(this.listCollection); + //AppLayout.modalRegion.show(rootFolderLayout); + SchemaModal.open(this.listCollection) + } + }, + + _fetchList : function() { + var self = this; + var listId = this.ui.listSelection.val(); + + this.fetchResult.show(new LoadingView()); + + this.currentFetchPromise = this.collection.fetch( + { data : { listId : listId} } + ) + this.currentFetchPromise.fail(function() { + self._showError(); + }); + + }, + + _listsUpdated : function() { + this.templateHelpers.lists = this.listCollection.toJSON(); + this.render(); + }, + + _importSelected : function() { + var selected = this.importGrid.getSelectedModels(); + console.log(selected); + var promise = MoviesCollection.importFromList(selected); + this.ui.importSelected.spinForPromise(promise); + this.ui.importSelected.addClass('disabled'); + + Messenger.show({ + message : "Importing {0} movies. This can take multiple minutes depending on how many movies should be imported. Don't close this browser window until it is finished!".format(selected.length), + hideOnNavigate : false, + hideAfter : 30, + type : "error" + }); + + promise.done(function() { + Messenger.show({ + message : "Imported movies from list.", + hideAfter : 8, + hideOnNavigate : true + }); + }); + /*for (m in selected) { + debugger; + m.save() + MoviesCollection.add(m); + }*/ + + //MoviesCollection.save(); + }, + + _clearResults : function() { + + if (!this.isExisting) { + this.searchResult.show(new EmptyView()); + } else { + this.searchResult.close(); + } + }, + + _showResults : function() { + if (this.collection.length === 0) { + this.fetchResult.show(new NotFoundView({ term : "" })); + } else { + this.importGrid = new Backgrid.Grid({ + collection : this.collection, + columns : this.columns, + className : 'table table-hover' + }); + this.fetchResult.show(this.importGrid); + this.ui.importSelected.show(); + } + + }, + + _abortExistingSearch : function() { + if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { + console.log('aborting previous pending search request.'); + this.currentSearchPromise.abort(); + } else { + this._clearResults(); + } + }, + + _showError : function() { + this.fetchResult.show(new ErrorView({ term : "" })); + } +}); diff --git a/src/UI/AddMovies/List/AddFromListViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs new file mode 100644 index 000000000..f63c95573 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs @@ -0,0 +1,18 @@ + +
+
+
diff --git a/src/UI/AddMovies/List/ListItemView.js b/src/UI/AddMovies/List/ListItemView.js new file mode 100644 index 000000000..f93b2778e --- /dev/null +++ b/src/UI/AddMovies/List/ListItemView.js @@ -0,0 +1,22 @@ +var _ = require('underscore'); +var vent = require('vent'); +var AppLayout = require('../../AppLayout'); +var Backbone = require('backbone'); +var Marionette = require('marionette'); +var Config = require('../../Config'); +var Messenger = require('../../Shared/Messenger'); +var AsValidatedView = require('../../Mixins/AsValidatedView'); + +require('jquery.dotdotdot'); + +var view = Marionette.ItemView.extend({ + + template : 'AddMovies/SearchResultViewTemplate', + + +}) + + +AsValidatedView.apply(view); + +module.exports = view; diff --git a/src/UI/AddMovies/List/ListItemViewTemplate.hbs b/src/UI/AddMovies/List/ListItemViewTemplate.hbs new file mode 100644 index 000000000..70d974ae7 --- /dev/null +++ b/src/UI/AddMovies/List/ListItemViewTemplate.hbs @@ -0,0 +1,3 @@ +
+ ASDF +
diff --git a/src/UI/Cells/MovieListTitleCell.js b/src/UI/Cells/MovieListTitleCell.js new file mode 100644 index 000000000..6d9142131 --- /dev/null +++ b/src/UI/Cells/MovieListTitleCell.js @@ -0,0 +1,7 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Cells/MovieListTitleTemplate', + +}); diff --git a/src/UI/Cells/MovieListTitleTemplate.hbs b/src/UI/Cells/MovieListTitleTemplate.hbs new file mode 100644 index 000000000..6c4bb964b --- /dev/null +++ b/src/UI/Cells/MovieListTitleTemplate.hbs @@ -0,0 +1 @@ +{{title}} diff --git a/src/UI/Content/Images/safari/logo.svg b/src/UI/Content/Images/safari/logo.svg new file mode 100644 index 000000000..d3eece392 --- /dev/null +++ b/src/UI/Content/Images/safari/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/UI/Content/Images/touch/114.png b/src/UI/Content/Images/touch/114.png new file mode 100644 index 000000000..6f23cd0f6 Binary files /dev/null and b/src/UI/Content/Images/touch/114.png differ diff --git a/src/UI/Content/Images/touch/144.png b/src/UI/Content/Images/touch/144.png new file mode 100644 index 000000000..a524e0cec Binary files /dev/null and b/src/UI/Content/Images/touch/144.png differ diff --git a/src/UI/Content/Images/touch/57.png b/src/UI/Content/Images/touch/57.png new file mode 100644 index 000000000..0a715a3f6 Binary files /dev/null and b/src/UI/Content/Images/touch/57.png differ diff --git a/src/UI/Content/Images/touch/72.png b/src/UI/Content/Images/touch/72.png new file mode 100644 index 000000000..2971f6b1e Binary files /dev/null and b/src/UI/Content/Images/touch/72.png differ diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js index 77f710890..efd0ece09 100644 --- a/src/UI/Movies/Details/MoviesDetailsLayout.js +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -16,302 +16,303 @@ require('backstrech'); require('../../Mixins/backbone.signalr.mixin'); module.exports = Marionette.Layout.extend({ - itemViewContainer : '.x-movie-seasons', - template : 'Movies/Details/MoviesDetailsTemplate', - - regions : { - seasons : '#seasons', - info : '#info', - search : '#movie-search', - history : '#movie-history', - files : "#movie-files" - }, - - - ui : { - header : '.x-header', - monitored : '.x-monitored', - edit : '.x-edit', - refresh : '.x-refresh', - rename : '.x-rename', - searchAuto : '.x-search', - poster : '.x-movie-poster', - manualSearch : '.x-manual-search', - history : '.x-movie-history', - search : '.x-movie-search', - files : ".x-movie-files" - }, - - events : { - 'click .x-episode-file-editor' : '_showFiles', - 'click .x-monitored' : '_toggleMonitored', - 'click .x-edit' : '_editMovie', - 'click .x-refresh' : '_refreshMovies', - 'click .x-rename' : '_renameMovies', - 'click .x-search' : '_moviesSearch', - 'click .x-manual-search' : '_showSearch', - 'click .x-movie-history' : '_showHistory', - 'click .x-movie-search' : '_showSearch', - "click .x-movie-files" : "_showFiles", - }, - - initialize : function() { - this.moviesCollection = MoviesCollection.clone(); - this.moviesCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._moviesRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - - this.listenTo(this.model, 'change:images', this._updateImages); - }, - - onShow : function() { - this.searchLayout = new SearchLayout({ model : this.model }); - this.searchLayout.startManualSearch = true; - - this.filesLayout = new FilesLayout({ model : this.model }); - - this._showBackdrop(); - this._showSeasons(); - this._setMonitoredState(); - this._showInfo(); - if (this.model.get("movieFile")) { - this._showFiles() - } else { - this._showHistory(); - } - - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshMovie' - } - }); - - CommandController.bindToCommand({ - element : this.ui.searchAuto, - command : { - name : 'moviesSearch' - } - }); - - CommandController.bindToCommand({ - element : this.ui.rename, - command : { - name : 'renameMovieFiles', - movieId : this.model.id, - seasonNumber : -1 - } - }); - }, - - onClose : function() { - if (this._backstrech) { - this._backstrech.destroy(); - delete this._backstrech; - } - - $('body').removeClass('backdrop'); - reqres.removeHandler(reqres.Requests.GetEpisodeFileById); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.history.tab('show'); - this.history.show(new HistoryLayout({ - model : this.model - })); - }, - - _showSearch : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.search.tab('show'); - this.search.show(this.searchLayout); - }, - - _showFiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.files.tab('show'); - this.files.show(this.filesLayout); - }, - - _toggleMonitored : function() { - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.monitored.spinForPromise(savePromise); - }, - - _setMonitoredState : function() { - var monitored = this.model.get('monitored'); - - this.ui.monitored.removeAttr('data-idle-icon'); - this.ui.monitored.removeClass('fa-spin icon-sonarr-spinner'); - - if (monitored) { - this.ui.monitored.addClass('icon-sonarr-monitored'); - this.ui.monitored.removeClass('icon-sonarr-unmonitored'); - this.$el.removeClass('movie-not-monitored'); - } else { - this.ui.monitored.addClass('icon-sonarr-unmonitored'); - this.ui.monitored.removeClass('icon-sonarr-monitored'); - this.$el.addClass('movie-not-monitored'); - } - }, - - _editMovie : function() { - vent.trigger(vent.Commands.EditMovieCommand, { movie : this.model }); - }, - - _refreshMovies : function() { - CommandController.Execute('refreshMovie', { - name : 'refreshMovie', - movieId : this.model.id - }); - }, - - _moviesRemoved : function() { - Backbone.history.navigate('/', { trigger : true }); - }, - - _renameMovies : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { movie : this.model }); - }, - - _moviesSearch : function() { - CommandController.Execute('moviesSearch', { - name : 'moviesSearch', - movieIds : [this.model.id] - }); - }, - - _showSeasons : function() { - var self = this; - - return; - - reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { - return self.episodeFileCollection.get(episodeFileId); - }); - - reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(moviesId, seasonNumber, sceneSeasonNumber) { - if (self.model.get('id') !== moviesId) { - return []; - } - - if (sceneSeasonNumber === undefined) { - sceneSeasonNumber = seasonNumber; - } - - return _.where(self.model.get('alternateTitles'), - function(alt) { - return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; - }); - }); - - $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { - var seasonCollectionView = new SeasonCollectionView({ - collection : self.seasonCollection, - episodeCollection : self.episodeCollection, - movies : self.model - }); - - if (!self.isClosed) { - self.seasons.show(seasonCollectionView); - } - }); - }, - - _showInfo : function() { - this.info.show(new InfoView({ - model : this.model - })); - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'renameMoviefiles') { - if (options.command.get('moviesId') === this.model.get('id')) { - this._refresh(); - } - } - }, - - _refresh : function() { - //this.seasonCollection.add(this.model.get('seasons'), { merge : true }); - //this.episodeCollection.fetch(); - //this.episodeFileCollection.fetch(); - - this._setMonitoredState(); - this._showInfo(); - }, - - _openEpisodeFileEditor : function() { - var view = new EpisodeFileEditorLayout({ - movies : this.model, - episodeCollection : this.episodeCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - }, - - _updateImages : function () { - var poster = this._getImage('poster'); - - if (poster) { - this.ui.poster.attr('src', poster); - } - - this._showBackdrop(); - }, - - _showBackdrop : function () { - $('body').addClass('backdrop'); - var fanArt = this._getImage('banner'); - - if (fanArt) { - this._backstrech = $.backstretch(fanArt); - } else { - $('body').removeClass('backdrop'); - } - }, - - _manualSearchM : function() { - console.warn("Manual Search started"); - console.warn(this.model.id); - console.warn(this.model) - console.warn(this.episodeCollection); - vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.model, - hideMoviesLink : true, - openingTab : 'search' - }); - } + itemViewContainer : '.x-movie-seasons', + template : 'Movies/Details/MoviesDetailsTemplate', + + regions : { + seasons : '#seasons', + info : '#info', + search : '#movie-search', + history : '#movie-history', + files : "#movie-files" + }, + + + ui : { + header : '.x-header', + monitored : '.x-monitored', + edit : '.x-edit', + refresh : '.x-refresh', + rename : '.x-rename', + searchAuto : '.x-search', + poster : '.x-movie-poster', + manualSearch : '.x-manual-search', + history : '.x-movie-history', + search : '.x-movie-search', + files : ".x-movie-files" + }, + + events : { + 'click .x-episode-file-editor' : '_showFiles', + 'click .x-monitored' : '_toggleMonitored', + 'click .x-edit' : '_editMovie', + 'click .x-refresh' : '_refreshMovies', + 'click .x-rename' : '_renameMovies', + 'click .x-search' : '_moviesSearch', + 'click .x-manual-search' : '_showSearch', + 'click .x-movie-history' : '_showHistory', + 'click .x-movie-search' : '_showSearch', + "click .x-movie-files" : "_showFiles", + }, + + initialize : function() { + this.moviesCollection = MoviesCollection.clone(); + this.moviesCollection.shadowCollection.bindSignalR(); + + this.listenTo(this.model, 'change:monitored', this._setMonitoredState); + this.listenTo(this.model, 'remove', this._moviesRemoved); + this.listenTo(this.model, "change:movieFile", this._refreshFiles); + + this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); + + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(); + } + }); + + this.listenTo(this.model, 'change:images', this._updateImages); + }, + + onShow : function() { + this.searchLayout = new SearchLayout({ model : this.model }); + this.searchLayout.startManualSearch = true; + + this.filesLayout = new FilesLayout({ model : this.model }); + + this._showBackdrop(); + this._showSeasons(); + this._setMonitoredState(); + this._showInfo(); + if (this.model.get("movieFile")) { + this._showFiles() + } else { + this._showHistory(); + } + + }, + + onRender : function() { + CommandController.bindToCommand({ + element : this.ui.refresh, + command : { + name : 'refreshMovie' + } + }); + + CommandController.bindToCommand({ + element : this.ui.searchAuto, + command : { + name : 'moviesSearch' + } + }); + + CommandController.bindToCommand({ + element : this.ui.rename, + command : { + name : 'renameMovieFiles', + movieId : this.model.id, + seasonNumber : -1 + } + }); + }, + + onClose : function() { + if (this._backstrech) { + this._backstrech.destroy(); + delete this._backstrech; + } + + $('body').removeClass('backdrop'); + reqres.removeHandler(reqres.Requests.GetEpisodeFileById); + }, + + _getImage : function(type) { + var image = _.where(this.model.get('images'), { coverType : type }); + + if (image && image[0]) { + return image[0].url; + } + + return undefined; + }, + + _showHistory : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.history.tab('show'); + this.history.show(new HistoryLayout({ + model : this.model + })); + }, + + _showSearch : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.search.tab('show'); + this.search.show(this.searchLayout); + }, + + _showFiles : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.files.tab('show'); + this.files.show(this.filesLayout); + }, + + _toggleMonitored : function() { + var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); + + this.ui.monitored.spinForPromise(savePromise); + }, + + _setMonitoredState : function() { + var monitored = this.model.get('monitored'); + + this.ui.monitored.removeAttr('data-idle-icon'); + this.ui.monitored.removeClass('fa-spin icon-sonarr-spinner'); + + if (monitored) { + this.ui.monitored.addClass('icon-sonarr-monitored'); + this.ui.monitored.removeClass('icon-sonarr-unmonitored'); + this.$el.removeClass('movie-not-monitored'); + } else { + this.ui.monitored.addClass('icon-sonarr-unmonitored'); + this.ui.monitored.removeClass('icon-sonarr-monitored'); + this.$el.addClass('movie-not-monitored'); + } + }, + + _editMovie : function() { + vent.trigger(vent.Commands.EditMovieCommand, { movie : this.model }); + }, + + _refreshMovies : function() { + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', + movieId : this.model.id + }); + }, + + _moviesRemoved : function() { + Backbone.history.navigate('/', { trigger : true }); + }, + + _renameMovies : function() { + vent.trigger(vent.Commands.ShowRenamePreview, { movie : this.model }); + }, + + _moviesSearch : function() { + CommandController.Execute('moviesSearch', { + name : 'moviesSearch', + movieIds : [this.model.id] + }); + }, + + _showSeasons : function() { + var self = this; + + return; + + reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { + return self.episodeFileCollection.get(episodeFileId); + }); + + reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(moviesId, seasonNumber, sceneSeasonNumber) { + if (self.model.get('id') !== moviesId) { + return []; + } + + if (sceneSeasonNumber === undefined) { + sceneSeasonNumber = seasonNumber; + } + + return _.where(self.model.get('alternateTitles'), + function(alt) { + return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; + }); + }); + + $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { + var seasonCollectionView = new SeasonCollectionView({ + collection : self.seasonCollection, + episodeCollection : self.episodeCollection, + movies : self.model + }); + + if (!self.isClosed) { + self.seasons.show(seasonCollectionView); + } + }); + }, + + _showInfo : function() { + this.info.show(new InfoView({ + model : this.model + })); + }, + + _commandComplete : function(options) { + if (options.command.get('name') === 'renameMoviefiles') { + if (options.command.get('moviesId') === this.model.get('id')) { + this._refresh(); + } + } + }, + + _refresh : function() { + //this.seasonCollection.add(this.model.get('seasons'), { merge : true }); + //this.episodeCollection.fetch(); + //this.episodeFileCollection.fetch(); + this._setMonitoredState(); + this._showInfo(); + }, + + _openEpisodeFileEditor : function() { + var view = new EpisodeFileEditorLayout({ + movies : this.model, + episodeCollection : this.episodeCollection + }); + + vent.trigger(vent.Commands.OpenModalCommand, view); + }, + + _updateImages : function () { + var poster = this._getImage('poster'); + + if (poster) { + this.ui.poster.attr('src', poster); + } + + this._showBackdrop(); + }, + + _showBackdrop : function () { + $('body').addClass('backdrop'); + var fanArt = this._getImage('banner'); + + if (fanArt) { + this._backstrech = $.backstretch(fanArt); + } else { + $('body').removeClass('backdrop'); + } + }, + + _manualSearchM : function() { + console.warn("Manual Search started"); + console.warn(this.model.id); + console.warn(this.model) + console.warn(this.episodeCollection); + vent.trigger(vent.Commands.ShowEpisodeDetails, { + episode : this.model, + hideMoviesLink : true, + openingTab : 'search' + }); + } }); diff --git a/src/UI/Movies/Files/Edit/EditFileTemplate.hbs b/src/UI/Movies/Files/Edit/EditFileTemplate.hbs new file mode 100644 index 000000000..e06c410d2 --- /dev/null +++ b/src/UI/Movies/Files/Edit/EditFileTemplate.hbs @@ -0,0 +1,32 @@ + diff --git a/src/UI/Movies/Files/Edit/EditFileView.js b/src/UI/Movies/Files/Edit/EditFileView.js new file mode 100644 index 000000000..8a0633fb8 --- /dev/null +++ b/src/UI/Movies/Files/Edit/EditFileView.js @@ -0,0 +1,60 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Qualities = require('../../../Quality/QualityDefinitionCollection'); +var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var AsEditModalView = require('../../../Mixins/AsEditModalView'); +require('../../../Mixins/TagInput'); +require('../../../Mixins/FileBrowser'); + +var view = Marionette.ItemView.extend({ + template : 'Movies/Files/Edit/EditFileTemplate', + + ui : { + quality : '.x-quality', + path : '.x-path', + tags : '.x-tags' + }, + + events : { + + }, + + initialize : function() { + this.qualities = new Qualities(); + var self = this; + this.listenTo(this.qualities, 'all', this._qualitiesUpdated); + this.qualities.fetch() + + }, + + onRender : function() { + this.ui.quality.val(this.model.get("quality").quality.id) + }, + + _onBeforeSave : function() { + var qualityId = this.ui.quality.val(); + var quality = this.qualities.find(function(m){return m.get("quality").id == qualityId}).get("quality"); + var mQuality = this.model.get("quality"); + mQuality.quality = quality; + this.model.set({ quality : mQuality }); + }, + + _qualitiesUpdated : function() { + this.templateHelpers = {}; + this.templateHelpers.qualities = this.qualities.toJSON(); + this.render(); + }, + + _onAfterSave : function() { + this.trigger('saved'); + vent.trigger(vent.Commands.CloseModalCommand); + }, + +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); +AsEditModalView.call(view); + +module.exports = view; diff --git a/src/UI/Movies/Files/EditFileCell.js b/src/UI/Movies/Files/EditFileCell.js new file mode 100644 index 000000000..27b831799 --- /dev/null +++ b/src/UI/Movies/Files/EditFileCell.js @@ -0,0 +1,22 @@ +var vent = require('vent'); +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Cell.extend({ + className : 'edit-episode-file-cell', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + this.$el.html(''); + + return this; + }, + + _onClick : function() { + var self = this; + vent.trigger(vent.Commands.EditFileCommand, { file : this.model }); + } +}); diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js index 3e6dd2bdd..c30c59564 100644 --- a/src/UI/Movies/Files/FilesLayout.js +++ b/src/UI/Movies/Files/FilesLayout.js @@ -19,89 +19,117 @@ var ProtocolCell = require('../../Release/ProtocolCell'); var PeersCell = require('../../Release/PeersCell'); var EditionCell = require('../../Cells/EditionCell'); var DeleteFileCell = require("./DeleteFileCell"); +var EditFileCell = require("./EditFileCell"); module.exports = Marionette.Layout.extend({ - template : 'Movies/Files/FilesLayoutTemplate', + template : 'Movies/Files/FilesLayoutTemplate', - regions : { - main : '#movie-files-region', - grid : "#movie-files-grid" - }, + regions : { + main : '#movie-files-region', + grid : "#movie-files-grid" + }, - events : { - 'click .x-search-auto' : '_searchAuto', - 'click .x-search-manual' : '_searchManual', - 'click .x-search-back' : '_showButtons' - }, + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, - columns : [ - { - name : 'title', - label : 'Title', - cell : FileTitleCell - }, - { - name : "mediaInfo", - label : "Media Info", - cell : MediaInfoCell - }, - { - name : 'edition', - label : 'Edition', - cell : EditionCell, - title : "Edition", - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - }, - { - name : "delete", - label : "", - cell : DeleteFileCell, - } - ], + columns : [ + { + name : 'title', + label : 'Title', + cell : FileTitleCell + }, + { + name : "mediaInfo", + label : "Media Info", + cell : MediaInfoCell + }, + { + name : 'edition', + label : 'Edition', + cell : EditionCell, + title : "Edition", + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + }, + { + name : "delete", + label : "", + cell : DeleteFileCell, + }, + { + name : "edit", + label : "", + cell : EditFileCell, + } + ], - initialize : function(movie) { - this.filesCollection = new FilesCollection(); - var file = movie.model.get("movieFile"); - this.filesCollection.add(file); - //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - }, + initialize : function(movie) { + this.filesCollection = new FilesCollection(); + var file = movie.model.get("movieFile"); + this.movie = movie; + this.filesCollection.add(file); + //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(movie); + } + }); - onShow : function() { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.filesCollection, - className : 'table table-hover' - })); - }, + vent.on(vent.Commands.CloseModalCommand, this._refreshClose, this); + }, - _showMainView : function() { - this.main.show(this.mainView); - }, + _refresh : function(movie) { + this.filesCollection = new FilesCollection(); + var file = movie.model.get("movieFile"); + this.filesCollection.add(file); + this.onShow(); + }, - _showButtons : function() { - this._showMainView(); - }, + _refreshClose : function(options) { + this.filesCollection = new FilesCollection(); + var file = this.movie.model.get("movieFile"); + this.filesCollection.add(file); + this.onShow(); + }, - _showSearchResults : function() { - if (this.releaseCollection.length === 0) { - this.mainView = new NoResultsView(); - } + onShow : function() { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.filesCollection, + className : 'table table-hover' + })); + }, - else { - //this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); - } + _showMainView : function() { + this.main.show(this.mainView); + }, - this._showMainView(); - } + _showButtons : function() { + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + //this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } }); diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js index 763ec21cd..8ec279436 100644 --- a/src/UI/Movies/MoviesCollection.js +++ b/src/UI/Movies/MoviesCollection.js @@ -10,9 +10,9 @@ var moment = require('moment'); require('../Mixins/backbone.signalr.mixin'); var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/movie', - model : MovieModel, - tableName : 'movie', + url : window.NzbDrone.ApiRoot + '/movie', + model : MovieModel, + tableName : 'movie', state : { sortKey : 'sortTitle', @@ -22,30 +22,30 @@ var Collection = PageableCollection.extend({ secondarySortOrder : -1 }, - mode : 'client', + mode : 'client', - save : function() { - var self = this; + save : function() { + var self = this; - var proxy = _.extend(new Backbone.Model(), { - id : '', + var proxy = _.extend(new Backbone.Model(), { + id : '', - url : self.url + '/editor', + url : self.url + '/editor', - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); + toJSON : function() { + return self.filter(function(model) { + return model.edited; + }); + } + }); - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true }); + this.trigger('save', this); + }); - return proxy.save(); - }, + return proxy.save(); + }, filterModes : { 'all' : [ @@ -85,82 +85,126 @@ var Collection = PageableCollection.extend({ ] }, - sortMappings : { - title : { - sortKey : 'sortTitle' - }, - statusWeight : { - sortValue : function(model, attr) { - if (model.getStatus() == "released") { - return 1; - } - if (model.getStatus() == "inCinemas") { - return 0; - } - return -1; - } - }, - downloadedQuality : { - sortValue : function(model, attr) { - if (model.get("movieFile")) { - return 1000-model.get("movieFile").quality.quality.id; - } - - return -1; - } - }, - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (order === 1) { - return 0; - } - - return Number.MAX_VALUE; - } - }, - status: { - sortValue : function(model, attr) { - debugger; - if (model.get("downloaded")) { - return -1; - } - return 0; - } - }, - percentOfEpisodes : { - sortValue : function(model, attr) { - var percentOfEpisodes = model.get(attr); - var episodeCount = model.get('episodeCount'); - - return percentOfEpisodes + episodeCount / 1000000; - } - }, - inCinemas : { - - sortValue : function(model, attr) { - var monthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - if (model.get("inCinemas")) { - return model.get("inCinemas"); - } - return "2100-01-01"; - } - }, - path : { - sortValue : function(model) { - var path = model.get('path'); - - return path.toLowerCase(); - } - } - } + importFromList : function(models) { + var self = this; + + var proxy = _.extend(new Backbone.Model(), { + id : "", + + url : self.url + "/import", + + toJSON : function() { + return models; + } + }); + + this.listenTo(proxy, "sync", function(proxyModel, models) { + this.add(models, { merge : true}); + this.trigger("save", this); + }); + + return proxy.save(); + }, + + filterModes : { + 'all' : [ + null, + null + ], + 'continuing' : [ + 'status', + 'continuing' + ], + 'ended' : [ + 'status', + 'ended' + ], + 'monitored' : [ + 'monitored', + true + ], + 'missing' : [ + 'downloaded', + false + ] + }, + + sortMappings : { + title : { + sortKey : 'sortTitle' + }, + statusWeight : { + sortValue : function(model, attr) { + if (model.getStatus() == "released") { + return 1; + } + if (model.getStatus() == "inCinemas") { + return 0; + } + return -1; + } + }, + downloadedQuality : { + sortValue : function(model, attr) { + if (model.get("movieFile")) { + return 1000-model.get("movieFile").quality.quality.id; + } + + return -1; + } + }, + nextAiring : { + sortValue : function(model, attr, order) { + var nextAiring = model.get(attr); + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (order === 1) { + return 0; + } + + return Number.MAX_VALUE; + } + }, + status: { + sortValue : function(model, attr) { + debugger; + if (model.get("downloaded")) { + return -1; + } + return 0; + } + }, + percentOfEpisodes : { + sortValue : function(model, attr) { + var percentOfEpisodes = model.get(attr); + var episodeCount = model.get('episodeCount'); + + return percentOfEpisodes + episodeCount / 1000000; + } + }, + inCinemas : { + + sortValue : function(model, attr) { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + if (model.get("inCinemas")) { + return model.get("inCinemas"); + } + return "2100-01-01"; + } + }, + path : { + sortValue : function(model) { + var path = model.get('path'); + + return path.toLowerCase(); + } + } + } }); Collection = AsFilteredCollection.call(Collection); diff --git a/src/UI/Settings/NetImport/Add/NetImportAddCollectionView.js b/src/UI/Settings/NetImport/Add/NetImportAddCollectionView.js new file mode 100644 index 000000000..46699fbd7 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/NetImportAddCollectionView.js @@ -0,0 +1,9 @@ +var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); +var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); +var AddItemView = require('./NetImportAddItemView'); + +module.exports = ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), + itemViewContainer : '.add-indexer .items', + template : 'Settings/NetImport/Add/NetImportAddCollectionViewTemplate' +}); diff --git a/src/UI/Settings/NetImport/Add/NetImportAddCollectionViewTemplate.hbs b/src/UI/Settings/NetImport/Add/NetImportAddCollectionViewTemplate.hbs new file mode 100644 index 000000000..ea3559a5b --- /dev/null +++ b/src/UI/Settings/NetImport/Add/NetImportAddCollectionViewTemplate.hbs @@ -0,0 +1,18 @@ + diff --git a/src/UI/Settings/NetImport/Add/NetImportAddItemView.js b/src/UI/Settings/NetImport/Add/NetImportAddItemView.js new file mode 100644 index 000000000..38fce07fb --- /dev/null +++ b/src/UI/Settings/NetImport/Add/NetImportAddItemView.js @@ -0,0 +1,51 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var AppLayout = require('../../../AppLayout'); +var Marionette = require('marionette'); +var EditView = require('../Edit/NetImportEditView'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/NetImport/Add/NetImportAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', + + events : { + 'click .x-preset' : '_addPreset', + 'click' : '_add' + }, + + initialize : function(options) { + this.targetCollection = options.targetCollection; + }, + + _addPreset : function(e) { + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; + + this.model.set(presetData); + + this._openEdit(); + }, + + _add : function(e) { + if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { + return; + } + + this._openEdit(); + }, + + _openEdit : function() { + this.model.set({ + id : undefined, + enableAuto : this.model.get('enableAuto') + }); + + var editView = new EditView({ + model : this.model, + targetCollection : this.targetCollection + }); + + AppLayout.modalRegion.show(editView); + } +}); diff --git a/src/UI/Settings/NetImport/Add/NetImportAddItemViewTemplate.hbs b/src/UI/Settings/NetImport/Add/NetImportAddItemViewTemplate.hbs new file mode 100644 index 000000000..9456cfef5 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/NetImportAddItemViewTemplate.hbs @@ -0,0 +1,30 @@ +
+
+ {{implementationName}} +
+
+ {{#if_gt presets.length compare=0}} + +
+ + +
+ {{/if_gt}} + {{#if infoLink}} + + + + {{/if}} +
+
diff --git a/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js b/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js new file mode 100644 index 000000000..42423ef18 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js @@ -0,0 +1,40 @@ +var _ = require('underscore'); +var AppLayout = require('../../../AppLayout'); +var Backbone = require('backbone'); +var SchemaCollection = require('../NetImportCollection'); +var AddCollectionView = require('./NetImportAddCollectionView'); + +module.exports = { + open : function(collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { + return model.get('protocol'); + }); + //key is "undefined", which is being placed in the header + var modelCollection = _.map(groups, function(values, key, list) { + return { + //"header" : key, + collection : values + }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ + collection : groupedSchemaCollection, + targetCollection : collection + }); + + AppLayout.modalRegion.show(view); + } +}; diff --git a/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js b/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js new file mode 100644 index 000000000..58e7e3eb5 --- /dev/null +++ b/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js @@ -0,0 +1,19 @@ +var vent = require('vent'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', + + events : { + 'click .x-confirm-delete' : '_delete' + }, + + _delete : function() { + this.model.destroy({ + wait : true, + success : function() { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs b/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs new file mode 100644 index 000000000..c5c7ad7db --- /dev/null +++ b/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Edit/NetImportEditView.js b/src/UI/Settings/NetImport/Edit/NetImportEditView.js new file mode 100644 index 000000000..ba08dd809 --- /dev/null +++ b/src/UI/Settings/NetImport/Edit/NetImportEditView.js @@ -0,0 +1,175 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var DeleteView = require('../Delete/IndexerDeleteView'); +var Profiles = require('../../../Profile/ProfileCollection'); +var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var AsEditModalView = require('../../../Mixins/AsEditModalView'); +var RootFolders = require('../../../AddMovies/RootFolders/RootFolderCollection'); +var RootFolderLayout = require('../../../AddMovies/RootFolders/RootFolderLayout'); +var Config = require('../../../Config'); +require('../../../Form/FormBuilder'); +require('../../../Mixins/AutoComplete'); +require('bootstrap'); + +var view = Marionette.ItemView.extend({ + template : 'Settings/NetImport/Edit/NetImportEditViewTemplate', + + ui : { + profile : '.x-profile', + rootFolder : '.x-root-folder', + }, + + events : { + 'click .x-back' : '_back', + 'click .x-captcha-refresh' : '_onRefreshCaptcha', + 'change .x-root-folder' : '_rootFolderChanged', + }, + + _deleteView : DeleteView, + + initialize : function(options) { + this.targetCollection = options.targetCollection; + this.templateHelpers = {}; + + this._configureTemplateHelpers(); + this.listenTo(this.model, 'change', this.render); + this.listenTo(RootFolders, 'all', this._rootFoldersUpdated); + }, + + onRender : function() { + var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); + if (RootFolders.get(defaultRoot)) { + this.ui.rootFolder.val(defaultRoot); + } + }, + + _onBeforeSave : function() { + var profile = this.ui.profile.val(); + var rootFolderPath = this.ui.rootFolder.children(':selected').text(); + this.model.set({ + profileId : profile, + rootFolderPath : rootFolderPath, + }) + }, + + _onAfterSave : function() { + this.targetCollection.add(this.model, { merge : true }); + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _onAfterSaveAndAdd : function() { + this.targetCollection.add(this.model, { merge : true }); + + require('../Add/NetImportSchemaModal').open(this.targetCollection); + }, + + _back : function() { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('../Add/NetImportSchemaModal').open(this.targetCollection); + }, + + _configureTemplateHelpers : function() { + this.templateHelpers.profiles = Profiles.toJSON(); + this.templateHelpers.rootFolders = RootFolders.toJSON(); + }, + + _rootFolderChanged : function() { + var rootFolderValue = this.ui.rootFolder.val(); + if (rootFolderValue === 'addNew') { + var rootFolderLayout = new RootFolderLayout(); + this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); + AppLayout.modalRegion.show(rootFolderLayout); + } else { + Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); + } + }, + + _rootFoldersUpdated : function() { + this._configureTemplateHelpers(); + debugger; + this.render(); + }, + + _onRefreshCaptcha : function(event) { + var self = this; + + var target = $(event.target).parents('.input-group'); + + this.ui.indicator.show(); + + this.model.requestAction("checkCaptcha") + .then(function(result) { + if (!result.captchaRequest) { + self.model.setFieldValue('CaptchaToken', ''); + + return result; + } + + return self._showCaptcha(target, result.captchaRequest); + }) + .always(function() { + self.ui.indicator.hide(); + }); + }, + + _showCaptcha : function(target, captchaRequest) { + var self = this; + + var widget = $('
').insertAfter(target); + + return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken) + .then(function(captchaResponse) { + target.parents('.form-group').removeAllErrors(); + widget.remove(); + + var queryParams = { + responseUrl : captchaRequest.responseUrl, + ray : captchaRequest.ray, + captchaResponse: captchaResponse + }; + + return self.model.requestAction("getCaptchaCookie", queryParams); + }) + .then(function(response) { + self.model.setFieldValue('CaptchaToken', response.captchaToken); + }); + }, + + _loadRecaptchaWidget : function(widget, sitekey, stoken) { + var promise = $.Deferred(); + + var renderWidget = function() { + window.grecaptcha.render(widget, { + 'sitekey' : sitekey, + 'stoken' : stoken, + 'callback' : promise.resolve + }); + }; + + if (window.grecaptcha) { + renderWidget(); + } else { + window.grecaptchaLoadCallback = function() { + delete window.grecaptchaLoadCallback; + renderWidget(); + }; + + $.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit') + .fail(function() { promise.reject(); }); + } + + return promise; + } +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); +AsEditModalView.call(view); + +module.exports = view; diff --git a/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs b/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs new file mode 100644 index 000000000..1d18dea5c --- /dev/null +++ b/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs @@ -0,0 +1,110 @@ + diff --git a/src/UI/Settings/NetImport/ListSelectionPartial.hbs b/src/UI/Settings/NetImport/ListSelectionPartial.hbs new file mode 100644 index 000000000..d2b37459d --- /dev/null +++ b/src/UI/Settings/NetImport/ListSelectionPartial.hbs @@ -0,0 +1,11 @@ + diff --git a/src/UI/Settings/NetImport/NetImportCollection.js b/src/UI/Settings/NetImport/NetImportCollection.js new file mode 100644 index 000000000..05dcd7a25 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportCollection.js @@ -0,0 +1,13 @@ +var Backbone = require('backbone'); +var NetImportModel = require('./NetImportModel'); + +module.exports = Backbone.Collection.extend({ + model : NetImportModel, + url : window.NzbDrone.ApiRoot + '/netimport', + + comparator : function(left, right, collection) { + var result = 0; + + return result; + } +}); diff --git a/src/UI/Settings/NetImport/NetImportCollectionView.hbs b/src/UI/Settings/NetImport/NetImportCollectionView.hbs new file mode 100644 index 000000000..1381f4c70 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportCollectionView.hbs @@ -0,0 +1,16 @@ +
+ Lists +
+
+
    +
  • +
    + + + +
    +
  • +
+
+
+
diff --git a/src/UI/Settings/NetImport/NetImportCollectionView.js b/src/UI/Settings/NetImport/NetImportCollectionView.js new file mode 100644 index 000000000..17ee0de4a --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportCollectionView.js @@ -0,0 +1,25 @@ +var Marionette = require('marionette'); +var ItemView = require('./NetImportItemView'); +var SchemaModal = require('./Add/NetImportSchemaModal'); + +module.exports = Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer : '.list-list', + template : 'Settings/NetImport/NetImportCollectionViewTemplate', + + ui : { + 'addCard' : '.x-add-card' + }, + + events : { + 'click .x-add-card' : '_openSchemaModal' + }, + + appendHtml : function(collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal : function() { + SchemaModal.open(this.collection); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportItemView.js b/src/UI/Settings/NetImport/NetImportItemView.js new file mode 100644 index 000000000..ff990e108 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportItemView.js @@ -0,0 +1,24 @@ +var AppLayout = require('../../AppLayout'); +var Marionette = require('marionette'); +var EditView = require('./Edit/NetImportEditView'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/NetImport/NetImportItemViewTemplate', + tagName : 'li', + + events : { + 'click' : '_edit' + }, + + initialize : function() { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit : function() { + var view = new EditView({ + model : this.model, + targetCollection : this.model.collection + }); + AppLayout.modalRegion.show(view); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs b/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs new file mode 100644 index 000000000..81f3041ba --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs @@ -0,0 +1,13 @@ +
+
+

{{name}}

+
+ +
+ {{#if enableAuto}} + Auto + {{else}} + Auto + {{/if}} +
+
diff --git a/src/UI/Settings/NetImport/NetImportLayout.js b/src/UI/Settings/NetImport/NetImportLayout.js new file mode 100644 index 000000000..def13963e --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportLayout.js @@ -0,0 +1,27 @@ +var Marionette = require('marionette'); +var NetImportCollection = require('./NetImportCollection'); +var CollectionView = require('./NetImportCollectionView'); +var OptionsView = require('./Options/NetImportOptionsView'); +var RootFolderCollection = require('../../AddMovies/RootFolders/RootFolderCollection'); + +module.exports = Marionette.Layout.extend({ + template : 'Settings/NetImport/NetImportLayoutTemplate', + + regions : { + lists : '#x-lists-region', + listOption : '#x-list-options-region', + }, + + initialize : function() { + this.indexersCollection = new NetImportCollection(); + this.indexersCollection.fetch(); + RootFolderCollection.fetch().done(function() { + RootFolderCollection.synced = true; + }); + }, + + onShow : function() { + this.lists.show(new CollectionView({ collection : this.indexersCollection })); + this.listOption.show(new OptionsView({ model : this.model })); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs new file mode 100644 index 000000000..c97943aa1 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/UI/Settings/NetImport/NetImportModel.js b/src/UI/Settings/NetImport/NetImportModel.js new file mode 100644 index 000000000..f072da427 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportModel.js @@ -0,0 +1,3 @@ +var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); + +module.exports = ProviderSettingsModelBase.extend({}); diff --git a/src/UI/Settings/NetImport/NetImportSettingsModel.js b/src/UI/Settings/NetImport/NetImportSettingsModel.js new file mode 100644 index 000000000..dbc882fbb --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportSettingsModel.js @@ -0,0 +1,7 @@ +var SettingsModelBase = require('../SettingsModelBase'); + +module.exports = SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/netimport', + successMessage : 'Net Import settings saved', + errorMessage : 'Failed to save net import settings' +}); diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsView.js b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js new file mode 100644 index 000000000..63fd1d9cc --- /dev/null +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsView.js @@ -0,0 +1,12 @@ +var Marionette = require('marionette'); +var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../../Mixins/AsValidatedView'); + +var view = Marionette.ItemView.extend({ + template : 'Settings/NetImport/Options/NetImportOptionsViewTemplate' +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); + +module.exports = view; diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs new file mode 100644 index 000000000..6e6072609 --- /dev/null +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs @@ -0,0 +1,16 @@ +
+ Options + +
+ + +
+ + +
+ +
+ +
+
+
diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionCollection.js b/src/UI/Settings/NetImport/Restriction/RestrictionCollection.js new file mode 100644 index 000000000..369250343 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionCollection.js @@ -0,0 +1,7 @@ +var Backbone = require('backbone'); +var RestrictionModel = require('./RestrictionModel'); + +module.exports = Backbone.Collection.extend({ + model : RestrictionModel, + url : window.NzbDrone.ApiRoot + '/Restriction' +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionCollectionView.js b/src/UI/Settings/NetImport/Restriction/RestrictionCollectionView.js new file mode 100644 index 000000000..58b3a6bfa --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionCollectionView.js @@ -0,0 +1,26 @@ +var AppLayout = require('../../../AppLayout'); +var Marionette = require('marionette'); +var RestrictionItemView = require('./RestrictionItemView'); +var EditView = require('./RestrictionEditView'); +require('../../../Tags/TagHelpers'); +require('bootstrap'); + +module.exports = Marionette.CompositeView.extend({ + template : 'Settings/Indexers/Restriction/RestrictionCollectionViewTemplate', + itemViewContainer : '.x-rows', + itemView : RestrictionItemView, + + events : { + 'click .x-add' : '_addMapping' + }, + + _addMapping : function() { + var model = this.collection.create({ tags : [] }); + var view = new EditView({ + model : model, + targetCollection : this.collection + }); + + AppLayout.modalRegion.show(view); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionCollectionViewTemplate.hbs b/src/UI/Settings/NetImport/Restriction/RestrictionCollectionViewTemplate.hbs new file mode 100644 index 000000000..6dc978854 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionCollectionViewTemplate.hbs @@ -0,0 +1,24 @@ +
+ Restrictions + +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionDeleteView.js b/src/UI/Settings/NetImport/Restriction/RestrictionDeleteView.js new file mode 100644 index 000000000..d2166c5ed --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionDeleteView.js @@ -0,0 +1,19 @@ +var vent = require('vent'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Restriction/RestrictionDeleteViewTemplate', + + events : { + 'click .x-confirm-delete' : '_delete' + }, + + _delete : function() { + this.model.destroy({ + wait : true, + success : function() { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionDeleteViewTemplate.hbs b/src/UI/Settings/NetImport/Restriction/RestrictionDeleteViewTemplate.hbs new file mode 100644 index 000000000..215631e5b --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionDeleteViewTemplate.hbs @@ -0,0 +1,13 @@ + diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionEditView.js b/src/UI/Settings/NetImport/Restriction/RestrictionEditView.js new file mode 100644 index 000000000..e8540d1a5 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionEditView.js @@ -0,0 +1,55 @@ +var _ = require('underscore'); +var vent = require('vent'); +var AppLayout = require('../../../AppLayout'); +var Marionette = require('marionette'); +var DeleteView = require('./RestrictionDeleteView'); +var CommandController = require('../../../Commands/CommandController'); +var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var AsEditModalView = require('../../../Mixins/AsEditModalView'); +require('../../../Mixins/TagInput'); +require('bootstrap'); +require('bootstrap.tagsinput'); + +var view = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Restriction/RestrictionEditViewTemplate', + + ui : { + required : '.x-required', + ignored : '.x-ignored', + tags : '.x-tags' + }, + + _deleteView : DeleteView, + + initialize : function(options) { + this.targetCollection = options.targetCollection; + }, + + onRender : function() { + this.ui.required.tagsinput({ + trimValue : true, + tagClass : 'label label-success' + }); + + this.ui.ignored.tagsinput({ + trimValue : true, + tagClass : 'label label-danger' + }); + + this.ui.tags.tagInput({ + model : this.model, + property : 'tags' + }); + }, + + _onAfterSave : function() { + this.targetCollection.add(this.model, { merge : true }); + vent.trigger(vent.Commands.CloseModalCommand); + } +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); +AsEditModalView.call(view); +module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionEditViewTemplate.hbs b/src/UI/Settings/NetImport/Restriction/RestrictionEditViewTemplate.hbs new file mode 100644 index 000000000..e02175c20 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionEditViewTemplate.hbs @@ -0,0 +1,60 @@ + diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionItemView.js b/src/UI/Settings/NetImport/Restriction/RestrictionItemView.js new file mode 100644 index 000000000..729d8ef7d --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionItemView.js @@ -0,0 +1,28 @@ +var AppLayout = require('../../../AppLayout'); +var Marionette = require('marionette'); +var EditView = require('./RestrictionEditView'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Restriction/RestrictionItemViewTemplate', + className : 'row', + + ui : { + tags : '.x-tags' + }, + + events : { + 'click .x-edit' : '_edit' + }, + + initialize : function() { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit : function() { + var view = new EditView({ + model : this.model, + targetCollection : this.model.collection + }); + AppLayout.modalRegion.show(view); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionItemViewTemplate.hbs b/src/UI/Settings/NetImport/Restriction/RestrictionItemViewTemplate.hbs new file mode 100644 index 000000000..d7648cb73 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionItemViewTemplate.hbs @@ -0,0 +1,12 @@ +
+ {{genericTagDisplay required 'label label-success'}} +
+
+ {{genericTagDisplay ignored 'label label-danger'}} +
+
+ {{tagDisplay tags}} +
+
+
+
\ No newline at end of file diff --git a/src/UI/Settings/NetImport/Restriction/RestrictionModel.js b/src/UI/Settings/NetImport/Restriction/RestrictionModel.js new file mode 100644 index 000000000..e8ea08465 --- /dev/null +++ b/src/UI/Settings/NetImport/Restriction/RestrictionModel.js @@ -0,0 +1,4 @@ +var $ = require('jquery'); +var DeepModel = require('backbone.deepmodel'); + +module.exports = DeepModel.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/list.less b/src/UI/Settings/NetImport/list.less new file mode 100644 index 000000000..4579f083a --- /dev/null +++ b/src/UI/Settings/NetImport/list.less @@ -0,0 +1,33 @@ +@import "../../Shared/Styles/clickable.less"; + +.lists-list { + li { + display: inline-block; + vertical-align: top; + } +} + +.list-item { + + .clickable; + + width: 290px; + height: 90px; + padding: 10px 15px; + + &.add-card { + .center { + margin-top: -3px; + } + } +} + +.modal-overflow { + overflow-y: visible; +} + +.add-list { + li.add-thingy-item { + width: 33%; + } +} diff --git a/src/UI/Settings/Profile/Edit/EditProfileView.js b/src/UI/Settings/Profile/Edit/EditProfileView.js index 23535d9e6..056a23d2c 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileView.js +++ b/src/UI/Settings/Profile/Edit/EditProfileView.js @@ -4,25 +4,38 @@ var LanguageCollection = require('../Language/LanguageCollection'); var Config = require('../../../Config'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsValidatedView = require('../../../Mixins/AsValidatedView'); +require('../../../Mixins/TagInput'); +require('bootstrap'); +require('bootstrap.tagsinput'); var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileViewTemplate', + template : 'Settings/Profile/Edit/EditProfileViewTemplate', - ui : { cutoff : '.x-cutoff' }, + ui : { cutoff : '.x-cutoff', + preferred : '.x-preferred', + }, - templateHelpers : function() { - return { - languages : LanguageCollection.toJSON() - }; - }, + onRender : function() { + this.ui.preferred.tagsinput({ + trimValue : true, + allowDuplicates: true, + tagClass : 'label label-success' + }); + }, - getCutoff : function() { - var self = this; + templateHelpers : function() { + return { + languages : LanguageCollection.toJSON() + }; + }, - return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); - } + getCutoff : function() { + var self = this; + + return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); + } }); AsValidatedView.call(view); -module.exports = AsModelBoundView.call(view); \ No newline at end of file +module.exports = AsModelBoundView.call(view); diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs index c19d10e5c..072a70ed0 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs @@ -1,45 +1,59 @@
- + -
- -
+
+ +

- - -
- -
- -
- -
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ +
- - -
- -
- -
- -
+ + +
+ +
+ +
+ +
diff --git a/src/UI/Settings/Profile/ProfileView.js b/src/UI/Settings/Profile/ProfileView.js index 4241c3f12..10a4a9be3 100644 --- a/src/UI/Settings/Profile/ProfileView.js +++ b/src/UI/Settings/Profile/ProfileView.js @@ -6,30 +6,32 @@ require('./AllowedLabeler'); require('./LanguageLabel'); require('bootstrap'); + var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/ProfileViewTemplate', - tagName : 'li', - - ui : { - "progressbar" : '.progress .bar', - "deleteButton" : '.x-delete' - }, - - events : { - 'click' : '_editProfile' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _editProfile : function() { - var view = new EditProfileView({ - model : this.model, - profileCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } + template : 'Settings/Profile/ProfileViewTemplate', + tagName : 'li', + + ui : { + "progressbar" : '.progress .bar', + "deleteButton" : '.x-delete', + + }, + + events : { + 'click' : '_editProfile' + }, + + initialize : function() { + this.listenTo(this.model, 'sync', this.render); + }, + + _editProfile : function() { + var view = new EditProfileView({ + model : this.model, + profileCollection : this.model.collection + }); + AppLayout.modalRegion.show(view); + } }); -module.exports = AsModelBoundView.call(view); \ No newline at end of file +module.exports = AsModelBoundView.call(view); diff --git a/src/UI/Settings/Profile/ProfileViewTemplate.hbs b/src/UI/Settings/Profile/ProfileViewTemplate.hbs index 4f5b3eef0..2f827a351 100644 --- a/src/UI/Settings/Profile/ProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/ProfileViewTemplate.hbs @@ -1,13 +1,13 @@
-
-

-
+
+

+
-
- {{languageLabel}} -
+
+ {{languageLabel}} +
-
    - {{allowedLabeler}} -
-
\ No newline at end of file +
    + {{allowedLabeler}} +
+
diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 429d702cd..8d852ea5f 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -12,6 +12,9 @@ var QualityLayout = require('./Quality/QualityLayout'); var IndexerLayout = require('./Indexers/IndexerLayout'); var IndexerCollection = require('./Indexers/IndexerCollection'); var IndexerSettingsModel = require('./Indexers/IndexerSettingsModel'); +var NetImportSettingsModel = require("./NetImport/NetImportSettingsModel"); +var NetImportCollection = require('./NetImport/NetImportCollection'); +var NetImportLayout = require('./NetImport/NetImportLayout'); var DownloadClientLayout = require('./DownloadClient/DownloadClientLayout'); var DownloadClientSettingsModel = require('./DownloadClient/DownloadClientSettingsModel'); var NotificationCollectionView = require('./Notifications/NotificationCollectionView'); @@ -24,229 +27,246 @@ var LoadingView = require('../Shared/LoadingView'); var Config = require('../Config'); module.exports = Marionette.Layout.extend({ - template : 'Settings/SettingsLayoutTemplate', - - regions : { - mediaManagement : '#media-management', - profiles : '#profiles', - quality : '#quality', - indexers : '#indexers', - downloadClient : '#download-client', - notifications : '#notifications', - metadata : '#metadata', - general : '#general', - uiRegion : '#ui', - loading : '#loading-region' - }, - - ui : { - mediaManagementTab : '.x-media-management-tab', - profilesTab : '.x-profiles-tab', - qualityTab : '.x-quality-tab', - indexersTab : '.x-indexers-tab', - downloadClientTab : '.x-download-client-tab', - notificationsTab : '.x-notifications-tab', - metadataTab : '.x-metadata-tab', - generalTab : '.x-general-tab', - uiTab : '.x-ui-tab', - advancedSettings : '.x-advanced-settings' - }, - - events : { - 'click .x-media-management-tab' : '_showMediaManagement', - 'click .x-profiles-tab' : '_showProfiles', - 'click .x-quality-tab' : '_showQuality', - 'click .x-indexers-tab' : '_showIndexers', - 'click .x-download-client-tab' : '_showDownloadClient', - 'click .x-notifications-tab' : '_showNotifications', - 'click .x-metadata-tab' : '_showMetadata', - 'click .x-general-tab' : '_showGeneral', - 'click .x-ui-tab' : '_showUi', - 'click .x-save-settings' : '_save', - 'change .x-advanced-settings' : '_toggleAdvancedSettings' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - - this.listenTo(vent, vent.Hotkeys.SaveSettings, this._save); - }, - - onRender : function() { - this.loading.show(new LoadingView()); - var self = this; - - this.mediaManagementSettings = new MediaManagementSettingsModel(); - this.namingSettings = new NamingModel(); - this.indexerSettings = new IndexerSettingsModel(); - this.downloadClientSettings = new DownloadClientSettingsModel(); - this.notificationCollection = new NotificationCollection(); - this.generalSettings = new GeneralSettingsModel(); - this.uiSettings = new UiSettingsModel(); - Backbone.$.when(this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), this.downloadClientSettings.fetch(), - this.notificationCollection.fetch(), this.generalSettings.fetch(), this.uiSettings.fetch()).done(function() { - if (!self.isClosed) { - self.loading.$el.hide(); - self.mediaManagement.show(new MediaManagementLayout({ - settings : self.mediaManagementSettings, - namingSettings : self.namingSettings - })); - self.profiles.show(new ProfileLayout()); - self.quality.show(new QualityLayout()); - self.indexers.show(new IndexerLayout({ model : self.indexerSettings })); - self.downloadClient.show(new DownloadClientLayout({ model : self.downloadClientSettings })); - self.notifications.show(new NotificationCollectionView({ collection : self.notificationCollection })); - self.metadata.show(new MetadataLayout()); - self.general.show(new GeneralView({ model : self.generalSettings })); - self.uiRegion.show(new UiView({ model : self.uiSettings })); - } - }); - - this._setAdvancedSettingsState(); - }, - - onShow : function() { - switch (this.action) { - case 'profiles': - this._showProfiles(); - break; - case 'quality': - this._showQuality(); - break; - case 'indexers': - this._showIndexers(); - break; - case 'downloadclient': - this._showDownloadClient(); - break; - case 'connect': - this._showNotifications(); - break; - case 'notifications': - this._showNotifications(); - break; - case 'metadata': - this._showMetadata(); - break; - case 'general': - this._showGeneral(); - break; - case 'ui': - this._showUi(); - break; - default: - this._showMediaManagement(); - } - }, - - _showMediaManagement : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.mediaManagementTab.tab('show'); - this._navigate('settings/mediamanagement'); - }, - - _showProfiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.profilesTab.tab('show'); - this._navigate('settings/profiles'); - }, - - _showQuality : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.qualityTab.tab('show'); - this._navigate('settings/quality'); - }, - - _showIndexers : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.indexersTab.tab('show'); - this._navigate('settings/indexers'); - }, - - _showDownloadClient : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.downloadClientTab.tab('show'); - this._navigate('settings/downloadclient'); - }, - - _showNotifications : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.notificationsTab.tab('show'); - this._navigate('settings/connect'); - }, - - _showMetadata : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.metadataTab.tab('show'); - this._navigate('settings/metadata'); - }, - - _showGeneral : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.generalTab.tab('show'); - this._navigate('settings/general'); - }, - - _showUi : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.uiTab.tab('show'); - this._navigate('settings/ui'); - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _save : function() { - vent.trigger(vent.Commands.SaveSettings); - }, - - _setAdvancedSettingsState : function() { - var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings); - this.ui.advancedSettings.prop('checked', checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } - }, - - _toggleAdvancedSettings : function() { - var checked = this.ui.advancedSettings.prop('checked'); - Config.setValue(Config.Keys.AdvancedSettings, checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } else { - $('body').removeClass('show-advanced-settings'); - } - } -}); \ No newline at end of file + template : 'Settings/SettingsLayoutTemplate', + + regions : { + mediaManagement : '#media-management', + profiles : '#profiles', + quality : '#quality', + indexers : '#indexers', + downloadClient : '#download-client', + netImport : "#net-import", + notifications : '#notifications', + metadata : '#metadata', + general : '#general', + uiRegion : '#ui', + loading : '#loading-region' + }, + + ui : { + mediaManagementTab : '.x-media-management-tab', + profilesTab : '.x-profiles-tab', + qualityTab : '.x-quality-tab', + indexersTab : '.x-indexers-tab', + downloadClientTab : '.x-download-client-tab', + netImportTab : ".x-net-import-tab", + notificationsTab : '.x-notifications-tab', + metadataTab : '.x-metadata-tab', + generalTab : '.x-general-tab', + uiTab : '.x-ui-tab', + advancedSettings : '.x-advanced-settings' + }, + + events : { + 'click .x-media-management-tab' : '_showMediaManagement', + 'click .x-profiles-tab' : '_showProfiles', + 'click .x-quality-tab' : '_showQuality', + 'click .x-indexers-tab' : '_showIndexers', + 'click .x-download-client-tab' : '_showDownloadClient', + "click .x-net-import-tab" : "_showNetImport", + 'click .x-notifications-tab' : '_showNotifications', + 'click .x-metadata-tab' : '_showMetadata', + 'click .x-general-tab' : '_showGeneral', + 'click .x-ui-tab' : '_showUi', + 'click .x-save-settings' : '_save', + 'change .x-advanced-settings' : '_toggleAdvancedSettings' + }, + + initialize : function(options) { + if (options.action) { + this.action = options.action.toLowerCase(); + } + + this.listenTo(vent, vent.Hotkeys.SaveSettings, this._save); + }, + + onRender : function() { + this.loading.show(new LoadingView()); + var self = this; + + this.mediaManagementSettings = new MediaManagementSettingsModel(); + this.namingSettings = new NamingModel(); + this.indexerSettings = new IndexerSettingsModel(); + this.netImportSettings = new NetImportSettingsModel(); + this.downloadClientSettings = new DownloadClientSettingsModel(); + this.notificationCollection = new NotificationCollection(); + this.generalSettings = new GeneralSettingsModel(); + this.uiSettings = new UiSettingsModel(); + Backbone.$.when(this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), this.downloadClientSettings.fetch(), + this.notificationCollection.fetch(), this.generalSettings.fetch(), this.uiSettings.fetch(), this.netImportSettings.fetch()).done(function() { + if (!self.isClosed) { + self.loading.$el.hide(); + self.mediaManagement.show(new MediaManagementLayout({ + settings : self.mediaManagementSettings, + namingSettings : self.namingSettings + })); + self.profiles.show(new ProfileLayout()); + self.quality.show(new QualityLayout()); + self.indexers.show(new IndexerLayout({ model : self.indexerSettings })); + self.downloadClient.show(new DownloadClientLayout({ model : self.downloadClientSettings })); + self.netImport.show(new NetImportLayout({model : self.netImportSettings})); + self.notifications.show(new NotificationCollectionView({ collection : self.notificationCollection })); + self.metadata.show(new MetadataLayout()); + self.general.show(new GeneralView({ model : self.generalSettings })); + self.uiRegion.show(new UiView({ model : self.uiSettings })); + } + }); + + this._setAdvancedSettingsState(); + }, + + onShow : function() { + switch (this.action) { + case 'profiles': + this._showProfiles(); + break; + case 'quality': + this._showQuality(); + break; + case 'indexers': + this._showIndexers(); + break; + case 'downloadclient': + this._showDownloadClient(); + break; + case "netimport": + this._showNetImport(); + break; + case 'connect': + this._showNotifications(); + break; + case 'notifications': + this._showNotifications(); + break; + case 'metadata': + this._showMetadata(); + break; + case 'general': + this._showGeneral(); + break; + case 'ui': + this._showUi(); + break; + default: + this._showMediaManagement(); + } + }, + + _showMediaManagement : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.mediaManagementTab.tab('show'); + this._navigate('settings/mediamanagement'); + }, + + _showProfiles : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.profilesTab.tab('show'); + this._navigate('settings/profiles'); + }, + + _showQuality : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.qualityTab.tab('show'); + this._navigate('settings/quality'); + }, + + _showIndexers : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.indexersTab.tab('show'); + this._navigate('settings/indexers'); + }, + + _showNetImport : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.netImportTab.tab('show'); + this._navigate('settings/netimport'); + }, + + _showDownloadClient : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.downloadClientTab.tab('show'); + this._navigate('settings/downloadclient'); + }, + + _showNotifications : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.notificationsTab.tab('show'); + this._navigate('settings/connect'); + }, + + _showMetadata : function(e) { + if (e) { + e.preventDefault(); + } + this.ui.metadataTab.tab('show'); + this._navigate('settings/metadata'); + }, + + _showGeneral : function(e) { + if (e) { + e.preventDefault(); + } + this.ui.generalTab.tab('show'); + this._navigate('settings/general'); + }, + + _showUi : function(e) { + if (e) { + e.preventDefault(); + } + this.ui.uiTab.tab('show'); + this._navigate('settings/ui'); + }, + + _navigate : function(route) { + Backbone.history.navigate(route, { + trigger : false, + replace : true + }); + }, + + _save : function() { + vent.trigger(vent.Commands.SaveSettings); + }, + + _setAdvancedSettingsState : function() { + var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings); + this.ui.advancedSettings.prop('checked', checked); + + if (checked) { + $('body').addClass('show-advanced-settings'); + } + }, + + _toggleAdvancedSettings : function() { + var checked = this.ui.advancedSettings.prop('checked'); + Config.setValue(Config.Keys.AdvancedSettings, checked); + + if (checked) { + $('body').addClass('show-advanced-settings'); + } else { + $('body').removeClass('show-advanced-settings'); + } + } +}); diff --git a/src/UI/Settings/SettingsLayoutTemplate.hbs b/src/UI/Settings/SettingsLayoutTemplate.hbs index c69ba9f16..c605e061f 100644 --- a/src/UI/Settings/SettingsLayoutTemplate.hbs +++ b/src/UI/Settings/SettingsLayoutTemplate.hbs @@ -1,49 +1,51 @@
-
-
- -
-
- -
+
+
+ +
+
+ +
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file +
diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index ec6bd2a1c..b5b788971 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -7,155 +7,148 @@ @import "Metadata/metadata"; @import "DownloadClient/downloadclient"; @import "thingy"; +@import "NetImport/list.less"; li.save-and-add { - .clickable; - - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: rgb(51, 51, 51); - white-space: nowrap; + .clickable; + + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: rgb(51, 51, 51); + white-space: nowrap; } li.save-and-add:hover { - text-decoration: none; - color: rgb(255, 255, 255); - background-color: rgb(0, 129, 194); + text-decoration: none; + color: rgb(255, 255, 255); + background-color: rgb(0, 129, 194); } .add-card { - .clickable; - color: #adadad; - font-size: 50px; - text-align: center; - background-color: #f5f5f5; - - .center { - display: inline-block; - padding: 5px 20px 0px; - background-color: white; - } - - i { - .clickable; - } + .clickable; + color: #adadad; + font-size: 50px; + text-align: center; + background-color: #f5f5f5; + + .center { + display: inline-block; + padding: 5px 20px 0px; + background-color: white; + } + + i { + .clickable; + } } .naming-example { - display: inline-block; - margin-top: 5px; + display: inline-block; + margin-top: 5px; } .naming-format { - width: 500px; + width: 500px; } .settings-controls { - margin-top: 10px; + margin-top: 10px; } .advanced-settings-toggle { - display: inline-block; - margin-bottom: 10px; - - .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : -10px; - margin-top : -1px; - } - - .help-inline-checkbox { - display : inline-block; - margin-top : -3px; - margin-bottom : 0; - vertical-align : middle; - } + display: inline-block; + margin-bottom: 10px; + + .checkbox { + width : 100px; + margin-left : 0px; + display : inline-block; + padding-top : 0px; + margin-bottom : -10px; + margin-top : -1px; + } + + .help-inline-checkbox { + display : inline-block; + margin-top : -3px; + margin-bottom : 0; + vertical-align : middle; + } } .advanced-setting { - display: none; + display: none; - .control-label { - color: @brand-warning; - } + .control-label { + color: @brand-warning; + } } .basic-setting { - display: block; + display: block; } .show-advanced-settings { - .advanced-setting { - display: block; - } + .advanced-setting { + display: block; + } - .basic-setting { - display: none; - } + .basic-setting { + display: none; + } } .api-key { - input { - width : 280px; - cursor : text; - } + input { + width : 280px; + cursor : text; + } } .settings-tabs { - li>a { - padding : 10px; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { - li { - a { - white-space : nowrap; - padding : 10px; - } - } - } + li>a { + padding : 10px; + white-space : nowrap; + } } .indicator { - display : none; - padding-right : 5px; + display : none; + padding-right : 5px; } .add-rule-setting-mapping { - cursor : pointer; - font-size : 14px; - text-align : center; - display : inline-block; - padding : 2px 6px; - - i { - cursor : pointer; - } + cursor : pointer; + font-size : 14px; + text-align : center; + display : inline-block; + padding : 2px 6px; + + i { + cursor : pointer; + } } .rule-setting-list { - .rule-setting-header .row { - font-weight : bold; - line-height : 40px; - } - - .rows .row { - line-height : 30px; - border-top : 1px solid #ddd; - vertical-align : middle; - padding : 5px; - - i { - cursor : pointer; - margin-left : 5px; - } - } + .rule-setting-header .row { + font-weight : bold; + line-height : 40px; + } + + .rows .row { + line-height : 30px; + border-top : 1px solid #ddd; + vertical-align : middle; + padding : 5px; + + i { + cursor : pointer; + margin-left : 5px; + } + } } diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less index 2368240b7..3b7a46b19 100644 --- a/src/UI/Settings/thingy.less +++ b/src/UI/Settings/thingy.less @@ -8,6 +8,10 @@ font-weight: lighter; text-align: center; height: 85px; + + .long-title { + font-size: 16px; + } } .add-thingies { @@ -36,7 +40,7 @@ h3 { margin-top: 0px; - display: inline-block; + //display: inline-block; //this stops the text-overflow from applying white-space: nowrap; overflow: hidden; line-height: 30px; diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 4392967df..f94f1d2e4 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -11,101 +11,108 @@ var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout'); var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); var MoviesDetailsLayout = require('../../Movies/Details/MoviesDetailsLayout'); +var EditFileView = require("../../Movies/Files/Edit/EditFileView"); module.exports = Marionette.AppRouter.extend({ - initialize : function() { - vent.on(vent.Commands.OpenModalCommand, this._openModal, this); - vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); - vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); - vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); - vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); - vent.on(vent.Commands.EditMovieCommand, this._editMovie, this); - vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); - vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); - vent.on(vent.Commands.ShowMovieDetails, this._showMovie, this); - vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); - vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); - vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); - vent.on(vent.Commands.ShowManualImport, this._showManualImport, this); - vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); - vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); - }, - - _openModal : function(view) { - AppLayout.modalRegion.show(view); - }, - - _closeModal : function() { - AppLayout.modalRegion.closeModal(); - }, - - _openModal2 : function(view) { - AppLayout.modalRegion2.show(view); - }, - - _closeModal2 : function() { - AppLayout.modalRegion2.closeModal(); - }, - - _editSeries : function(options) { - var view = new EditSeriesView({ model : options.series }); - AppLayout.modalRegion.show(view); - }, - - _editMovie : function(options) { - var view = new EditMovieView({ model : options.movie }); - AppLayout.modalRegion.show(view); - }, - - _deleteSeries : function(options) { - var view = new DeleteSeriesView({ model : options.series }); - AppLayout.modalRegion.show(view); - }, - - _showEpisode : function(options) { - var view = new EpisodeDetailsLayout({ - model : options.episode, - hideSeriesLink : options.hideSeriesLink, - openingTab : options.openingTab - }); - AppLayout.modalRegion.show(view); - }, - - _showMovie : function(options) { - var view = new MoviesDetailsLayout({ - model : options.movie, - hideSeriesLink : options.hideSeriesLink, - openingTab : options.openingTab - }); - AppLayout.modalRegion.show(view); - }, - - _showHistory : function(options) { - var view = new HistoryDetailsLayout({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showLogDetails : function(options) { - var view = new LogDetailsView({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showRenamePreview : function(options) { - var view = new RenamePreviewLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showManualImport : function(options) { - var view = new ManualImportLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showFileBrowser : function(options) { - var view = new FileBrowserLayout(options); - AppLayout.modalRegion2.show(view); - }, - - _closeFileBrowser : function() { - AppLayout.modalRegion2.closeModal(); - } + initialize : function() { + vent.on(vent.Commands.OpenModalCommand, this._openModal, this); + vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); + vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); + vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); + vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); + vent.on(vent.Commands.EditMovieCommand, this._editMovie, this); + vent.on(vent.Commands.EditFileCommand, this._editFile, this); + vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); + vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); + vent.on(vent.Commands.ShowMovieDetails, this._showMovie, this); + vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); + vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); + vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); + vent.on(vent.Commands.ShowManualImport, this._showManualImport, this); + vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); + vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); + }, + + _openModal : function(view) { + AppLayout.modalRegion.show(view); + }, + + _closeModal : function() { + AppLayout.modalRegion.closeModal(); + }, + + _openModal2 : function(view) { + AppLayout.modalRegion2.show(view); + }, + + _closeModal2 : function() { + AppLayout.modalRegion2.closeModal(); + }, + + _editSeries : function(options) { + var view = new EditSeriesView({ model : options.series }); + AppLayout.modalRegion.show(view); + }, + + _editMovie : function(options) { + var view = new EditMovieView({ model : options.movie }); + AppLayout.modalRegion.show(view); + }, + + _editFile : function(options) { + var view = new EditFileView({ model : options.file }); + AppLayout.modalRegion.show(view); + }, + + _deleteSeries : function(options) { + var view = new DeleteSeriesView({ model : options.series }); + AppLayout.modalRegion.show(view); + }, + + _showEpisode : function(options) { + var view = new EpisodeDetailsLayout({ + model : options.episode, + hideSeriesLink : options.hideSeriesLink, + openingTab : options.openingTab + }); + AppLayout.modalRegion.show(view); + }, + + _showMovie : function(options) { + var view = new MoviesDetailsLayout({ + model : options.movie, + hideSeriesLink : options.hideSeriesLink, + openingTab : options.openingTab + }); + AppLayout.modalRegion.show(view); + }, + + _showHistory : function(options) { + var view = new HistoryDetailsLayout({ model : options.model }); + AppLayout.modalRegion.show(view); + }, + + _showLogDetails : function(options) { + var view = new LogDetailsView({ model : options.model }); + AppLayout.modalRegion.show(view); + }, + + _showRenamePreview : function(options) { + var view = new RenamePreviewLayout(options); + AppLayout.modalRegion.show(view); + }, + + _showManualImport : function(options) { + var view = new ManualImportLayout(options); + AppLayout.modalRegion.show(view); + }, + + _showFileBrowser : function(options) { + var view = new FileBrowserLayout(options); + AppLayout.modalRegion2.show(view); + }, + + _closeFileBrowser : function() { + AppLayout.modalRegion2.closeModal(); + } }); diff --git a/src/UI/vent.js b/src/UI/vent.js index 1962f9d22..a6a7be318 100644 --- a/src/UI/vent.js +++ b/src/UI/vent.js @@ -3,40 +3,41 @@ var Wreqr = require('./JsLibraries/backbone.wreqr'); var vent = new Wreqr.EventAggregator(); vent.Events = { - SeriesAdded : 'series:added', - SeriesDeleted : 'series:deleted', - CommandComplete : 'command:complete', - ServerUpdated : 'server:updated', - EpisodeFileDeleted : 'episodefile:deleted' + SeriesAdded : 'series:added', + SeriesDeleted : 'series:deleted', + CommandComplete : 'command:complete', + ServerUpdated : 'server:updated', + EpisodeFileDeleted : 'episodefile:deleted' }; vent.Commands = { - EditSeriesCommand : 'EditSeriesCommand', - EditMovieCommand : 'EditMovieCommand', - DeleteSeriesCommand : 'DeleteSeriesCommand', - OpenModalCommand : 'OpenModalCommand', - CloseModalCommand : 'CloseModalCommand', - OpenModal2Command : 'OpenModal2Command', - CloseModal2Command : 'CloseModal2Command', - ShowEpisodeDetails : 'ShowEpisodeDetails', - ShowMovieDetails : 'ShowMovieDetails', - ShowHistoryDetails : 'ShowHistoryDetails', - ShowLogDetails : 'ShowLogDetails', - SaveSettings : 'saveSettings', - ShowLogFile : 'showLogFile', - ShowRenamePreview : 'showRenamePreview', - ShowManualImport : 'showManualImport', - ShowFileBrowser : 'showFileBrowser', - CloseFileBrowser : 'closeFileBrowser', - OpenControlPanelCommand : 'OpenControlPanelCommand', - CloseControlPanelCommand : 'CloseControlPanelCommand', - ShowExistingCommand : 'ShowExistingCommand' + EditSeriesCommand : 'EditSeriesCommand', + EditMovieCommand : 'EditMovieCommand', + EditFileCommand : "EditFileCommand", + DeleteSeriesCommand : 'DeleteSeriesCommand', + OpenModalCommand : 'OpenModalCommand', + CloseModalCommand : 'CloseModalCommand', + OpenModal2Command : 'OpenModal2Command', + CloseModal2Command : 'CloseModal2Command', + ShowEpisodeDetails : 'ShowEpisodeDetails', + ShowMovieDetails : 'ShowMovieDetails', + ShowHistoryDetails : 'ShowHistoryDetails', + ShowLogDetails : 'ShowLogDetails', + SaveSettings : 'saveSettings', + ShowLogFile : 'showLogFile', + ShowRenamePreview : 'showRenamePreview', + ShowManualImport : 'showManualImport', + ShowFileBrowser : 'showFileBrowser', + CloseFileBrowser : 'closeFileBrowser', + OpenControlPanelCommand : 'OpenControlPanelCommand', + CloseControlPanelCommand : 'CloseControlPanelCommand', + ShowExistingCommand : 'ShowExistingCommand' }; vent.Hotkeys = { - NavbarSearch : 'navbar:search', - SaveSettings : 'settings:save', - ShowHotkeys : 'hotkeys:show' + NavbarSearch : 'navbar:search', + SaveSettings : 'settings:save', + ShowHotkeys : 'hotkeys:show' }; module.exports = vent;