Merge pull request #497 from Radarr/feature/net-import

Feature/net import
pull/2/head
Devin Buhl 8 years ago committed by GitHub
commit a1cb5eb420

@ -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
{
@ -147,6 +150,18 @@ namespace NzbDrone.Api.ClientSchema
private static List<SelectOption> GetSelectOptions(Type selectOptions)
{
if (selectOptions == typeof(Profile))
{
return new List<SelectOption>();
}
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() };

@ -0,0 +1,22 @@
using FluentValidation;
using NzbDrone.Api.Validation;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Config
{
public class NetImportConfigModule : NzbDroneConfigModule<NetImportConfigResource>
{
public NetImportConfigModule(IConfigService configService)
: base(configService)
{
SharedValidator.RuleFor(c => c.NetImportSyncInterval)
.IsValidNetImportSyncInterval();
}
protected override NetImportConfigResource ToResource(IConfigService model)
{
return NetImportConfigResourceMapper.ToResource(model);
}
}
}

@ -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
};
}
}
}

@ -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<List<MovieResource>>();
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);
}
}
}

@ -0,0 +1,44 @@
using NzbDrone.Api.ClientSchema;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.Profiles;
namespace NzbDrone.Api.NetImport
{
public class NetImportModule : ProviderModuleBase<NetImportResource, INetImport, NetImportDefinition>
{
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);
}
}
}

@ -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; }
}
}

@ -109,6 +109,8 @@
<Compile Include="ClientSchema\SelectOption.cs" />
<Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandResource.cs" />
<Compile Include="Config\NetImportConfigModule.cs" />
<Compile Include="Config\NetImportConfigResource.cs" />
<Compile Include="Extensions\AccessControlHeaders.cs" />
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
<Compile Include="Extensions\Pipelines\RequestLoggingPipeline.cs" />
@ -121,6 +123,9 @@
<Compile Include="Movies\RenameMovieModule.cs" />
<Compile Include="Movies\RenameMovieResource.cs" />
<Compile Include="Movies\MovieEditorModule.cs" />
<Compile Include="NetImport\ListImportModule.cs" />
<Compile Include="NetImport\NetImportModule.cs" />
<Compile Include="NetImport\NetImportResource.cs" />
<Compile Include="Parse\ParseModule.cs" />
<Compile Include="Parse\ParseResource.cs" />
<Compile Include="ManualImport\ManualImportModule.cs" />
@ -234,6 +239,7 @@
<Compile Include="SeasonPass\SeasonPassResource.cs" />
<Compile Include="Series\AlternateTitleResource.cs" />
<Compile Include="Series\MovieFileResource.cs" />
<Compile Include="Series\FetchMovieListModule.cs" />
<Compile Include="Series\SeasonResource.cs" />
<Compile Include="SeasonPass\SeasonPassModule.cs" />
<Compile Include="Series\SeriesEditorModule.cs" />
@ -254,6 +260,7 @@
<Compile Include="TinyIoCNancyBootstrapper.cs" />
<Compile Include="Update\UpdateModule.cs" />
<Compile Include="Update\UpdateResource.cs" />
<Compile Include="Validation\NetImportSyncIntervalValidator.cs" />
<Compile Include="Validation\RssSyncIntervalValidator.cs" />
<Compile Include="Validation\EmptyCollectionValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" />
@ -295,4 +302,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

@ -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<MovieResource>
{
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<Core.Tv.Movie> realResults = new List<Core.Tv.Movie>();
/*foreach (var movie in results)
{
var mapped = _movieSearch.MapMovieToTmdbMovie(movie);
if (mapped != null)
{
realResults.Add(mapped);
}
}*/
return MapToResource(results).AsResponse();
}
private static IEnumerable<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> 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;
}
}
}
}

@ -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;
}
}
}

@ -36,5 +36,10 @@ namespace NzbDrone.Api.Validation
{
return ruleBuilder.SetValidator(new RssSyncIntervalValidator());
}
public static IRuleBuilderOptions<T, int> IsValidNetImportSyncInterval<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator());
}
}
}

@ -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
}

File diff suppressed because it is too large Load Diff

@ -45,6 +45,7 @@ namespace NzbDrone.Core.Test.IndexerTests
return new IndexerResponse(new IndexerRequest(httpRequest), httpResponse);
}
[Test]
public void should_handle_relative_url()
{

@ -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<CouchPotatoParser>
{
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");
}
}
}

@ -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<RSSImport>
{
[SetUp]
public void Setup()
{
Subject.Definition = Subject.DefaultDefinitions.First();
}
private void GivenRecentFeedResponse(string rssXmlFile)
{
var recentFeed = ReadAllText(@"Files/" + rssXmlFile);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(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");
}
}
}

@ -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<RSSImportParser>
{
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");
}
}
}

@ -190,6 +190,9 @@
<Compile Include="Download\Pending\PendingReleaseServiceTests\RemoveRejectedFixture.cs" />
<Compile Include="Download\Pending\PendingReleaseServiceTests\RemoveGrabbedFixture.cs" />
<Compile Include="Download\Pending\PendingReleaseServiceTests\AddFixture.cs" />
<None Include="Files\couchpotato_movie_list.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Files\Indexers\Rarbg\RecentFeed_v1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@ -284,6 +287,9 @@
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchSeriesComparerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoParserFixture.cs" />
<Compile Include="NetImport\RSSImportFixture.cs" />
<Compile Include="NetImport\RSSImportParserFixture.cs" />
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\EpisodeTitleCollapseFixture.cs" />
@ -409,6 +415,9 @@
<Link>sqlite3.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\imdb_watchlist.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="License.txt" />
<None Include="Files\Indexers\BroadcastheNet\RecentFeed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

@ -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); }

@ -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; }

@ -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();
}
}
}
}

@ -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<NetImportDefinition>().RegisterDefinition("NetImport")
.Ignore(i => i.Enable)
.Relationship()
.HasOne(n => n.Profile, n => n.ProfileId);
Mapper.Entity<NotificationDefinition>().RegisterDefinition("Notifications")
.Ignore(i => i.SupportsOnGrab)
.Ignore(i => i.SupportsOnDownload)

@ -306,6 +306,8 @@ namespace NzbDrone.Core.Indexers
request.HttpRequest.RateLimit = RateLimit;
}
request.HttpRequest.AllowAutoRedirect = true;
return new IndexerResponse(request, _httpClient.Execute(request.HttpRequest));
}

@ -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<ScheduledTask> { rss, downloadedEpisodes });
var netImport = _scheduledTaskRepository.GetDefinition(typeof(NetImportSyncCommand));
netImport.Interval = _configService.NetImportSyncInterval;
_scheduledTaskRepository.UpdateMany(new List<ScheduledTask> { rss, downloadedEpisodes, netImport });
}
}
}

@ -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;

@ -6,5 +6,7 @@ namespace NzbDrone.Core.MetadataSource
public interface ISearchForNewMovie
{
List<Movie> SearchForNewMovie(string title);
Movie MapMovieToTmdbMovie(Movie movie);
}
}

@ -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; }

@ -88,6 +88,18 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var response = _httpClient.Get<MovieResourceRoot>(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;
}
}
}

@ -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<Release> 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; }
}
}

@ -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<CouchPotatoSettings>
{
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<ProviderDefinition> 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);
}
}
}

@ -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<Tv.Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<Tv.Movie>();
if (!PreProcess(_importResponse))
{
return movies;
}
var jsonResponse = JsonConvert.DeserializeObject<CouchPotatoResponse>(_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;
}
}
}

@ -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<NetImportRequest> 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;
}
}
}

@ -0,0 +1,38 @@
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 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; }
}
}

@ -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;
}
}

@ -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<TSettings> : NetImportBase<TSettings>
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<Movie> Fetch()
{
var generator = GetRequestGenerator();
return FetchMovies(generator.GetMovies());
}
protected virtual IList<Movie> FetchMovies(NetImportPageableRequestChain pageableRequestChain, bool isRecent = false)
{
var movies = new List<Movie>();
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<Movie>();
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<Movie> page)
{
return PageSize != 0 && page.Count >= PageSize;
}
protected virtual IList<Movie> 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<ValidationFailure> 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;
}
}
}

@ -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<Movie> Fetch();
}
}

@ -0,0 +1,9 @@
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.NetImport
{
public interface INetImportRequestGenerator
{
NetImportPageableRequestChain GetMovies();
}
}

@ -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<Movie> ParseResponse(NetImportResponse netMovieImporterResponse);
}
}

@ -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<TSettings> : 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<ProviderDefinition> 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<string, string> query) { return null; }
protected TSettings Settings => (TSettings)Definition.Settings;
public abstract IList<Movie> Fetch();
public ValidationResult Test()
{
var failures = new List<ValidationFailure>();
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<ValidationFailure> failures);
public override string ToString()
{
return Definition.Name;
}
}
}

@ -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<NetImportBaseSettings>
{
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));
}
}
}

@ -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> Profile { get; set; }
public string RootFolderPath { get; set; }
public override bool Enable => Enabled;
}
}

@ -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<INetImport, NetImportDefinition>
{
List<INetImport> Enabled();
}
public class NetImportFactory : ProviderFactory<INetImport, NetImportDefinition>, INetImportFactory
{
private readonly INetImportRepository _providerRepository;
private readonly Logger _logger;
public NetImportFactory(INetImportRepository providerRepository,
IEnumerable<INetImport> providers,
IContainer container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
_providerRepository = providerRepository;
_logger = logger;
}
protected override List<NetImportDefinition> Active()
{
return base.Active().Where(c => c.Enabled).ToList();
}
public override void SetProviderCharacteristics(INetImport provider, NetImportDefinition definition)
{
base.SetProviderCharacteristics(provider, definition);
}
public List<INetImport> Enabled()
{
var enabledImporters = GetAvailableProviders().Where(n => ((NetImportDefinition)n.Definition).Enabled);
var indexers = FilterBlockedIndexers(enabledImporters);
return indexers.ToList();
}
private IEnumerable<INetImport> FilterBlockedIndexers(IEnumerable<INetImport> importers)
{
foreach (var importer in importers)
{
yield return importer;
}
}
}
}

@ -0,0 +1,25 @@
using System.Collections;
using System.Collections.Generic;
namespace NzbDrone.Core.NetImport
{
public class NetImportPageableRequest : IEnumerable<NetImportRequest>
{
private readonly IEnumerable<NetImportRequest> _enumerable;
public NetImportPageableRequest(IEnumerable<NetImportRequest> enumerable)
{
_enumerable = enumerable;
}
public IEnumerator<NetImportRequest> GetEnumerator()
{
return _enumerable.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _enumerable.GetEnumerator();
}
}
}

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.NetImport
{
public class NetImportPageableRequestChain
{
private List<List<NetImportPageableRequest>> _chains;
public NetImportPageableRequestChain()
{
_chains = new List<List<NetImportPageableRequest>>();
_chains.Add(new List<NetImportPageableRequest>());
}
public int Tiers => _chains.Count;
public IEnumerable<NetImportPageableRequest> GetAllTiers()
{
return _chains.SelectMany(v => v);
}
public IEnumerable<NetImportPageableRequest> GetTier(int index)
{
return _chains[index];
}
public void Add(IEnumerable<NetImportRequest> request)
{
if (request == null) return;
_chains.Last().Add(new NetImportPageableRequest(request));
}
public void AddTier(IEnumerable<NetImportRequest> request)
{
AddTier();
Add(request);
}
public void AddTier()
{
if (_chains.Last().Count == 0) return;
_chains.Add(new List<NetImportPageableRequest>());
}
}
}

@ -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<NetImportDefinition>
{
}
public class NetImportRepository : ProviderRepository<NetImportDefinition>, INetImportRepository
{
public NetImportRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

@ -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;
}
}

@ -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;
}
}

@ -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<Movie> Fetch(int listId, bool onlyEnableAuto);
List<Movie> FetchAndFilter(int listId, bool onlyEnableAuto);
}
public class NetImportSearchService : IFetchNetImport, IExecute<NetImportSyncCommand>
{
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<Movie> Fetch(int listId, bool onlyEnableAuto = false)
{
return MovieListSearch(listId, onlyEnableAuto);
}
public List<Movie> FetchAndFilter(int listId, bool onlyEnableAuto)
{
var movies = MovieListSearch(listId, onlyEnableAuto);
return movies.Where(x => !_movieService.MovieExists(x)).ToList();
}
public List<Movie> MovieListSearch(int listId, bool onlyEnableAuto = false)
{
var movies = new List<Movie>();
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);
}
}
}
}
}

@ -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;
}
}

@ -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<RSSImportSettings>
{
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<ProviderDefinition> 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);
}
}
}

@ -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<Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<Movie>();
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<XElement> GetItems(XDocument document)
{
var root = document.Root;
if (root == null)
{
return Enumerable.Empty<XElement>();
}
var channel = root.Element("channel");
if (channel == null)
{
return Enumerable.Empty<XElement>();
}
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;
}
}
}
}

@ -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<NetImportRequest> GetMovies(string searchParameters)
{
var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Rss);
yield return request;
}
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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<StevenLuSettings>
{
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);
}
}
}

@ -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<Tv.Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<Tv.Movie>();
if (!PreProcess(_importResponse))
{
return movies;
}
var jsonResponse = JsonConvert.DeserializeObject<List<StevenLuResponse>>(_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;
}
}
}

@ -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<NetImportRequest> GetMovies(string searchParameters)
{
var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Json);
yield return request;
}
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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<TraktSettings>
{
public override string Name => "Trakt User 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);
}
}
}

@ -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<Tv.Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<Tv.Movie>();
if (!PreProcess(_importResponse))
{
return movies;
}
var jsonResponse = JsonConvert.DeserializeObject<List<TraktResponse>>(_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
});
}
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;
}
}
}

@ -0,0 +1,35 @@
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<NetImportRequest> GetMovies(string searchParameters)
{
// https://api.trakt.tv/users/timdturner/lists/custom1/items/movies
// trakt-api-version = 2
// trakt-api-key = 657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0
var request = new NetImportRequest($"{Settings.Link.Trim()}{Settings.Username.Trim()}/lists/{Settings.Listname.Trim()}/items/movies", HttpAccept.Json);
request.HttpRequest.Headers.Add("trakt-api-version", "2");
request.HttpRequest.Headers.Add("trakt-api-key", "657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0");
yield return request;
}
}
}

@ -0,0 +1,30 @@
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 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 Username", HelpText = "Trakt Username the list belongs to.")]
public string Username { get; set; }
[FieldDefinition(2, Label = "Trakt List Name", HelpText = "Trakt List Name")]
public string Listname { get; set; }
}
}

@ -211,7 +211,7 @@ 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;

@ -17,7 +17,7 @@ namespace NzbDrone.Core.Notifications.Plex
public interface IPlexServerProxy
{
List<PlexSection> GetTvSections(PlexServerSettings settings);
List<PlexSection> GetMovieSections(PlexServerSettings settings);
List<PlexSection> 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<PlexMediaContainerLegacy>(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();
}

@ -86,8 +86,8 @@ namespace NzbDrone.Core.Notifications.Slack
};
NotifySlack(payload);
}
}
public override void OnRename(Series series)
{
var payload = new SlackPayload

@ -122,6 +122,41 @@
<Compile Include="Authentication\User.cs" />
<Compile Include="Authentication\UserRepository.cs" />
<Compile Include="Authentication\UserService.cs" />
<Compile Include="Datastore\Migration\123_create_netimport_table.cs" />
<Compile Include="NetImport\Trakt\TraktAPI.cs" />
<Compile Include="NetImport\Trakt\TraktImport.cs" />
<Compile Include="NetImport\Trakt\TraktParser.cs" />
<Compile Include="NetImport\Trakt\TraktRequestGenerator.cs" />
<Compile Include="NetImport\Trakt\TraktSettings.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoAPI.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoParser.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoRequestGenerator.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoSettings.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoImport.cs" />
<Compile Include="NetImport\StevenLu\StevenLuAPI.cs" />
<Compile Include="NetImport\StevenLu\StevenLuParser.cs" />
<Compile Include="NetImport\StevenLu\StevenLuRequestGenerator.cs" />
<Compile Include="NetImport\StevenLu\StevenLuSettings.cs" />
<Compile Include="NetImport\StevenLu\StevenLuImport.cs" />
<Compile Include="NetImport\Exceptions\NetImportException.cs" />
<Compile Include="NetImport\HttpNetImportBase.cs" />
<Compile Include="NetImport\NetImportSearchService.cs" />
<Compile Include="NetImport\NetImportFactory.cs" />
<Compile Include="NetImport\IProcessNetImportResponse.cs" />
<Compile Include="NetImport\NetImportBaseSettings.cs" />
<Compile Include="NetImport\NetImportPageableRequest.cs" />
<Compile Include="NetImport\NetImportPageableRequestChain.cs" />
<Compile Include="NetImport\INetImportRequestGenerator.cs" />
<Compile Include="NetImport\RSSImport\RSSImportParser.cs" />
<Compile Include="NetImport\RSSImport\RSSImportRequestGenerator.cs" />
<Compile Include="NetImport\NetImportRequest.cs" />
<Compile Include="NetImport\NetImportResponse.cs" />
<Compile Include="NetImport\NetImportBase.cs" />
<Compile Include="NetImport\NetImportRepository.cs" />
<Compile Include="NetImport\INetImport.cs" />
<Compile Include="NetImport\NetImportDefinition.cs" />
<Compile Include="NetImport\RSSImport\RSSImport.cs" />
<Compile Include="NetImport\RSSImport\RSSImportSettings.cs" />
<Compile Include="Backup\Backup.cs" />
<Compile Include="Backup\BackupCommand.cs" />
<Compile Include="Backup\BackupService.cs" />
@ -864,6 +899,7 @@
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="MetadataSource\TmdbConfigurationService.cs" />
<Compile Include="NetImport\NetImportSyncCommand.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" />
<Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" />
<Compile Include="Notifications\Join\JoinResponseModel.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<Episode> {episodes.First()};
var eps = new List<Episode> { 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<Match>()
return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
.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
}
}
}

@ -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(@"(?<imdbid>tt\d{9})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|576[ip]|720[ip]|1080[ip]|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;
}

@ -45,7 +45,7 @@ namespace NzbDrone.Core.ThingiProvider
{
var definition = provider.DefaultDefinitions
.OfType<TProviderDefinition>()
.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<TProviderDefinition>()
.Where(v => v.Name != null && v.Name != provider.GetType().Name)
.Where(v => v.Name != null && v.Name != provider.Name)
.ToList();
return definitions;

@ -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<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
List<Movie> 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<string, string> 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<Movie> 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();
}
}
}

@ -21,11 +21,13 @@ namespace NzbDrone.Core.Tv
Movie GetMovie(int movieId);
List<Movie> GetMovies(IEnumerable<int> movieIds);
Movie AddMovie(Movie newMovie);
List<Movie> AddMovies(List<Movie> 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<Movie> GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec);
@ -91,6 +93,35 @@ namespace NzbDrone.Core.Tv
return newMovie;
}
public List<Movie> AddMovies(List<Movie> 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;
}
}
}

@ -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());
}
});

@ -1,43 +1,46 @@
<div class="row">
<div class="col-md-12">
<div class="btn-group add-movies-btn-group btn-group-lg btn-block">
<button type="button" class="btn btn-default col-md-10 col-xs-8 add-movies-import-btn x-import">
<i class="icon-sonarr-hdd"/>
Import existing movies on disk
</button>
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button>
</div>
</div>
<div class="col-md-12">
<div class="btn-group add-movies-btn-group btn-group-lg btn-block">
<button type="button" class="btn btn-default col-md-7 col-xs-4 add-movies-import-btn x-import">
<i class="icon-sonarr-hdd"/>
Import existing movies on disk
</button>
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button>
<button class="btn btn-default col-md-3 col-xs-4 x-add-lists"><i class="icon-sonarr-active hidden-xs"></i> Add Movies from Lists</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" style="margin-bottom: 0px;">
<label class="col-sm-3 control-label">Display Existing Movies</label>
<div class="col-md-12">
<div class="form-horizontal" style="margin-top: 15px;">
<div id="show-existing-movies-toggle">
<div class="form-group" style="margin-bottom: 0px;">
<label class="col-sm-3 control-label">Display Existing Movies</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input class="x-show-existing" type="checkbox" checked="checked" name="showExisting"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input class="x-show-existing" type="checkbox" checked="checked" name="showExisting"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-info" title="Should Radarr display movies already in your collection?"/>
</span>
</div>
</div>
</div>
</div>
</div>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-info" title="Should Radarr display movies already in your collection?"/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="add-movies-workspace"></div>
</div>
<div class="col-md-12">
<div id="add-movies-workspace"></div>
</div>
</div>

@ -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;
}
});

@ -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
// }
});

@ -0,0 +1,4 @@
<div class="x-list">
<div class="x-loading-list">
</div>
</div>

@ -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 : "" }));
}
});

@ -0,0 +1,18 @@
<div class="x-search-bar">
<div class="form-group" style="margin-bottom: 0px;">
<label class="col-sm-1 control-label">List</label>
<div class="col-sm-8">
{{> ListSelectionPartial lists}}
</div>
<div class="col-sm-1">
<button class="btn x-fetch-list">Fetch List</button>
</div>
<div class="col-sm-2">
<button class="btn btn-success x-import-selected"><i class="icon-sonarr-add"></i> Import Selected</button>
</div>
</div>
</div>
<div class="row">
<div id="fetch-result" class="result-list col-md-12"/>
</div>

@ -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;

@ -0,0 +1,3 @@
<div class="fetch-item">
ASDF
</div>

@ -0,0 +1,7 @@
var TemplatedCell = require('./TemplatedCell');
module.exports = TemplatedCell.extend({
className : 'series-title-cell',
template : 'Cells/MovieListTitleTemplate',
});

@ -0,0 +1 @@
<a href="{{imdbUrl}}">{{title}}</a>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -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);

@ -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'
});

@ -0,0 +1,18 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Add List</h3>
</div>
<div class="modal-body">
<div class="alert alert-info">
Radarr supports any RSS movie lists as well as the one stated below.<br/>
For more information on the individual lists, click on the info buttons.
</div>
<div class="add-indexer add-thingies">
<ul class="items"></ul>
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">Close</button>
</div>
</div>

@ -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);
}
});

@ -0,0 +1,30 @@
<div class="add-thingy">
<div>
{{implementationName}}
</div>
<div class="pull-right">
{{#if_gt presets.length compare=0}}
<button class="btn btn-xs btn-default x-custom">
Custom
</button>
<div class="btn-group">
<button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
Presets
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{{#each presets}}
<li class="x-preset" data-id="{{name}}">
<a>{{name}}</a>
</li>
{{/each}}
</ul>
</div>
{{/if_gt}}
{{#if infoLink}}
<a class="btn btn-xs btn-default x-info" href="{{infoLink}}">
<i class="icon-sonarr-form-info"/>
</a>
{{/if}}
</div>
</div>

@ -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);
}
};

@ -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);
}
});
}
});

@ -0,0 +1,13 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Delete Indexer</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete '{{name}}'?</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger x-confirm-delete">Delete</button>
</div>
</div>

@ -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 = $('<div class="g-recaptcha"></div>').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;

@ -0,0 +1,106 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" aria-hidden="true" data-dismiss="modal">&times;</button>
{{#if id}}
<h3>Edit - {{implementationName}}</h3>
{{else}}
<h3>Add - {{implementationName}}</h3>
{{/if}}
</div>
<div class="modal-body indexer-modal">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">Name</label>
<div class="col-sm-5">
<input type="text" name="name" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Enable Automatic Sync</label>
<div class="col-sm-5">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="enableAuto" {{#if enableAuto}} checked="checked" {{/if}} />
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"></div>
</label>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-warning" title="" data-original-title="New movies found by this list are automatically added to your collection."></i>
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Add Movies Monitored</label>
<div class="col-sm-5">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="shouldMonitor" {{#if shouldMonitor}} checked="checked" {{/if}} />
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"></div>
</label>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-info" title="" data-original-title="If enabled, movies found by this list are added and monitored."></i>
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Quality Profile</label>
<div class="col-sm-5">
{{> ProfileSelectionPartial profiles}}
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Folder</label>
<div class="col-sm-5">
{{> RootFolderSelectionPartial rootFolders}}
</div>
</div>
{{formBuilder}}
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">Delete</button>
{{else}}
<button class="btn pull-left x-back">Back</button>
{{/if}}
<span class="indicator x-indicator"><i class="icon-sonarr-spinner fa-spin"></i></span>
<button class="btn x-test">test <i class="x-test-icon icon-sonarr-test"/></button>
<button class="btn" data-dismiss="modal">Cancel</button>
<div class="btn-group">
<button class="btn btn-primary x-save">Save</button>
<button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li class="save-and-add x-save-and-add">
save and add
</li>
</ul>
</div>
</div>
</div>

@ -0,0 +1,11 @@
<select class="col-md-4 form-control x-list-selection" validation-name="ListSelection">
<option value="0">All</option>
{{#if this}}
{{#each this}}
<option value="{{id}}">{{name}}</option>
{{/each}}
{{else}}
<option value="">Select List</option>
{{/if}}
<option value="addNew">Add a new list</option>
</select>

@ -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;
}
});

@ -0,0 +1,16 @@
<fieldset>
<legend>Lists</legend>
<div class="row">
<div class="col-md-12">
<ul class="list-list thingies">
<li>
<div class="list-item thingy add-card x-add-card">
<span class="center well">
<i class="icon-sonarr-add"/>
</span>
</div>
</li>
</ul>
</div>
</div>
</fieldset>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save