Merge pull request #1025 from tidusjar/EAP

Release 2.1
pull/1033/head v2.1.0
Jamie 8 years ago committed by GitHub
commit 2e8736de46

@ -0,0 +1,38 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: IDiscordApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
using Ombi.Api.Models.Notifications;
namespace Ombi.Api.Interfaces
{
public interface IDiscordApi
{
void SendMessage(string message, string webhookId, string webhookToken, string username = null);
Task SendMessageAsync(string message, string webhookId, string webhookToken, string username = null);
}
}

@ -31,6 +31,6 @@ namespace Ombi.Api.Interfaces
{
public interface INetflixApi
{
NetflixMovieResult GetMovies(string movieName, string year = null);
NetflixMovieResult CheckNetflix(string title, string year = null);
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Ombi.Api.Models.Radarr;
using Ombi.Api.Models.Sonarr;
namespace Ombi.Api.Interfaces
{
public interface IRadarrApi
{
RadarrAddMovie AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, Uri baseUrl, bool searchNow = false);
List<RadarrMovieResponse> GetMovies(string apiKey, Uri baseUrl);
List<SonarrProfile> GetProfiles(string apiKey, Uri baseUrl);
SystemStatus SystemStatus(string apiKey, Uri baseUrl);
}
}

@ -40,10 +40,11 @@ namespace Ombi.Api.Interfaces
bool searchForMissingEpisodes = false);
SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath,
int[] seasons, string apiKey, Uri baseUrl, bool monitor = true,
int[] seasons, string apiKey, Uri baseUrl, bool monitor = true,
bool searchForMissingEpisodes = false);
SystemStatus SystemStatus(string apiKey, Uri baseUrl);
List<SonarrRootFolder> GetRootFolders(string apiKey, Uri baseUrl);
List<Series> GetSeries(string apiKey, Uri baseUrl);
Series GetSeries(string seriesId, string apiKey, Uri baseUrl);

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using TraktApiSharp.Enums;
using TraktApiSharp.Objects.Get.Shows;
using TraktApiSharp.Objects.Get.Shows.Common;
namespace Ombi.Api.Interfaces
{
public interface ITraktApi
{
Task<IEnumerable<TraktMostAnticipatedShow>> GetAnticipatedShows(int? page = default(int?), int? limitPerPage = default(int?));
Task<IEnumerable<TraktMostWatchedShow>> GetMostWatchesShows(TraktTimePeriod period = null, int? page = default(int?), int? limitPerPage = default(int?));
Task<IEnumerable<TraktShow>> GetPopularShows(int? page = default(int?), int? limitPerPage = default(int?));
Task<IEnumerable<TraktTrendingShow>> GetTrendingShows(int? page = default(int?), int? limitPerPage = default(int?));
}
}

@ -31,6 +31,10 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll</HintPath>
<Private>True</Private>
@ -43,19 +47,26 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="TraktApiSharp, Version=0.8.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="IApiRequest.cs" />
<Compile Include="ICouchPotatoApi.cs" />
<Compile Include="IDiscordApi.cs" />
<Compile Include="IHeadphonesApi.cs" />
<Compile Include="IMusicBrainzApi.cs" />
<Compile Include="INetflixApi.cs" />
<Compile Include="IPlexApi.cs" />
<Compile Include="IPushbulletApi.cs" />
<Compile Include="IRadarrApi.cs" />
<Compile Include="ISlackApi.cs" />
<Compile Include="IPushoverApi.cs" />
<Compile Include="ISickRageApi.cs" />
<Compile Include="ISonarrApi.cs" />
<Compile Include="ITraktApi.cs" />
<Compile Include="IWatcherApi.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net45" />
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
<package id="TraktApiSharp" version="0.8.0" targetFramework="net45" />
</packages>

@ -0,0 +1,104 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: TmdbMovieDetails.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
namespace Ombi.Api.Models.Movie
{
public class Genre
{
public int id { get; set; }
public string name { get; set; }
}
public class ProductionCompany
{
public string name { get; set; }
public int id { get; set; }
}
public class ProductionCountry
{
public string iso_3166_1 { get; set; }
public string name { get; set; }
}
public class SpokenLanguage
{
public string iso_639_1 { get; set; }
public string name { get; set; }
}
public class Result
{
public string id { get; set; }
public string iso_639_1 { get; set; }
public string iso_3166_1 { get; set; }
public string key { get; set; }
public string name { get; set; }
public string site { get; set; }
public int size { get; set; }
public string type { get; set; }
}
public class Videos
{
public List<Result> results { get; set; }
}
public class TmdbMovieDetails
{
public bool adult { get; set; }
public string backdrop_path { get; set; }
public object belongs_to_collection { get; set; }
public int budget { get; set; }
public List<Genre> genres { get; set; }
public string homepage { get; set; }
public int id { get; set; }
public string imdb_id { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public string overview { get; set; }
public double popularity { get; set; }
public string poster_path { get; set; }
public List<ProductionCompany> production_companies { get; set; }
public List<ProductionCountry> production_countries { get; set; }
public string release_date { get; set; }
public int revenue { get; set; }
public int runtime { get; set; }
public List<SpokenLanguage> spoken_languages { get; set; }
public string status { get; set; }
public string tagline { get; set; }
public string title { get; set; }
public bool video { get; set; }
public double vote_average { get; set; }
public int vote_count { get; set; }
public Videos videos { get; set; }
}
}

@ -58,5 +58,12 @@ namespace Ombi.Api.Models.Netflix
public string Mediatype { get; set; }
[JsonProperty(PropertyName = "runtime")]
public string Runtime { get; set; }
// For errors
[JsonProperty(PropertyName = "errorcode")]
public int ErrorCode { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
}
}

@ -0,0 +1,34 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: DiscordWebhookRequest.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.Api.Models.Notifications
{
public class DiscordWebhookRequest
{
public string content { get; set; }
public string username { get; set; }
}
}

@ -0,0 +1,47 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: DiscordWebhookResponse.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Ombi.Api.Models.Notifications
{
public class DiscordWebhookResponse
{
public string name { get; set; }
[JsonProperty(PropertyName = "channel_id")]
public string channelid { get; set; }
public string token { get; set; }
public string avatar { get; set; }
[JsonProperty(PropertyName = "guild_id")]
public string guildid { get; set; }
public string id { get; set; }
}
}

@ -54,6 +54,7 @@
<Compile Include="Movie\CouchPotatoProfiles.cs" />
<Compile Include="Movie\CouchPotatoStatus.cs" />
<Compile Include="Movie\CouchPotatoApiKey.cs" />
<Compile Include="Movie\TmdbMovieDetails.cs" />
<Compile Include="Music\HeadphonesAlbumSearchResult.cs" />
<Compile Include="Music\HeadphonesArtistSearchResult.cs" />
<Compile Include="Music\HeadphonesGetIndex.cs" />
@ -62,6 +63,8 @@
<Compile Include="Music\MusicBrainzReleaseInfo.cs" />
<Compile Include="Music\MusicBrainzSearchResults.cs" />
<Compile Include="Netflix\NetflixMovieResult.cs" />
<Compile Include="Notifications\DiscordWebhookRequest.cs" />
<Compile Include="Notifications\DiscordWebhookResponse.cs" />
<Compile Include="Notifications\PushbulletPush.cs" />
<Compile Include="Notifications\PushbulletResponse.cs" />
<Compile Include="Notifications\PushoverResponse.cs" />
@ -80,6 +83,10 @@
<Compile Include="Plex\PlexUserRequest.cs" />
<Compile Include="Plex\RecentlyAddedModelOld.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Radarr\RadarrAddMovie.cs" />
<Compile Include="Radarr\RadarrAddOptions.cs" />
<Compile Include="Radarr\RadarrError.cs" />
<Compile Include="Radarr\RadarrMovieResponse.cs" />
<Compile Include="SickRage\SickRageBase.cs" />
<Compile Include="SickRage\SickrageShows.cs" />
<Compile Include="SickRage\SickRagePing.cs" />
@ -95,6 +102,7 @@
<Compile Include="Sonarr\SonarrEpisodes.cs" />
<Compile Include="Sonarr\SonarrError.cs" />
<Compile Include="Sonarr\SonarrProfile.cs" />
<Compile Include="Sonarr\SonarrRootFolder.cs" />
<Compile Include="Sonarr\SonarrSearchCommand.cs" />
<Compile Include="Sonarr\SonarrSeasonSearchResult.cs" />
<Compile Include="Sonarr\SonarrSeriesSearchResult.cs" />

@ -0,0 +1,56 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: RadarrAddMovie.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using Newtonsoft.Json;
using Ombi.Api.Models.Sonarr;
namespace Ombi.Api.Models.Radarr
{
public class RadarrAddMovie
{
public RadarrAddMovie()
{
images = new List<string>();
}
public RadarrError Error { get; set; }
public RadarrAddOptions addOptions { get; set; }
public string title { get; set; }
public string rootFolderPath { get; set; }
public int qualityProfileId { get; set; }
public bool monitored { get; set; }
public int tmdbId { get; set; }
public List<string> images { get; set; }
public string cleanTitle { get; set; }
public string imdbId { get; set; }
public string titleSlug { get; set; }
public int id { get; set; }
public int year { get; set; }
}
}

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: RadarrAddOptions.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.Api.Models.Radarr
{
public class RadarrAddOptions
{
public bool ignoreEpisodesWithFiles { get; set; }
public bool ignoreEpisodesWithoutFiles { get; set; }
public bool searchForMovie { get; set; }
}
}

@ -0,0 +1,34 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: RadarrError.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.Api.Models.Radarr
{
public class RadarrError
{
public string message { get; set; }
public string description { get; set; }
}
}

@ -0,0 +1,80 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: RadarrMovieResponse.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
namespace Ombi.Api.Models.Radarr
{
public class Image
{
public string coverType { get; set; }
public string url { get; set; }
}
public class Ratings
{
public int votes { get; set; }
public double value { get; set; }
}
public class RadarrMovieResponse
{
public string title { get; set; }
public string sortTitle { get; set; }
public double sizeOnDisk { get; set; }
public string status { get; set; }
public string overview { get; set; }
public string inCinemas { get; set; }
public string physicalRelease { get; set; }
public List<Image> images { get; set; }
public string website { get; set; }
public bool downloaded { get; set; }
public int year { get; set; }
public bool hasFile { get; set; }
public string youTubeTrailerId { get; set; }
public string studio { get; set; }
public string path { get; set; }
public int profileId { get; set; }
public bool monitored { get; set; }
public int runtime { get; set; }
public string lastInfoSync { get; set; }
public string cleanTitle { get; set; }
public string imdbId { get; set; }
public int tmdbId { get; set; }
public string titleSlug { get; set; }
public List<string> genres { get; set; }
public List<object> tags { get; set; }
public string added { get; set; }
public Ratings ratings { get; set; }
public List<string> alternativeTitles { get; set; }
public int qualityProfileId { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: SonarrRootFolder.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.Api.Models.Sonarr
{
public class SonarrRootFolder
{
public int id { get; set; }
public string path { get; set; }
public long freespace { get; set; }
}
}

@ -70,7 +70,7 @@ namespace Ombi.Api
return response.Data;
}
public IRestResponse Execute(IRestRequest request, Uri baseUri)
{
var client = new RestClient { BaseUrl = baseUri };

@ -0,0 +1,115 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: NetflixRouletteApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Interfaces;
using Ombi.Api.Models.Netflix;
using Ombi.Api.Models.Notifications;
using RestSharp;
namespace Ombi.Api
{
public class DiscordApi : IDiscordApi
{
public DiscordApi(IApiRequest req)
{
Api = req;
}
private IApiRequest Api { get; }
private Uri Endpoint => new Uri("https://discordapp.com/api/"); //webhooks/270828242636636161/lLysOMhJ96AFO1kvev0bSqP-WCZxKUh1UwfubhIcLkpS0DtM3cg4Pgeraw3waoTXbZii
public void SendMessage(string message, string webhookId, string webhookToken, string username = null)
{
var request = new RestRequest
{
Resource = "webhooks/{webhookId}/{webhookToken}",
Method = Method.POST
};
request.AddUrlSegment("webhookId", webhookId);
request.AddUrlSegment("webhookToken", webhookToken);
var body = new DiscordWebhookRequest
{
content = message,
username = username
};
request.AddJsonBody(body);
request.AddHeader("Content-Type", "application/json");
Api.Execute(request, Endpoint);
}
public async Task SendMessageAsync(string message, string webhookId, string webhookToken, string username = null)
{
var request = new RestRequest
{
Resource = "webhooks/{webhookId}/{webhookToken}",
Method = Method.POST
};
request.AddUrlSegment("webhookId", webhookId);
request.AddUrlSegment("webhookToken", webhookToken);
var body = new DiscordWebhookRequest
{
content = message,
username = username
};
request.AddJsonBody(body);
request.AddHeader("Content-Type", "application/json");
await Task.Run(
() =>
{
Api.Execute(request, Endpoint);
});
}
public NetflixMovieResult CheckNetflix(string title, string year = null)
{
var request = new RestRequest();
request.AddQueryParameter("title", title);
if (!string.IsNullOrEmpty(year))
{
request.AddQueryParameter("year", year);
}
var result = Api.Execute(request, Endpoint);
return JsonConvert.DeserializeObject<NetflixMovieResult>(result.Content);
}
}
}

@ -32,6 +32,8 @@ namespace Ombi.Api
public abstract class MovieBase
{
private static readonly string Encrypted = "0T3QNSseexLO7n7UPiJvl70Y+KKnvbeTlsusl7Kwq0hPH0BHOuFNGwksNCjkwqWedyDdI/MJeUR4wtL4bIl0Z+//uHXEaYM/4H2pjeLbH5EWdUe5TTj1AhaIR5PQweamvcienRyFD/3YPCC/+qL5mHkKXBkPumMod3Zb/4yN0Ik=";
protected string ApiKey = StringCipher.Decrypt(Encrypted, "ApiKey");
private string _apiKey;
protected string ApiKey => _apiKey ?? (_apiKey = StringCipher.Decrypt(Encrypted, "ApiKey"));
}
}

@ -26,6 +26,7 @@
#endregion
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Interfaces;
using Ombi.Api.Models.Netflix;
@ -43,10 +44,10 @@ namespace Ombi.Api
private IApiRequest Api { get; }
private Uri Endpoint => new Uri("http://netflixroulette.net/api/api.php");
public NetflixMovieResult GetMovies(string movieName, string year = null)
public NetflixMovieResult CheckNetflix(string title, string year = null)
{
var request = new RestRequest();
request.AddQueryParameter("title", movieName);
request.AddQueryParameter("title", title);
if (!string.IsNullOrEmpty(year))
{
request.AddQueryParameter("year", year);

@ -66,10 +66,17 @@
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="TraktApiSharp, Version=0.8.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ApiRequest.cs" />
<Compile Include="DiscordApi.cs" />
<Compile Include="NetflixRouletteApi.cs" />
<Compile Include="RadarrApi.cs" />
<Compile Include="TraktApi.cs" />
<Compile Include="WatcherApi.cs" />
<Compile Include="MusicBrainzApi.cs" />
<Compile Include="SlackApi.cs" />

@ -0,0 +1,158 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CouchPotatoApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NLog;
using Ombi.Api.Interfaces;
using Ombi.Api.Models.Radarr;
using Ombi.Api.Models.Sonarr;
using Ombi.Helpers;
using RestSharp;
namespace Ombi.Api
{
public class RadarrApi : IRadarrApi
{
public RadarrApi()
{
Api = new ApiRequest();
}
private ApiRequest Api { get; set; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public List<SonarrProfile> GetProfiles(string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/profile", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetProfiles for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var obj = policy.Execute(() => Api.ExecuteJson<List<SonarrProfile>>(request, baseUrl));
return obj;
}
public RadarrAddMovie AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, Uri baseUrl, bool searchNow = false)
{
var request = new RestRequest
{
Resource = "/api/movie",
Method = Method.POST
};
var options = new RadarrAddMovie
{
title = title,
tmdbId = tmdbId,
qualityProfileId = qualityId,
rootFolderPath = rootPath,
titleSlug = title,
monitored = true,
year = year
};
if (searchNow)
{
options.addOptions = new RadarrAddOptions
{
searchForMovie = true
};
}
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options);
RadarrAddMovie result;
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (2)
});
var response = policy.Execute(() => Api.Execute(request, baseUrl));
if (response.Content.Contains("\"message\":"))
{
var error = JsonConvert.DeserializeObject < RadarrError>(response.Content);
return new RadarrAddMovie {Error = error};
}
if (response.Content.Contains("\"errorMessage\":"))
{
var error = JsonConvert.DeserializeObject<List<SonarrError>>(response.Content).FirstOrDefault();
return new RadarrAddMovie {Error = new RadarrError {message = error?.errorMessage}};
}
return JsonConvert.DeserializeObject < RadarrAddMovie>(response.Content);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
}
return null;
}
public SystemStatus SystemStatus(string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var obj = policy.Execute(() => Api.ExecuteJson<SystemStatus>(request, baseUrl));
return obj;
}
public List<RadarrMovieResponse> GetMovies(string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/movie", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var obj = policy.Execute(() => Api.Execute(request, baseUrl));
return JsonConvert.DeserializeObject<List<RadarrMovieResponse>>(obj.Content);
}
}
}

@ -62,6 +62,22 @@ namespace Ombi.Api
return obj;
}
public List<SonarrRootFolder> GetRootFolders(string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/rootfolder", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetRootFolders for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var obj = policy.Execute(() => Api.ExecuteJson<List<SonarrRootFolder>>(request, baseUrl));
return obj;
}
public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, bool searchForMissingEpisodes = false)
{
Log.Debug("Adding series {0}", title);

@ -25,12 +25,18 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NLog;
using NLog.Fluent;
using Ombi.Api.Models.Movie;
using RestSharp;
using TMDbLib.Client;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Movies;
using TMDbLib.Objects.Search;
using Movie = TMDbLib.Objects.Movies.Movie;
namespace Ombi.Api
{
@ -39,9 +45,13 @@ namespace Ombi.Api
public TheMovieDbApi()
{
Client = new TMDbClient(ApiKey);
Api = new ApiRequest();
}
private ApiRequest Api { get; }
public TMDbClient Client { get; set; }
private const string BaseUrl = "https://api.themoviedb.org/3/";
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task<List<SearchMovie>> SearchMovie(string searchTerm)
{
var results = await Client.SearchMovie(searchTerm);
@ -56,7 +66,27 @@ namespace Ombi.Api
public async Task<List<MovieResult>> GetUpcomingMovies()
{
var movies = await Client.GetMovieList(MovieListType.Upcoming);
return movies?.Results ?? new List<MovieResult>();
return movies?.Results ?? new List<MovieResult>();
}
public TmdbMovieDetails GetMovieInformationWithVideos(int tmdbId)
{
var request = new RestRequest { Resource = "movie/{movieId}", Method = Method.GET };
request.AddUrlSegment("movieId", tmdbId.ToString());
request.AddQueryParameter("api_key", ApiKey);
request.AddQueryParameter("append_to_response", "videos"); // Get the videos
try
{
var obj = Api.ExecuteJson<TmdbMovieDetails>(request, new Uri(BaseUrl));
return obj;
}
catch (Exception e)
{
Log.Error(e);
return null;
}
}
public async Task<Movie> GetMovieInformation(int tmdbId)

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ombi.Api.Interfaces;
using Ombi.Helpers;
using TraktApiSharp;
using TraktApiSharp.Enums;
using TraktApiSharp.Objects.Get.Shows;
using TraktApiSharp.Objects.Get.Shows.Common;
using TraktApiSharp.Requests.Params;
namespace Ombi.Api
{
public class TraktApi : ITraktApi
{
private TraktClient Client { get; }
private static readonly string Encrypted = "z/56wM/oEkkCWEvSIZCrzQyUvvqmafQ3njqf0UNK5xuKbNYh5Wz8ocoG2QDa5y1DBkozLaKsGxORmAB1XUvwbnom8DVNo9gE++9GTuwxmGlLDD318PXpRmYmpKqNwFSKRZgF6ewiY9qR4t3iG0pGQwPA08FK3+H7kpOKAGJNR9RMDP9wwB6Vl4DuOiZb9/DETjzZ+/zId0ZqimrbN+PLrg==";
private readonly string _apiKey = StringCipher.Decrypt(Encrypted, "ApiKey");
public TraktApi()
{
Client = new TraktClient(_apiKey);
}
public async Task<IEnumerable<TraktShow>> GetPopularShows(int? page = null, int? limitPerPage = null)
{
var popular = await Client.Shows.GetPopularShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10);
return popular.Items;
}
public async Task<IEnumerable<TraktTrendingShow>> GetTrendingShows(int? page = null, int? limitPerPage = null)
{
var trendingShowsTop10 = await Client.Shows.GetTrendingShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10);
return trendingShowsTop10.Items;
}
public async Task<IEnumerable<TraktMostAnticipatedShow>> GetAnticipatedShows(int? page = null, int? limitPerPage = null)
{
var anticipatedShows = await Client.Shows.GetMostAnticipatedShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10);
return anticipatedShows.Items;
}
public async Task<IEnumerable<TraktMostWatchedShow>> GetMostWatchesShows(TraktTimePeriod period = null, int? page = null, int? limitPerPage = null)
{
var anticipatedShows = await Client.Shows.GetMostWatchedShowsAsync(period ?? TraktTimePeriod.Monthly, new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10);
return anticipatedShows.Items;
}
}
}

@ -8,4 +8,5 @@
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
<package id="System.Net.Http" version="4.0.0" targetFramework="net45" />
<package id="TMDbLib" version="0.9.0.0-alpha" targetFramework="net45" />
<package id="TraktApiSharp" version="0.8.0" targetFramework="net45" />
</packages>

@ -0,0 +1,64 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Version1100.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Data;
using NLog;
using Ombi.Core.SettingModels;
namespace Ombi.Core.Migration.Migrations
{
[Migration(22000, "v2.20.0.0")]
public class Version2200 : BaseMigration, IMigration
{
public Version2200(ISettingsService<CustomizationSettings> custom)
{
Customization = custom;
}
public int Version => 22000;
private ISettingsService<CustomizationSettings> Customization { get; set; }
private static Logger Logger = LogManager.GetCurrentClassLogger();
public void Start(IDbConnection con)
{
//UpdateCustomSettings(); Turned off the migration for now until the search has been improved on.
//UpdateSchema(con, Version);
}
private void UpdateCustomSettings()
{
var settings = Customization.GetSettings();
settings.NewSearch = true; // Use the new search
Customization.SaveSettings(settings);
}
}
}

@ -69,6 +69,7 @@
<Compile Include="MigrationAttribute.cs" />
<Compile Include="MigrationRunner.cs" />
<Compile Include="Migrations\BaseMigration.cs" />
<Compile Include="Migrations\Version2200.cs" />
<Compile Include="Migrations\Version1100.cs" />
<Compile Include="Migrations\Version195.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

@ -37,7 +37,9 @@ namespace Ombi.Core
public const string PlexEpisodes = nameof(PlexEpisodes);
public const string TvDbToken = nameof(TvDbToken);
public const string SonarrQualityProfiles = nameof(SonarrQualityProfiles);
public const string RadarrQualityProfiles = nameof(RadarrQualityProfiles);
public const string SonarrQueued = nameof(SonarrQueued);
public const string RadarrMovies = nameof(RadarrMovies);
public const string SickRageQualityProfiles = nameof(SickRageQualityProfiles);
public const string SickRageQueued = nameof(SickRageQueued);
public const string CouchPotatoQualityProfiles = nameof(CouchPotatoQualityProfiles);
@ -45,5 +47,6 @@ namespace Ombi.Core
public const string WatcherQueued = nameof(WatcherQueued);
public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings);
public const string LastestProductVersion = nameof(LastestProductVersion);
public const string SonarrRootFolders = nameof(SonarrRootFolders);
}
}

@ -29,7 +29,10 @@ namespace Ombi.Core
/// Gets the username this could be the alias! We should always use this method when getting the username
/// </summary>
/// <param name="username">The username.</param>
/// <returns><c>null</c> if we cannot find a user</returns>
/// <param name="session">The session.</param>
/// <returns>
/// <c>null</c> if we cannot find a user
/// </returns>
string GetUsername(string username, ISession session);
}
}

@ -37,16 +37,20 @@ namespace Ombi.Core
public class MovieSender : IMovieSender
{
public MovieSender(ISettingsService<CouchPotatoSettings> cp, ISettingsService<WatcherSettings> watcher,
ICouchPotatoApi cpApi, IWatcherApi watcherApi)
ICouchPotatoApi cpApi, IWatcherApi watcherApi, IRadarrApi radarrApi, ISettingsService<RadarrSettings> radarrSettings)
{
CouchPotatoSettings = cp;
WatcherSettings = watcher;
CpApi = cpApi;
WatcherApi = watcherApi;
RadarrSettings = radarrSettings;
RadarrApi = radarrApi;
}
private ISettingsService<CouchPotatoSettings> CouchPotatoSettings { get; }
private ISettingsService<WatcherSettings> WatcherSettings { get; }
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IRadarrApi RadarrApi { get; }
private ICouchPotatoApi CpApi { get; }
private IWatcherApi WatcherApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
@ -55,6 +59,7 @@ namespace Ombi.Core
{
var cpSettings = await CouchPotatoSettings.GetSettingsAsync();
var watcherSettings = await WatcherSettings.GetSettingsAsync();
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (cpSettings.Enabled)
{
@ -66,6 +71,11 @@ namespace Ombi.Core
return SendToWatcher(model, watcherSettings);
}
if (radarrSettings.Enabled)
{
return SendToRadarr(model, radarrSettings);
}
return new MovieSenderResult { Result = false, MovieSendingEnabled = false };
}
@ -91,5 +101,23 @@ namespace Ombi.Core
var result = CpApi.AddMovie(model.ImdbId, settings.ApiKey, model.Title, settings.FullUri, qualityId);
return new MovieSenderResult { Result = result, MovieSendingEnabled = true };
}
private MovieSenderResult SendToRadarr(RequestedModel model, RadarrSettings settings)
{
var qualityProfile = 0;
int.TryParse(settings.QualityProfile, out qualityProfile);
var result = RadarrApi.AddMovie(model.ProviderId, model.Title, model.ReleaseDate.Year, qualityProfile, settings.RootPath, settings.ApiKey, settings.FullUri, true);
if (!string.IsNullOrEmpty(result.Error?.message))
{
Log.Error(result.Error.message);
return new MovieSenderResult { Result = false, Error = true};
}
if (!string.IsNullOrEmpty(result.title))
{
return new MovieSenderResult { Result = true, MovieSendingEnabled = true };
}
return new MovieSenderResult { Result = false, MovieSendingEnabled = true };
}
}
}

@ -36,5 +36,7 @@ namespace Ombi.Core
/// <c>true</c> if [movie sending enabled]; otherwise, <c>false</c>.
/// </value>
public bool MovieSendingEnabled { get; set; }
public bool Error { get; set; }
}
}

@ -122,6 +122,8 @@
<Compile Include="Queue\TransientFaultQueue.cs" />
<Compile Include="SecurityExtensions.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\DiscordNotificationSettings.cs" />
<Compile Include="SettingModels\RadarrSettings.cs" />
<Compile Include="SettingModels\WatcherSettings.cs" />
<Compile Include="SettingModels\ExternalSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />

@ -97,7 +97,7 @@ namespace Ombi.Core.Queue
Content = ByteConverterHelper.ReturnBytes(request),
PrimaryIdentifier = id,
FaultType = faultType,
Message = description ?? string.Empty
Description = description ?? string.Empty
};
await RequestQueue.InsertAsync(queue);
}

@ -94,40 +94,33 @@ namespace Ombi.Core
/// Gets the username this could be the alias! We should always use this method when getting the username
/// </summary>
/// <param name="username">The username.</param>
/// <returns><c>null</c> if we cannot find a user</returns>
/// <param name="session"></param>
/// <returns>
/// <c>null</c> if we cannot find a user
/// </returns>
public string GetUsername(string username, ISession session)
{
var plexUser = PlexUsers.GetUserByUsername(username);
if (plexUser != null)
{
if (!string.IsNullOrEmpty(plexUser.UserAlias))
{
return plexUser.UserAlias;
}
else
{
return plexUser.Username;
}
return !string.IsNullOrEmpty(plexUser.UserAlias) ? plexUser.UserAlias : plexUser.Username;
}
var dbUser = UserRepository.GetUserByUsername(username);
if (dbUser != null)
{
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(dbUser.UserProperties);
if (!string.IsNullOrEmpty(userProps.UserAlias))
{
return userProps.UserAlias;
}
else
{
return dbUser.UserName;
}
return !string.IsNullOrEmpty(userProps.UserAlias) ? userProps.UserAlias : dbUser.UserName;
}
// could be a local user
var localName = session[SessionKeys.UsernameKey];
var hasSessionKey = session[SessionKeys.UsernameKey] != null;
if (hasSessionKey)
{
return (string)session[SessionKeys.UsernameKey];
}
return localName as string;
return string.Empty;
}

@ -53,5 +53,7 @@ namespace Ombi.Core.SettingModels
public int DefaultLang { get; set; }
public bool NewSearch { get; set; }
}
}

@ -0,0 +1,31 @@
using System;
using Newtonsoft.Json;
namespace Ombi.Core.SettingModels
{
public sealed class DiscordNotificationSettings : NotificationSettings
{
public string WebhookUrl { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string WebookId => SplitWebUrl(4);
[JsonIgnore]
public string Token => SplitWebUrl(5);
private string SplitWebUrl(int index)
{
if (!WebhookUrl.StartsWith("http", StringComparison.InvariantCulture))
{
WebhookUrl = "https://" + WebhookUrl;
}
var split = WebhookUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
return split.Length < index
? string.Empty
: split[index];
}
}
}

@ -34,7 +34,6 @@ namespace Ombi.Core.SettingModels
public string EmailSender { get; set; }
public string EmailUsername { get; set; }
public bool Authentication { get; set; }
public bool EnableUserEmailNotifications { get; set; }
public string RecipientEmail { get; set; }
}
}

@ -0,0 +1,37 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SonarrSettings.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.Core.SettingModels
{
public sealed class RadarrSettings : ExternalSettings
{
public bool Enabled { get; set; }
public string ApiKey { get; set; }
public string QualityProfile { get; set; }
public string RootPath { get; set; }
}
}

@ -46,5 +46,6 @@ namespace Ombi.Core.SettingModels
public int FaultQueueHandler { get; set; }
public int PlexContentCacher { get; set; }
public int PlexUserChecker { get; set; }
public int RadarrCacher { get; set; }
}
}

@ -32,7 +32,13 @@ namespace Ombi.Core.SettingModels
public string ApiKey { get; set; }
public string QualityProfile { get; set; }
public bool SeasonFolders { get; set; }
/// <summary>
/// This is the root path ID
/// </summary>
/// <value>
/// The root path.
/// </value>
public string RootPath { get; set; }
public string FullRootPath { get; set; }
}
}

@ -34,19 +34,22 @@ using Ombi.Api.Interfaces;
using Ombi.Api.Models.SickRage;
using Ombi.Api.Models.Sonarr;
using Ombi.Core.SettingModels;
using Ombi.Helpers;
using Ombi.Store;
namespace Ombi.Core
{
public class TvSender
{
public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi)
public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi, ICacheProvider cache)
{
SonarrApi = sonarrApi;
SickrageApi = srApi;
Cache = cache;
}
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickrageApi { get; }
private ICacheProvider Cache { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model)
@ -82,6 +85,8 @@ namespace Ombi.Core
var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase);
var specificSeasonRequest = model.SeasonList?.Any();
var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetRootPath(model.RootFolderSelected, sonarrSettings);
if (episodeRequest)
{
// Does series exist?
@ -96,7 +101,7 @@ namespace Ombi.Core
// Series doesn't exist, need to add it as unmonitored.
var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.SeasonFolders, rootFolderPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.FullUri, false));
@ -125,7 +130,7 @@ namespace Ombi.Core
{
// Set the series as monitored with a season count as 0 so it doesn't search for anything
SonarrApi.AddSeriesNew(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey,
sonarrSettings.SeasonFolders, rootFolderPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
await Task.Delay(TimeSpan.FromSeconds(1));
@ -372,5 +377,20 @@ namespace Ombi.Core
return selectedSeries;
}
private async Task<string> GetRootPath(int pathId, SonarrSettings sonarrSettings)
{
var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () =>
{
return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
foreach (var r in rootFoldersResult.Where(r => r.id == pathId))
{
return r.path;
}
return string.Empty;
}
}
}

@ -34,19 +34,22 @@ using Ombi.Api.Interfaces;
using Ombi.Api.Models.SickRage;
using Ombi.Api.Models.Sonarr;
using Ombi.Core.SettingModels;
using Ombi.Helpers;
using Ombi.Store;
namespace Ombi.Core
{
public class TvSenderOld
{
public TvSenderOld(ISonarrApi sonarrApi, ISickRageApi srApi)
public TvSenderOld(ISonarrApi sonarrApi, ISickRageApi srApi, ICacheProvider cache)
{
SonarrApi = sonarrApi;
SickrageApi = srApi;
Cache = cache;
}
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickrageApi { get; }
private ICacheProvider Cache { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model)
@ -67,6 +70,8 @@ namespace Ombi.Core
{
int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
}
var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetRootPath(model.RootFolderSelected, sonarrSettings);
var series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
@ -84,7 +89,7 @@ namespace Ombi.Core
// Series doesn't exist, need to add it as unmonitored.
var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.SeasonFolders, rootFolderPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.FullUri, false));
@ -156,7 +161,7 @@ namespace Ombi.Core
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.SeasonFolders, rootFolderPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri, true, true);
return result;
@ -298,5 +303,20 @@ namespace Ombi.Core
return selectedSeries;
}
private async Task<string> GetRootPath(int pathId, SonarrSettings sonarrSettings)
{
var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () =>
{
return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
foreach (var r in rootFoldersResult.Where(r => r.id == pathId))
{
return r.path;
}
return string.Empty;
}
}
}

@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using Newtonsoft.Json;
using Ombi.Helpers;
using Ombi.Helpers.Permissions;
@ -38,5 +39,8 @@ namespace Ombi.Core.Users
public Features Features { get; set; }
public string EmailAddress { get; set; }
public UserType Type { get; set; }
[JsonIgnore]
public string UsernameOrAlias => string.IsNullOrEmpty(UserAlias) ? Username : UserAlias;
}
}

@ -46,5 +46,12 @@ namespace Ombi.Helpers
dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
return dtDateTime;
}
public static long ToJavascriptTimestamp(this DateTime input)
{
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var time = input.Subtract(new TimeSpan(epoch.Ticks));
return (long)(time.Ticks / 10000);
}
}
}

@ -34,6 +34,7 @@ namespace Ombi.Services.Interfaces
{
public interface IAvailabilityChecker
{
void Start();
void CheckAndUpdateAll();
IEnumerable<PlexContent> GetPlexMovies(IEnumerable<PlexContent> content);
bool IsMovieAvailable(PlexContent[] plexMovies, string title, string year, string providerId = null);

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Ombi.Services.Models;
namespace Ombi.Services.Interfaces
{
public interface IRadarrCacher
{
void Queued();
int[] QueuedIds();
}
}

@ -6,5 +6,6 @@ namespace Ombi.Services.Jobs
{
void Execute(IJobExecutionContext context);
void Test();
void Start();
}
}

@ -0,0 +1,10 @@
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IStoreBackup
{
void Start();
void Execute(IJobExecutionContext context);
}
}

@ -0,0 +1,10 @@
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IStoreCleanup
{
void Execute(IJobExecutionContext context);
void Start();
}
}

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Ombi.Core.SettingModels;
using Ombi.Store.Models;
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IUserRequestLimitResetter
{
void AlbumLimit(PlexRequestSettings s, IEnumerable<RequestLimit> allUsers);
void Execute(IJobExecutionContext context);
void MovieLimit(PlexRequestSettings s, IEnumerable<RequestLimit> allUsers);
void Start();
void TvLimit(PlexRequestSettings s, IEnumerable<RequestLimit> allUsers);
}
}

@ -45,7 +45,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class FaultQueueHandler : IJob
public class FaultQueueHandler : IJob, IFaultQueueHandler
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -53,7 +53,7 @@ namespace Ombi.Services.Jobs
ISickRageApi srApi, ISettingsService<SonarrSettings> sonarrSettings, ISettingsService<SickRageSettings> srSettings,
ICouchPotatoApi cpApi, ISettingsService<CouchPotatoSettings> cpsettings, IRequestService requestService,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi headphonesApi, ISettingsService<PlexRequestSettings> prSettings,
ISecurityExtensions security, IMovieSender movieSender)
ISecurityExtensions security, IMovieSender movieSender, ICacheProvider cache)
{
Record = record;
Repo = repo;
@ -71,6 +71,8 @@ namespace Ombi.Services.Jobs
Security = security;
PrSettings = prSettings.GetSettings();
MovieSender = movieSender;
Cache = cache;
}
private IMovieSender MovieSender { get; }
@ -78,6 +80,7 @@ namespace Ombi.Services.Jobs
private IJobRecord Record { get; }
private ISonarrApi SonarrApi { get; }
private ISickRageApi SrApi { get; }
private ICacheProvider Cache { get; }
private ICouchPotatoApi CpApi { get; }
private IHeadphonesApi HpApi { get; }
private IRequestService RequestService { get; }
@ -88,9 +91,8 @@ namespace Ombi.Services.Jobs
private ISettingsService<HeadphonesSettings> HeadphoneSettings { get; }
private ISecurityExtensions Security { get; }
public void Execute(IJobExecutionContext context)
public void Start()
{
Record.SetRunning(true, JobNames.CpCacher);
try
{
@ -113,6 +115,11 @@ namespace Ombi.Services.Jobs
Record.SetRunning(false, JobNames.CpCacher);
}
}
public void Execute(IJobExecutionContext context)
{
Start();
}
private void ProcessMissingInformation(List<RequestQueue> requests)
@ -163,7 +170,7 @@ namespace Ombi.Services.Jobs
try
{
var sender = new TvSenderOld(SonarrApi, SrApi);
var sender = new TvSenderOld(SonarrApi, SrApi, Cache);
if (sonarr.Enabled)
{
var task = sender.SendToSonarr(sonarr, tvModel, sonarr.QualityProfile);

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Ombi.Core.SettingModels;
using Ombi.Store;
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IFaultQueueHandler
{
void Execute(IJobExecutionContext context);
bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings, List<string> username);
void Start();
}
}

@ -0,0 +1,12 @@
using Ombi.Core.SettingModels;
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IPlexEpisodeCacher
{
void CacheEpisodes(PlexSettings settings);
void Execute(IJobExecutionContext context);
void Start();
}
}

@ -0,0 +1,10 @@
using Quartz;
namespace Ombi.Services.Jobs
{
public interface IPlexUserChecker
{
void Execute(IJobExecutionContext context);
void Start();
}
}

@ -32,6 +32,7 @@ namespace Ombi.Services.Jobs
public const string CpCacher = "CouchPotato Cacher";
public const string WatcherCacher = "Watcher Cacher";
public const string SonarrCacher = "Sonarr Cacher";
public const string RadarrCacher = "Radarr Cacher";
public const string SrCacher = "SickRage Cacher";
public const string PlexChecker = "Plex Availability Cacher";
public const string PlexCacher = "Plex Cacher";

@ -79,6 +79,7 @@ namespace Ombi.Services.Jobs
public void CheckAndUpdateAll()
{
var plexSettings = Plex.GetSettings();
if (!ValidateSettings(plexSettings))
@ -472,5 +473,23 @@ namespace Ombi.Services.Jobs
Job.SetRunning(false, JobNames.PlexChecker);
}
}
public void Start()
{
Job.SetRunning(true, JobNames.PlexChecker);
try
{
CheckAndUpdateAll();
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
Job.Record(JobNames.PlexChecker);
Job.SetRunning(false, JobNames.PlexChecker);
}
}
}
}

@ -43,7 +43,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class PlexEpisodeCacher : IJob
public class PlexEpisodeCacher : IJob, IPlexEpisodeCacher
{
public PlexEpisodeCacher(ISettingsService<PlexSettings> plexSettings, IPlexApi plex, ICacheProvider cache,
IJobRecord rec, IRepository<PlexEpisodes> repo, ISettingsService<ScheduledJobsSettings> jobs)
@ -140,6 +140,38 @@ namespace Ombi.Services.Jobs
}
}
public void Start()
{
try
{
var s = Plex.GetSettings();
if (!s.EnableTvEpisodeSearching)
{
return;
}
var jobs = Job.GetJobs();
var job = jobs.FirstOrDefault(x => x.Name.Equals(JobNames.EpisodeCacher, StringComparison.CurrentCultureIgnoreCase));
if (job != null)
{
if (job.LastRun > DateTime.Now.AddHours(-11)) // If it's been run in the last 11 hours
{
return;
}
}
Job.SetRunning(true, JobNames.EpisodeCacher);
CacheEpisodes(s);
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
Job.Record(JobNames.EpisodeCacher);
Job.SetRunning(false, JobNames.EpisodeCacher);
}
}
public void Execute(IJobExecutionContext context)
{

@ -42,7 +42,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class PlexUserChecker : IJob
public class PlexUserChecker : IJob, IPlexUserChecker
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -68,7 +68,7 @@ namespace Ombi.Services.Jobs
private IRequestService RequestService { get; }
private IUserRepository LocalUserRepository { get; }
public void Execute(IJobExecutionContext context)
public void Start()
{
JobRecord.SetRunning(true, JobNames.PlexUserChecker);
@ -153,7 +153,7 @@ namespace Ombi.Services.Jobs
}
// Looks like it's a new user!
var m = new PlexUsers
var m = new PlexUsers
{
PlexUserId = user.Id,
Permissions = UserManagementHelper.GetPermissions(userManagementSettings),
@ -170,7 +170,7 @@ namespace Ombi.Services.Jobs
// Main Plex user
var dbMainAcc = dbUsers.FirstOrDefault(x => x.Username.Equals(mainPlexAccount.Username, StringComparison.CurrentCulture));
var localMainAcc = localUsers.FirstOrDefault(x => x.UserName.Equals(mainPlexAccount.Username, StringComparison.CurrentCulture));
// TODO if admin acc does exist, check if we need to update it
@ -188,7 +188,7 @@ namespace Ombi.Services.Jobs
LoginId = Guid.NewGuid().ToString()
};
a.Permissions += (int) Permissions.Administrator; // Make admin
a.Permissions += (int)Permissions.Administrator; // Make admin
Repo.Insert(a);
}
@ -205,5 +205,9 @@ namespace Ombi.Services.Jobs
JobRecord.Record(JobNames.PlexUserChecker);
}
}
public void Execute(IJobExecutionContext context)
{
Start();
}
}
}

@ -0,0 +1,110 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexAvailabilityChecker.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using NLog;
using Ombi.Api.Interfaces;
using Ombi.Api.Models.Radarr;
using Ombi.Core;
using Ombi.Core.SettingModels;
using Ombi.Helpers;
using Ombi.Services.Interfaces;
using Quartz;
namespace Ombi.Services.Jobs
{
public class RadarrCacher : IJob, IRadarrCacher
{
public RadarrCacher(ISettingsService<RadarrSettings> radarrService, IRadarrApi radarrApi, ICacheProvider cache, IJobRecord rec)
{
RadarrSettings = radarrService;
RadarrApi = radarrApi;
Job = rec;
Cache = cache;
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private ICacheProvider Cache { get; }
private IRadarrApi RadarrApi { get; }
private IJobRecord Job { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public void Queued()
{
var settings = RadarrSettings.GetSettings();
if (settings.Enabled)
{
Job.SetRunning(true, JobNames.RadarrCacher);
try
{
var movies = RadarrApi.GetMovies(settings.ApiKey, settings.FullUri);
if (movies != null)
{
var movieIds = new List<int>();
foreach (var m in movies)
{
if (m.tmdbId > 0)
{
movieIds.Add(m.tmdbId);
}
}
//var movieIds = movies.Select(x => x.tmdbId).ToList();
Cache.Set(CacheKeys.RadarrMovies, movieIds, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (System.Exception ex)
{
Log.Error(ex, "Failed caching queued items from Radarr");
}
finally
{
Job.Record(JobNames.RadarrCacher);
Job.SetRunning(false, JobNames.RadarrCacher);
}
}
}
// we do not want to set here...
public int[] QueuedIds()
{
var retVal = new List<int>();
var movies = Cache.Get<List<int>>(CacheKeys.RadarrMovies);
if (movies != null)
{
retVal.AddRange(movies);
}
return retVal.ToArray();
}
public void Execute(IJobExecutionContext context)
{
Queued();
}
}
}

@ -78,7 +78,7 @@ namespace Ombi.Services.Jobs
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public void Execute(IJobExecutionContext context)
public void Start()
{
try
{
@ -100,6 +100,10 @@ namespace Ombi.Services.Jobs
JobRecord.SetRunning(false, JobNames.RecentlyAddedEmail);
}
}
public void Execute(IJobExecutionContext context)
{
Start();
}
public void Test()
{
@ -455,7 +459,7 @@ namespace Ombi.Services.Jobs
if (!testEmail)
{
var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification);
var users = UserHelper.GetUsersWithFeature(Features.Newsletter);
if (users != null)
{
foreach (var user in users)

@ -35,7 +35,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class StoreBackup : IJob
public class StoreBackup : IJob, IStoreBackup
{
public StoreBackup(ISqliteConfiguration sql, IJobRecord rec)
{
@ -48,6 +48,13 @@ namespace Ombi.Services.Jobs
private static Logger Log = LogManager.GetCurrentClassLogger();
public void Start()
{
JobRecord.SetRunning(true, JobNames.CpCacher);
TakeBackup();
Cleanup();
}
public void Execute(IJobExecutionContext context)
{
JobRecord.SetRunning(true, JobNames.CpCacher);

@ -36,7 +36,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class StoreCleanup : IJob
public class StoreCleanup : IJob, IStoreCleanup
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -81,6 +81,11 @@ namespace Ombi.Services.Jobs
}
public void Start()
{
JobRecord.SetRunning(true, JobNames.CpCacher);
Cleanup();
}
public void Execute(IJobExecutionContext context)
{
JobRecord.SetRunning(true, JobNames.CpCacher);

@ -39,7 +39,7 @@ using Quartz;
namespace Ombi.Services.Jobs
{
public class UserRequestLimitResetter : IJob
public class UserRequestLimitResetter : IJob, IUserRequestLimitResetter
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -94,6 +94,31 @@ namespace Ombi.Services.Jobs
}
}
public void Start()
{
Record.SetRunning(true, JobNames.CpCacher);
try
{
var settings = Settings.GetSettings();
var users = Repo.GetAll();
var requestLimits = users as RequestLimit[] ?? users.ToArray();
MovieLimit(settings, requestLimits);
TvLimit(settings, requestLimits);
AlbumLimit(settings, requestLimits);
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
Record.Record(JobNames.RequestLimitReset);
Record.SetRunning(false, JobNames.CpCacher);
}
}
public void Execute(IJobExecutionContext context)
{
Record.SetRunning(true, JobNames.CpCacher);

@ -0,0 +1,165 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SlackNotification.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Threading.Tasks;
using NLog;
using Ombi.Api.Interfaces;
using Ombi.Api.Models.Notifications;
using Ombi.Core;
using Ombi.Core.Models;
using Ombi.Core.SettingModels;
using Ombi.Services.Interfaces;
namespace Ombi.Services.Notification
{
public class DiscordNotification : INotification
{
public DiscordNotification(IDiscordApi api, ISettingsService<DiscordNotificationSettings> sn)
{
Api = api;
Settings = sn;
}
public string NotificationName => "DiscordNotification";
private IDiscordApi Api { get; }
private ISettingsService<DiscordNotificationSettings> Settings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task NotifyAsync(NotificationModel model)
{
var settings = Settings.GetSettings();
await NotifyAsync(model, settings);
}
public async Task NotifyAsync(NotificationModel model, Settings settings)
{
if (settings == null) await NotifyAsync(model);
var pushSettings = (DiscordNotificationSettings)settings;
if (!ValidateConfiguration(pushSettings))
{
Log.Error("Settings for Slack was not correct, we cannot push a notification");
return;
}
switch (model.NotificationType)
{
case NotificationType.NewRequest:
await PushNewRequestAsync(model, pushSettings);
break;
case NotificationType.Issue:
await PushIssueAsync(model, pushSettings);
break;
case NotificationType.RequestAvailable:
break;
case NotificationType.RequestApproved:
break;
case NotificationType.AdminNote:
break;
case NotificationType.Test:
await PushTest(pushSettings);
break;
case NotificationType.RequestDeclined:
await PushRequestDeclinedAsync(model, pushSettings);
break;
case NotificationType.ItemAddedToFaultQueue:
await PushFaultQueue(model, pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private async Task PushNewRequestAsync(NotificationModel model, DiscordNotificationSettings settings)
{
var message = $"{model.Title} has been requested by user: {model.User}";
await Push(settings, message);
}
private async Task PushRequestDeclinedAsync(NotificationModel model, DiscordNotificationSettings settings)
{
var message = $"Hello! Your request for {model.Title} has been declined, Sorry!";
await Push(settings, message);
}
private async Task PushIssueAsync(NotificationModel model, DiscordNotificationSettings settings)
{
var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
await Push(settings, message);
}
private async Task PushTest(DiscordNotificationSettings settings)
{
var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!";
await Push(settings, message);
}
private async Task PushFaultQueue(NotificationModel model, DiscordNotificationSettings settings)
{
var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying";
await Push(settings, message);
}
private async Task Push(DiscordNotificationSettings config, string message)
{
try
{
await Api.SendMessageAsync(message, config.WebookId, config.Token, config.Username);
}
catch (Exception e)
{
Log.Error(e);
}
}
private bool ValidateConfiguration(DiscordNotificationSettings settings)
{
if (!settings.Enabled)
{
return false;
}
if (string.IsNullOrEmpty(settings.WebhookUrl))
{
return false;
}
try
{
var a = settings.Token;
var b = settings.WebookId;
}
catch (IndexOutOfRangeException)
{
return false;
}
return true;
}
}
}

@ -119,14 +119,6 @@ namespace Ombi.Services.Notification
return false;
}
if (!settings.EnableUserEmailNotifications)
{
if (!settings.Enabled)
{
return false;
}
}
return true;
}
@ -237,16 +229,12 @@ namespace Ombi.Services.Notification
private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings)
{
if (!settings.EnableUserEmailNotifications)
{
await Task.FromResult(false);
}
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
$"Ombi: {model.Title} is now available!",
$"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)",
$"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" };
var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)" };
var message = new MimeMessage
{

@ -98,10 +98,31 @@ namespace Ombi.Services.Notification
return;
}
var localUser =
users.FirstOrDefault( x =>
x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase) ||
x.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase));
UserHelperModel localUser = null;
//users.FirstOrDefault( x =>
// x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase) ||
// x.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase));
foreach (var userHelperModel in users)
{
if (!string.IsNullOrEmpty(userHelperModel.Username))
{
if (userHelperModel.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase))
{
localUser = userHelperModel;
break;
}
}
if (!string.IsNullOrEmpty(userHelperModel.UserAlias))
{
if (userHelperModel.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase))
{
localUser = userHelperModel;
break;
}
}
}
// So if the request was from an alias, then we need to use the local user (since that contains the alias).
// If we do not have a local user, then we should be using the Plex user if that user exists.
@ -152,8 +173,36 @@ namespace Ombi.Services.Notification
var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).ToList();
Log.Debug("Notifying Users Count {0}", users.Count);
var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers, StringComparer.CurrentCultureIgnoreCase);
foreach (var user in selectedUsers)
// Get the usernames or alias depending if they have an alias
var userNamesWithFeature = users.Select(x => x.UsernameOrAlias).ToList();
Log.Debug("Users with the feature count {0}", userNamesWithFeature.Count);
Log.Debug("Usernames: ");
foreach (var u in userNamesWithFeature)
{
Log.Debug(u);
}
Log.Debug("Users in the requested model count: {0}", model.AllUsers.Count);
Log.Debug("usernames from model: ");
foreach (var modelAllUser in model.AllUsers)
{
Log.Debug(modelAllUser);
}
if (model.AllUsers == null || !model.AllUsers.Any())
{
Log.Debug("There are no users in the model.AllUsers, no users to notify");
return;
}
var usersToNotify = userNamesWithFeature.Intersect(model.AllUsers, StringComparer.CurrentCultureIgnoreCase).ToList();
if (!usersToNotify.Any())
{
Log.Debug("Could not find any users after the .Intersect()");
}
Log.Debug("Users being notified for this request count {0}", users.Count);
foreach (var user in usersToNotify)
{
Log.Info("Notifying user {0}", user);
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))

@ -86,13 +86,21 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Interfaces\IRadarrCacher.cs" />
<Compile Include="Interfaces\IWatcherCacher.cs" />
<Compile Include="Interfaces\IJobRecord.cs" />
<Compile Include="Interfaces\INotificationEngine.cs" />
<Compile Include="Interfaces\IStoreBackup.cs" />
<Compile Include="Interfaces\IStoreCleanup.cs" />
<Compile Include="Interfaces\IUserRequestLimitResetter.cs" />
<Compile Include="Jobs\IFaultQueueHandler.cs" />
<Compile Include="Jobs\IPlexEpisodeCacher.cs" />
<Compile Include="Jobs\IPlexUserChecker.cs" />
<Compile Include="Jobs\RadarrCacher.cs" />
<Compile Include="Jobs\WatcherCacher.cs" />
<Compile Include="Jobs\HtmlTemplateGenerator.cs" />
<Compile Include="Jobs\IPlexContentCacher.cs" />
<Compile Include="Jobs\IRecentlyAdded.cs" />
<Compile Include="Interfaces\IPlexContentCacher.cs" />
<Compile Include="Interfaces\IRecentlyAdded.cs" />
<Compile Include="Jobs\JobRecord.cs" />
<Compile Include="Jobs\JobNames.cs" />
<Compile Include="Jobs\PlexContentCacher.cs" />
@ -126,6 +134,7 @@
<Compile Include="Notification\NotificationService.cs" />
<Compile Include="Notification\PushoverNotification.cs" />
<Compile Include="Notification\PushbulletNotification.cs" />
<Compile Include="Notification\DiscordNotification.cs" />
<Compile Include="Notification\SlackNotification.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

@ -26,6 +26,7 @@
#endregion
using Dapper.Contrib.Extensions;
using Newtonsoft.Json;
namespace Ombi.Store.Models
{

@ -41,7 +41,7 @@ namespace Ombi.Store.Models
public FaultType FaultType { get; set; }
public DateTime? LastRetry { get; set; }
public string Message { get; set; }
public string Description { get; set; }
}
public enum FaultType

@ -27,9 +27,6 @@ namespace Ombi.Store
public string Status { get; set; }
public bool Approved { get; set; }
[Obsolete("Use RequestedUsers")] //TODO remove this obsolete property
public string RequestedBy { get; set; }
public DateTime RequestedDate { get; set; }
public bool Available { get; set; }
public IssueState Issues { get; set; }
@ -46,6 +43,13 @@ namespace Ombi.Store
public List<EpisodesModel> Episodes { get; set; }
public bool Denied { get; set; }
public string DeniedReason { get; set; }
/// <summary>
/// For TV Shows with a custom root folder
/// </summary>
/// <value>
/// The root folder selected.
/// </value>
public int RootFolderSelected { get; set; }
[JsonIgnore]
public List<string> AllUsers
@ -53,14 +57,9 @@ namespace Ombi.Store
get
{
var u = new List<string>();
if (!string.IsNullOrEmpty(RequestedBy))
{
u.Add(RequestedBy);
}
if (RequestedUsers.Any())
if (RequestedUsers != null && RequestedUsers.Any())
{
u.AddRange(RequestedUsers.Where(requestedUser => requestedUser != RequestedBy));
u.AddRange(RequestedUsers);
}
return u;
}

@ -35,6 +35,7 @@ using Ombi.Api.Interfaces;
using Ombi.Api.Models.Sonarr;
using Ombi.Core;
using Ombi.Core.SettingModels;
using Ombi.Helpers;
using Ombi.Store;
using Ploeh.AutoFixture;
@ -49,6 +50,7 @@ namespace Ombi.UI.Tests
private TvSender Sender { get; set; }
private Fixture F { get; set; }
private Mock<ICacheProvider> Cache { get; set; }
[SetUp]
public void Setup()
@ -56,7 +58,8 @@ namespace Ombi.UI.Tests
F = new Fixture();
SonarrMock = new Mock<ISonarrApi>();
SickrageMock = new Mock<ISickRageApi>();
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object);
Cache = new Mock<ICacheProvider>();
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object);
}
[Test]
@ -66,7 +69,7 @@ namespace Ombi.UI.Tests
var seriesResult = new SonarrAddSeries() { title = "ABC"};
SonarrMock.Setup(x => x.GetSeries(It.IsAny<string>(), It.IsAny<Uri>())).Returns(F.Build<Series>().With(x => x.tvdbId, 1).With(x => x.title, "ABC").CreateMany().ToList());
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object);
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object);
var request = new RequestedModel {SeasonsRequested = "All", ProviderId = 1, Title = "ABC"};
@ -116,7 +119,7 @@ namespace Ombi.UI.Tests
SonarrMock.Setup(x => x.GetEpisodes(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Uri>())).Returns(F.CreateMany<SonarrEpisodes>());
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object);
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object);
var episodes = new List<EpisodesModel>
{
new EpisodesModel

@ -509,3 +509,11 @@ label {
background-position: center;
position: absolute; }
.list-group-item-dropdown {
position: relative;
display: block;
padding: 10px 15px;
margin-bottom: -1px;
background-color: #3e3e3e;
border: 1px solid transparent; }

File diff suppressed because one or more lines are too long

@ -632,3 +632,13 @@ $border-radius: 10px;
background-position:center;
position:absolute;
}
.list-group-item-dropdown {
position: relative;
display: block;
padding: 10px 15px;
margin-bottom: -1px;
background-color: #3e3e3e;
border: 1px solid transparent;
}

@ -5259,6 +5259,7 @@ a.thumbnail.active {
background-color: #4e5d6c;
border: 1px solid transparent;
}
.list-group-item:first-child {
border-top-right-radius: 0;
border-top-left-radius: 0;

@ -559,6 +559,25 @@ $(document).on("click", ".approve-with-quality", function (e) {
});
// Change root folder
$(document).on("click", ".change-root-folder", function (e) {
e.preventDefault();
var $this = $(this);
var $button = $this.parents('.btn-split').children('.change').first();
var rootFolderId = e.target.id
var $form = $this.parents('form').first();
if ($button.text() === " Loading...") {
return;
}
loadingButton($button.attr('id'), "success");
changeRootFolder($form, rootFolderId, function () {
});
});
// Change Availability
$(document).on("click", ".change", function (e) {
@ -638,6 +657,37 @@ function approveRequest($form, qualityId, successCallback) {
});
}
function changeRootFolder($form, rootFolderId, successCallback) {
var formData = $form.serialize();
if (rootFolderId) formData += ("&rootFolderId=" + rootFolderId);
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: formData,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
if (response.message) {
generateNotify(response.message, "success");
} else {
generateNotify("Success! Changed Root Path.", "success");
}
if (successCallback) {
successCallback();
}
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
}
function denyRequest($form, successCallback) {
var formData = $form.serialize();
@ -808,6 +858,9 @@ function buildRequestContext(result, type) {
musicBrainzId: result.musicBrainzId,
denied: result.denied,
deniedReason: result.deniedReason,
hasRootFolders: result.hasRootFolders,
rootFolders: result.rootFolders,
currentRootPath : result.currentRootPath
};
return context;

@ -24,7 +24,8 @@ Function.prototype.bind = function (parent) {
$(function () {
var searchSource = $("#search-template").html();
var useNewSearch = $('#useNewSearch').text() == 'True';
var searchSource = useNewSearch ? $("#search-templateNew").html() : $("#search-template").html();
var seasonsSource = $("#seasons-template").html();
var musicSource = $("#music-template").html();
var seasonsNumberSource = $("#seasonNumber-template").html();
@ -72,6 +73,25 @@ $(function () {
moviesInTheaters();
});
// TV DropDown
$('#popularShows').on('click', function (e) {
e.preventDefault();
popularShows();
});
$('#trendingShows').on('click', function (e) {
e.preventDefault();
trendingTv();
});
$('#mostWatchedShows').on('click', function (e) {
e.preventDefault();
mostwatchedTv();
});
$('#anticipatedShows').on('click', function (e) {
e.preventDefault();
anticipatedTv();
});
// Type in TV search
$("#tvSearchContent").on("input", function () {
if (searchTimer) {
@ -293,6 +313,23 @@ $(function () {
getMovies(url);
}
function popularShows() {
var url = createBaseUrl(base, '/search/tv/popular');
getTvShows(url, true);
}
function anticipatedTv() {
var url = createBaseUrl(base, '/search/tv/anticipated');
getTvShows(url, true);
}
function trendingTv() {
var url = createBaseUrl(base, '/search/tv/trending');
getTvShows(url, true);
}
function mostwatchedTv() {
var url = createBaseUrl(base, '/search/tv/mostwatched');
getTvShows(url, true);
}
function getMovies(url) {
resetMovies();
@ -304,6 +341,8 @@ $(function () {
var html = searchTemplate(context);
$("#movieList").append(html);
checkNetflix(context.title, context.id);
});
}
else {
@ -321,10 +360,10 @@ $(function () {
var query = $("#tvSearchContent").val();
var url = createBaseUrl(base, '/search/tv/');
query ? getTvShows(url + query) : resetTvShows();
query ? getTvShows(url + query, false) : resetTvShows();
}
function getTvShows(url) {
function getTvShows(url, loadImage) {
resetTvShows();
$('#tvSearchButton').attr("class", "fa fa-spinner fa-spin");
@ -334,6 +373,11 @@ $(function () {
var context = buildTvShowContext(result);
var html = searchTemplate(context);
$("#tvList").append(html);
checkNetflix(context.title, context.id);
if (loadImage) {
getTvPoster(result.id);
}
});
}
else {
@ -343,6 +387,19 @@ $(function () {
});
};
function checkNetflix(title, id) {
var url = createBaseUrl(base, '/searchextension/netflix/' + title);
$.ajax(url).success(function (results) {
if (results.result) {
// It's on Netflix
$('#' + id + 'netflixTab')
.html("<a href='https://www.netflix.com/watch/"+results.netflixId+"' target='_blank'><span class='label label-success'>Avaialble on Netflix</span></a>");
}
});
}
function resetTvShows() {
$("#tvList").html("");
}
@ -388,6 +445,16 @@ $(function () {
});
};
function getTvPoster(theTvDbId) {
var url = createBaseUrl(base, '/search/tv/poster/');
$.ajax(url + theTvDbId).success(function (result) {
if (result) {
$('#' + theTvDbId + "imgDiv").html(" <img class='img-responsive' src='" + result + "' width='150' alt='poster'>");
}
});
};
function buildMovieContext(result) {
var date = new Date(result.releaseDate);
var year = date.getFullYear();
@ -404,7 +471,11 @@ $(function () {
requested: result.requested,
approved: result.approved,
available: result.available,
url: result.plexUrl
url: result.plexUrl,
trailer: result.trailer,
homepage: result.homepage,
releaseDate: Humanize(result.releaseDate),
status: result.status
};
return context;
@ -414,6 +485,7 @@ $(function () {
var date = new Date(result.firstAired);
var year = date.getFullYear();
var context = {
status: result.status,
posterPath: result.banner,
id: result.id,
title: result.seriesName,
@ -430,8 +502,11 @@ $(function () {
tvPartialAvailable: result.tvPartialAvailable,
disableTvRequestsByEpisode: result.disableTvRequestsByEpisode,
disableTvRequestsBySeason: result.disableTvRequestsBySeason,
enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries
};
enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries,
trailer: result.trailer,
homepage: result.homepage,
firstAired: Humanize(result.firstAired)
};
return context;
}

@ -8,7 +8,7 @@
return s;
}
$(function() {
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
@ -93,6 +93,19 @@ function createBaseUrl(base, url) {
return url;
}
function createLocalUrl(url) {
var base = $('#baseUrl').text();
if (base) {
if (url.charAt(0) === "/") {
url = "/" + base + url;
} else {
url = "/" + base + "/" + url;
}
}
return url;
}
var noResultsHtml = "<div class='no-search-results'>" +
"<i class='fa fa-film no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div></div>";
var noResultsMusic = "<div class='no-search-results'>" +

@ -0,0 +1,41 @@
/**
* System configuration for Angular 2 samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': '../node_modules/',
'app' : '../app/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);

@ -199,7 +199,7 @@ namespace Ombi.UI.Helpers
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
sb.AppendLine($"<script src=\"{content}/Content/wizard.js?v={Assembly}\" type=\"text/javascript\"></script>");
return helper.Raw(sb.ToString());
@ -226,9 +226,9 @@ namespace Ombi.UI.Helpers
sb.Append($"<script src=\"{content}/Content/app/userManagement/userManagementService.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/app/userManagement/Directives/userManagementDirective.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/moment.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/spin.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-spinner.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-loading-spinner.js\"></script>");
sb.Append($"<script src=\"{content}/Content/spin.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-spinner.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-loading-spinner.js\"></script>");
return helper.Raw(sb.ToString());
}
@ -290,16 +290,19 @@ namespace Ombi.UI.Helpers
return helper.Raw(asset);
}
public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title)
public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title, string icon = null)
{
var content = GetLinkUrl(GetBaseUrl());
if (!string.IsNullOrEmpty(content))
{
url = $"/{content}{url}";
}
var returnString = context.Request.Path == url
? $"<a class=\"list-group-item active\" href=\"{url}\">{title}</a>"
: $"<a class=\"list-group-item\" href=\"{url}\">{title}</a>";
var iconHtml = string.IsNullOrEmpty(icon) ? "" : $"<span style=\"font-size:16px; \" class=\"pull-right hidden-xs showopacity {icon}\"></span>";
var returnString = context.Request.Path == url
? $"<a class=\"list-group-item active\" href=\"{url}\">{title} {iconHtml}</a>"
: $"<a class=\"list-group-item\" href=\"{url}\">{title} {iconHtml}</a>";
return helper.Raw(returnString);
}
@ -311,8 +314,8 @@ namespace Ombi.UI.Helpers
{
url = $"/{content}{url}";
}
var returnString = context.Request.Path == url ?
$"<li class=\"active\"><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title}</a></li>"
var returnString = context.Request.Path == url ?
$"<li class=\"active\"><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title}</a></li>"
: $"<li><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title}</a></li>";
return helper.Raw(returnString);
@ -326,8 +329,8 @@ namespace Ombi.UI.Helpers
url = $"/{content}{url}";
}
var returnString = context.Request.Path == url
? $"<li class=\"active\"><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title} {extraHtml}</a></li>"
var returnString = context.Request.Path == url
? $"<li class=\"active\"><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title} {extraHtml}</a></li>"
: $"<li><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title} {extraHtml}</a></li>";
return helper.Raw(returnString);

@ -75,6 +75,7 @@ namespace Ombi.UI.Jobs
JobBuilder.Create<UserRequestLimitResetter>().WithIdentity("UserRequestLimiter", "Request").Build(),
JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build(),
JobBuilder.Create<FaultQueueHandler>().WithIdentity("FaultQueueHandler", "Fault").Build(),
JobBuilder.Create<RadarrCacher>().WithIdentity("RadarrCacher", "Cache").Build(),
};
jobs.AddRange(jobList);
@ -170,6 +171,10 @@ namespace Ombi.UI.Jobs
{
s.PlexUserChecker = 24;
}
if (s.RadarrCacher == 0)
{
s.RadarrCacher = 60;
}
var triggers = new List<ITrigger>();
@ -222,6 +227,14 @@ namespace Ombi.UI.Jobs
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.WatcherCacher).RepeatForever())
.Build();
var radarrCacher =
TriggerBuilder.Create()
.WithIdentity("RadarrCacher", "Cache")
.StartNow()
//.StartAt(DateBuilder.FutureDate(2, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.RadarrCacher).RepeatForever())
.Build();
var storeBackup =
TriggerBuilder.Create()
.WithIdentity("StoreBackup", "Database")
@ -280,6 +293,7 @@ namespace Ombi.UI.Jobs
triggers.Add(fault);
triggers.Add(plexCacher);
triggers.Add(plexUserChecker);
triggers.Add(radarrCacher);
return triggers;
}

@ -52,7 +52,7 @@ namespace Ombi.UI.ModelDataProviders
with.Property(x => x.QualityProfile).Description("Sonarr's quality profile").Required(true);
with.Property(x => x.SeasonFolders).Description("Sonarr's season folders").Required(false);
with.Property(x => x.RootPath).Description("Sonarr's root path").Required(false);
});
}

@ -58,5 +58,8 @@ namespace Ombi.UI.Models
public Store.EpisodesModel[] Episodes { get; set; }
public bool Denied { get; set; }
public string DeniedReason { get; set; }
public RootFolderModel[] RootFolders { get; set; }
public bool HasRootFolders { get; set; }
public string CurrentRootPath { get; set; }
}
}

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: RootFolderModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
namespace Ombi.UI.Models
{
public class RootFolderModel
{
public string Id { get; set; }
public string Path { get; set; }
public long FreeSpace { get; set; }
}
}

@ -0,0 +1,37 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: SearchLoadViewModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using Ombi.Core.SettingModels;
namespace Ombi.UI.Models
{
public class SearchLoadViewModel
{
public PlexRequestSettings Settings { get; set; }
public CustomizationSettings CustomizationSettings { get; set; }
}
}

@ -47,5 +47,8 @@ namespace Ombi.UI.Models
public double VoteAverage { get; set; }
public int VoteCount { get; set; }
public bool AlreadyInCp { get; set; }
public string Trailer { get; set; }
public string Homepage { get; set; }
public string ImdbId { get; set; }
}
}

@ -58,5 +58,20 @@ namespace Ombi.UI.Models
public bool DisableTvRequestsByEpisode { get; set; }
public bool DisableTvRequestsBySeason { get; set; }
public bool EnableTvRequestsForOnlySeries { get; set; }
/// <summary>
/// This is used from the Trakt API
/// </summary>
/// <value>
/// The trailer.
/// </value>
public string Trailer { get; set; }
/// <summary>
/// This is used from the Trakt API
/// </summary>
/// <value>
/// The trailer.
/// </value>
public string Homepage { get; set; }
}
}

@ -93,6 +93,10 @@ namespace Ombi.UI.Modules.Admin
private IAnalytics Analytics { get; }
private IRecentlyAdded RecentlyAdded { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private ISettingsService<DiscordNotificationSettings> DiscordSettings { get; }
private IDiscordApi DiscordApi { get; }
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IRadarrApi RadarrApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -118,7 +122,9 @@ namespace Ombi.UI.Modules.Admin
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics,
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded,
ISettingsService<WatcherSettings> watcherSettings
ISettingsService<WatcherSettings> watcherSettings ,
ISettingsService<DiscordNotificationSettings> discord,
IDiscordApi discordapi, ISettingsService<RadarrSettings> settings, IRadarrApi radarrApi
, ISecurityExtensions security) : base("admin", prService, security)
{
PrService = prService;
@ -150,6 +156,10 @@ namespace Ombi.UI.Modules.Admin
NotifySettings = notifyService;
RecentlyAdded = recentlyAdded;
WatcherSettings = watcherSettings;
DiscordSettings = discord;
DiscordApi = discordapi;
RadarrSettings = settings;
RadarrApi = radarrApi;
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
@ -165,18 +175,19 @@ namespace Ombi.UI.Modules.Admin
Get["/getusers"] = _ => GetUsers();
Get["/couchpotato"] = _ => CouchPotato();
Post["/couchpotato"] = _ => SaveCouchPotato();
Post["/couchpotato", true] = async (x, ct) => await SaveCouchPotato();
Get["/plex"] = _ => Plex();
Post["/plex", true] = async (x, ct) => await SavePlex();
Get["/sonarr"] = _ => Sonarr();
Post["/sonarr"] = _ => SaveSonarr();
Post["/sonarrprofiles"] = _ => GetSonarrQualityProfiles();
Get["/sickrage"] = _ => Sickrage();
Post["/sickrage"] = _ => SaveSickrage();
Post["/sonarrprofiles"] = _ => GetSonarrQualityProfiles();
Post["/cpprofiles", true] = async (x, ct) => await GetCpProfiles();
Post["/cpapikey"] = x => GetCpApiKey();
@ -200,18 +211,21 @@ namespace Ombi.UI.Modules.Admin
Get["/headphones"] = _ => Headphones();
Post["/headphones"] = _ => SaveHeadphones();
Get["/newsletter"] = _ => Newsletter();
Post["/newsletter"] = _ => SaveNewsletter();
Get["/newsletter", true] = async (x, ct) => await Newsletter();
Post["/newsletter", true] = async (x, ct) => await SaveNewsletter();
Post["/createapikey"] = x => CreateApiKey();
Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications();
Post["/testdiscordnotification", true] = async (x, ct) => await TestDiscordNotification();
Get["/discordnotification", true] = async (x, ct) => await DiscordNotification();
Post["/discordnotification", true] = async (x, ct) => await SaveDiscordNotifications();
Get["/landingpage", true] = async (x, ct) => await LandingPage();
Post["/landingpage", true] = async (x, ct) => await SaveLandingPage();
@ -368,7 +382,7 @@ namespace Ombi.UI.Modules.Admin
return View["CouchPotato", settings];
}
private Response SaveCouchPotato()
private async Task<Response> SaveCouchPotato()
{
var couchPotatoSettings = this.Bind<CouchPotatoSettings>();
var valid = this.Validate(couchPotatoSettings);
@ -377,7 +391,7 @@ namespace Ombi.UI.Modules.Admin
return Response.AsJson(valid.SendJsonError());
}
var watcherSettings = WatcherSettings.GetSettings();
var watcherSettings = await WatcherSettings.GetSettingsAsync();
if (watcherSettings.Enabled)
{
@ -389,8 +403,20 @@ namespace Ombi.UI.Modules.Admin
});
}
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
{
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "Cannot have Radarr and CouchPotato both enabled."
});
}
couchPotatoSettings.ApiKey = couchPotatoSettings.ApiKey.Trim();
var result = CpService.SaveSettings(couchPotatoSettings);
var result = await CpService.SaveSettingsAsync(couchPotatoSettings);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
@ -448,6 +474,7 @@ namespace Ombi.UI.Modules.Admin
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "SickRage is enabled, we cannot enable Sonarr and SickRage" });
}
sonarrSettings.ApiKey = sonarrSettings.ApiKey.Trim();
var result = SonarrService.SaveSettings(sonarrSettings);
@ -456,6 +483,10 @@ namespace Ombi.UI.Modules.Admin
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Negotiator Sickrage()
{
var settings = SickRageService.GetSettings();
@ -814,13 +845,13 @@ namespace Ombi.UI.Modules.Admin
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Negotiator Newsletter()
private async Task<Negotiator> Newsletter()
{
var settings = NewsLetterService.GetSettings();
var settings = await NewsLetterService.GetSettingsAsync();
return View["NewsletterSettings", settings];
}
private Response SaveNewsletter()
private async Task<Response> SaveNewsletter()
{
var settings = this.Bind<NewletterSettings>();
@ -828,9 +859,17 @@ namespace Ombi.UI.Modules.Admin
if (!valid.IsValid)
{
var error = valid.SendJsonError();
Log.Info("Error validating Headphones settings, message: {0}", error.Message);
Log.Info("Error validating Newsletter settings, message: {0}", error.Message);
return Response.AsJson(error);
}
// Make sure emails are setup
var emailSettings = await EmailService.GetSettingsAsync();
if (!emailSettings.Enabled)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please enable your email notifications" });
}
settings.SendRecentlyAddedEmail = settings.SendRecentlyAddedEmail;
var result = NewsLetterService.SaveSettings(settings);
@ -918,6 +957,71 @@ namespace Ombi.UI.Modules.Admin
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private async Task<Negotiator> DiscordNotification()
{
var settings = await DiscordSettings.GetSettingsAsync();
return View["DiscordNotification", settings];
}
private async Task<Response> TestDiscordNotification()
{
var settings = this.BindAndValidate<DiscordNotificationSettings>();
if (!ModelValidationResult.IsValid)
{
return Response.AsJson(ModelValidationResult.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
var currentDicordSettings = await DiscordSettings.GetSettingsAsync();
try
{
NotificationService.Subscribe(new DiscordNotification(DiscordApi, DiscordSettings));
settings.Enabled = true;
await NotificationService.Publish(notificationModel, settings);
Log.Info("Sent Discord notification test");
}
catch (Exception e)
{
Log.Error(e, "Failed to subscribe and publish test Discord Notification");
}
finally
{
if (!currentDicordSettings.Enabled)
{
NotificationService.UnSubscribe(new DiscordNotification(DiscordApi, DiscordSettings));
}
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Discord Notification! If you do not receive it please check the logs." });
}
private async Task<Response> SaveDiscordNotifications()
{
var settings = this.BindAndValidate<DiscordNotificationSettings>();
if (!ModelValidationResult.IsValid)
{
return Response.AsJson(ModelValidationResult.SendJsonError());
}
var result = await DiscordSettings.SaveSettingsAsync(settings);
if (settings.Enabled)
{
NotificationService.Subscribe(new DiscordNotification(DiscordApi, DiscordSettings));
}
else
{
NotificationService.UnSubscribe(new DiscordNotification(DiscordApi, DiscordSettings));
}
Log.Info("Saved discord settings, result: {0}", result);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Discord Notifications!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private async Task<Negotiator> LandingPage()
{
var settings = await LandingSettings.GetSettingsAsync();

@ -64,7 +64,7 @@ namespace Ombi.UI.Modules.Admin
Id = r.Id,
PrimaryIdentifier = r.PrimaryIdentifier,
LastRetry = r.LastRetry,
Message = r.Message
Message = r.Description
}).ToList();
return View["RequestFaultQueue", model];

@ -36,6 +36,7 @@ using Nancy;
using Nancy.ModelBinding;
using Nancy.Responses.Negotiation;
using Nancy.Validation;
using Ombi.Api.Interfaces;
using Ombi.Core;
using Ombi.Core.SettingModels;
using Ombi.Core.StatusChecker;
@ -52,22 +53,40 @@ namespace Ombi.UI.Modules.Admin
public class IntegrationModule : BaseModule
{
public IntegrationModule(ISettingsService<PlexRequestSettings> settingsService, ISettingsService<WatcherSettings> watcher,
ISettingsService<CouchPotatoSettings> cp,ISecurityExtensions security, IAnalytics a) : base("admin", settingsService, security)
ISettingsService<CouchPotatoSettings> cp,ISecurityExtensions security, IAnalytics a, ISettingsService<RadarrSettings> radarrSettings,
ICacheProvider cache, IRadarrApi radarrApi, ISonarrApi sonarrApi) : base("admin", settingsService, security)
{
WatcherSettings = watcher;
Analytics = a;
CpSettings = cp;
Cache = cache;
RadarrApi = radarrApi;
RadarrSettings = radarrSettings;
SonarrApi = sonarrApi;
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
Post["/sonarrrootfolders"] = _ => GetSonarrRootFolders();
Get["/watcher", true] = async (x, ct) => await Watcher();
Post["/watcher", true] = async (x, ct) => await SaveWatcher();
Get["/radarr", true] = async (x, ct) => await Radarr();
Post["/radarr", true] = async (x, ct) => await SaveRadarr();
Post["/radarrprofiles"] = _ => GetRadarrQualityProfiles();
}
private ISettingsService<WatcherSettings> WatcherSettings { get; }
private ISettingsService<CouchPotatoSettings> CpSettings { get; }
private ISettingsService<RadarrSettings> RadarrSettings { get; }
private IRadarrApi RadarrApi { get; }
private ICacheProvider Cache { get; }
private IAnalytics Analytics { get; }
private ISonarrApi SonarrApi { get; }
private async Task<Negotiator> Watcher()
{
@ -97,6 +116,18 @@ namespace Ombi.UI.Modules.Admin
});
}
var radarrSettings = await RadarrSettings.GetSettingsAsync();
if (radarrSettings.Enabled)
{
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "Cannot have Radarr and CouchPotato both enabled."
});
}
settings.ApiKey = settings.ApiKey.Trim();
var result = await WatcherSettings.SaveSettingsAsync(settings);
return Response.AsJson(result
@ -104,5 +135,72 @@ namespace Ombi.UI.Modules.Admin
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private async Task<Negotiator> Radarr()
{
var settings = await RadarrSettings.GetSettingsAsync();
return View["Radarr", settings];
}
private async Task<Response> SaveRadarr()
{
var radarrSettings = this.Bind<RadarrSettings>();
//Check Watcher and CP make sure they are not enabled
var watcher = await WatcherSettings.GetSettingsAsync();
if (watcher.Enabled)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Watcher is enabled, we cannot enable Watcher and Radarr" });
}
var cp = await CpSettings.GetSettingsAsync();
if (cp.Enabled)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "CouchPotato is enabled, we cannot enable Watcher and CouchPotato" });
}
var valid = this.Validate(radarrSettings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
radarrSettings.ApiKey = radarrSettings.ApiKey.Trim();
var result = await RadarrSettings.SaveSettingsAsync(radarrSettings);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Radarr!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Response GetRadarrQualityProfiles()
{
var settings = this.Bind<RadarrSettings>();
var profiles = RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
// set the cache
if (profiles != null)
{
Cache.Set(CacheKeys.RadarrQualityProfiles, profiles);
}
return Response.AsJson(profiles);
}
private Response GetSonarrRootFolders()
{
var settings = this.Bind<SonarrSettings>();
var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
// set the cache
if (rootFolders != null)
{
Cache.Set(CacheKeys.SonarrRootFolders, rootFolders);
}
return Response.AsJson(rootFolders);
}
}
}

@ -0,0 +1,150 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AboutModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Threading.Tasks;
using Nancy;
using Ombi.Core;
using Ombi.Core.SettingModels;
using Ombi.Helpers.Permissions;
using Ombi.Services.Interfaces;
using Ombi.Services.Jobs;
using Ombi.UI.Models;
using ISecurityExtensions = Ombi.Core.ISecurityExtensions;
namespace Ombi.UI.Modules.Admin
{
public class ScheduledJobsRunnerModule : BaseModule
{
public ScheduledJobsRunnerModule(ISettingsService<PlexRequestSettings> settingsService,
ISecurityExtensions security, IPlexContentCacher contentCacher, ISonarrCacher sonarrCacher, IWatcherCacher watcherCacher,
IRadarrCacher radarrCacher, ICouchPotatoCacher cpCacher, IStoreBackup store, ISickRageCacher srCacher, IAvailabilityChecker plexChceker,
IStoreCleanup cleanup, IUserRequestLimitResetter requestLimit, IPlexEpisodeCacher episodeCacher, IRecentlyAdded recentlyAdded,
IFaultQueueHandler faultQueueHandler, IPlexUserChecker plexUserChecker) : base("admin", settingsService, security)
{
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
PlexContentCacher = contentCacher;
SonarrCacher = sonarrCacher;
RadarrCacher = radarrCacher;
WatcherCacher = watcherCacher;
CpCacher = cpCacher;
StoreBackup = store;
SrCacher = srCacher;
AvailabilityChecker = plexChceker;
StoreCleanup = cleanup;
RequestLimit = requestLimit;
EpisodeCacher = episodeCacher;
RecentlyAdded = recentlyAdded;
FaultQueueHandler = faultQueueHandler;
PlexUserChecker = plexUserChecker;
Post["/schedulerun", true] = async (x, ct) => await ScheduleRun((string)Request.Form.key);
}
private IPlexContentCacher PlexContentCacher { get; }
private IRadarrCacher RadarrCacher { get; }
private ISonarrCacher SonarrCacher { get; }
private IWatcherCacher WatcherCacher { get; }
private ICouchPotatoCacher CpCacher { get; }
private IStoreBackup StoreBackup { get; }
private ISickRageCacher SrCacher { get; }
private IAvailabilityChecker AvailabilityChecker { get; }
private IStoreCleanup StoreCleanup { get; }
private IUserRequestLimitResetter RequestLimit { get; }
private IPlexEpisodeCacher EpisodeCacher { get; }
private IRecentlyAdded RecentlyAdded { get; }
private IFaultQueueHandler FaultQueueHandler { get; }
private IPlexUserChecker PlexUserChecker { get; }
private async Task<Response> ScheduleRun(string key)
{
if (key.Equals(JobNames.PlexCacher, StringComparison.CurrentCultureIgnoreCase))
{
PlexContentCacher.CacheContent();
}
if (key.Equals(JobNames.WatcherCacher, StringComparison.CurrentCultureIgnoreCase))
{
WatcherCacher.Queued();
}
if (key.Equals(JobNames.SonarrCacher, StringComparison.CurrentCultureIgnoreCase))
{
SonarrCacher.Queued();
}
if (key.Equals(JobNames.RadarrCacher, StringComparison.CurrentCultureIgnoreCase))
{
RadarrCacher.Queued();
}
if (key.Equals(JobNames.CpCacher, StringComparison.CurrentCultureIgnoreCase))
{
CpCacher.Queued();
}
if (key.Equals(JobNames.StoreBackup, StringComparison.CurrentCultureIgnoreCase))
{
StoreBackup.Start();
}
if (key.Equals(JobNames.SrCacher, StringComparison.CurrentCultureIgnoreCase))
{
SrCacher.Queued();
}
if (key.Equals(JobNames.PlexChecker, StringComparison.CurrentCultureIgnoreCase))
{
AvailabilityChecker.Start();
}
if (key.Equals(JobNames.StoreCleanup, StringComparison.CurrentCultureIgnoreCase))
{
StoreCleanup.Start();
}
if (key.Equals(JobNames.RequestLimitReset, StringComparison.CurrentCultureIgnoreCase))
{
RequestLimit.Start();
}
if (key.Equals(JobNames.EpisodeCacher, StringComparison.CurrentCultureIgnoreCase))
{
EpisodeCacher.Start();
}
if (key.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase))
{
RecentlyAdded.Start();
}
if (key.Equals(JobNames.FaultQueueHandler, StringComparison.CurrentCultureIgnoreCase))
{
FaultQueueHandler.Start();
}
if (key.Equals(JobNames.PlexUserChecker, StringComparison.CurrentCultureIgnoreCase))
{
RequestLimit.Start();
}
return Response.AsJson(new JsonResponseModel { Result = true });
}
}
}

@ -46,7 +46,7 @@ namespace Ombi.UI.Modules
public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi,
ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security,
IWatcherApi watcherApi) : base("test", pr, security)
IWatcherApi watcherApi, IRadarrApi radarrApi) : base("test", pr, security)
{
this.RequiresAuthentication();
@ -56,9 +56,11 @@ namespace Ombi.UI.Modules
SickRageApi = srApi;
HeadphonesApi = hpApi;
WatcherApi = watcherApi;
RadarrApi = radarrApi;
Post["/cp"] = _ => CouchPotatoTest();
Post["/sonarr"] = _ => SonarrTest();
Post["/radarr"] = _ => RadarrTest();
Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest();
Post["/headphones"] = _ => HeadphonesTest();
@ -73,6 +75,7 @@ namespace Ombi.UI.Modules
private ISickRageApi SickRageApi { get; }
private IHeadphonesApi HeadphonesApi { get; }
private IWatcherApi WatcherApi { get; }
private IRadarrApi RadarrApi { get; }
private Response CouchPotatoTest()
{
@ -148,7 +151,7 @@ namespace Ombi.UI.Modules
: Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Sonarr, please check your settings." });
}
catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them.
catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them.
{
Log.Warn("Exception thrown when attempting to get Sonarr's status: ");
Log.Warn(e);
@ -161,6 +164,35 @@ namespace Ombi.UI.Modules
}
}
private Response RadarrTest()
{
var radarrSettings = this.Bind<RadarrSettings>();
var valid = this.Validate(radarrSettings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
try
{
var status = RadarrApi.SystemStatus(radarrSettings.ApiKey, radarrSettings.FullUri);
return status?.version != null
? Response.AsJson(new JsonResponseModel { Result = true, Message = "Connected to Radarr successfully!" })
: Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Radarr, please check your settings." });
}
catch (Exception e) // Exceptions are expected, if we cannot connect so we will just log and swallow them.
{
Log.Warn("Exception thrown when attempting to get Radarr's status: ");
Log.Warn(e);
var message = $"Could not connect to Radarr, please check your settings. <strong>Exception Message:</strong> {e.Message}";
if (e.InnerException != null)
{
message = $"Could not connect to Radarr, please check your settings. <strong>Exception Message:</strong> {e.InnerException.Message}";
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = message });
}
}
private Response PlexTest()
{
var plexSettings = this.Bind<PlexSettings>();

@ -50,7 +50,7 @@ namespace Ombi.UI.Modules
public ApprovalModule(IRequestService service, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ITransientFaultQueue faultQueue
, ISecurityExtensions security, IMovieSender movieSender) : base("approval", pr, security)
, ISecurityExtensions security, IMovieSender movieSender, ICacheProvider cache) : base("approval", pr, security)
{
Before += (ctx) => Security.AdminLoginRedirect(ctx, Permissions.Administrator,Permissions.ManageRequests);
@ -64,6 +64,7 @@ namespace Ombi.UI.Modules
HeadphoneApi = hpApi;
FaultQueue = faultQueue;
MovieSender = movieSender;
Cache = cache;
Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/deny", true] = async (x, ct) => await DenyRequest((int)Request.Form.requestid, (string)Request.Form.reason);
@ -86,6 +87,7 @@ namespace Ombi.UI.Modules
private ISickRageApi SickRageApi { get; }
private IHeadphonesApi HeadphoneApi { get; }
private ITransientFaultQueue FaultQueue { get; }
private ICacheProvider Cache { get; }
/// <summary>
/// Approves the specified request identifier.
@ -120,7 +122,7 @@ namespace Ombi.UI.Modules
private async Task<Response> RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
{
var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back
var sender = new TvSenderOld(SonarrApi, SickRageApi, Cache); // TODO put back
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (sonarrSettings.Enabled)
@ -435,7 +437,7 @@ namespace Ombi.UI.Modules
}
if (r.Type == RequestType.TvShow)
{
var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back
var sender = new TvSenderOld(SonarrApi, SickRageApi, Cache); // TODO put back
var sr = await SickRageSettings.GetSettingsAsync();
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sr.Enabled)

@ -31,6 +31,7 @@ using System.Linq;
using System.Threading;
using Nancy;
using Nancy.Security;
using NLog;
using Ombi.Core;
using Ombi.Core.SettingModels;
using Ombi.Helpers;
@ -100,6 +101,7 @@ namespace Ombi.UI.Modules
}
private static Logger Log = LogManager.GetCurrentClassLogger();
private string _username;
/// <summary>
/// Returns the Username or UserAlias
@ -112,15 +114,16 @@ namespace Ombi.UI.Modules
{
try
{
var username = Security.GetUsername(User.UserName, Session);
var username = Security.GetUsername(User?.UserName, Session);
if (string.IsNullOrEmpty(username))
{
return Session[SessionKeys.UsernameKey].ToString();
return string.Empty;
}
_username = username;
}
catch (Exception)
catch (Exception e)
{
Log.Info(e);
return string.Empty;
}
}
@ -149,9 +152,10 @@ namespace Ombi.UI.Modules
protected bool LoggedIn => Context?.CurrentUser != null;
protected string Culture { get; set; }
private string Culture { get; set; }
protected const string CultureCookieName = "_culture";
protected Response SetCookie()
private Response SetCookie()
{
try
{

@ -96,6 +96,8 @@ namespace Ombi.UI.Modules
Post["/changeavailability", true] = async (x, ct) => await ChangeRequestAvailability((int)Request.Form.Id, (bool)Request.Form.Available);
Post["/changeRootFolder", true] = async (x, ct) => await ChangeRootFolder((int) Request.Form.requestId, (int) Request.Form.rootFolderId);
Get["/UpdateFilters", true] = async (x, ct) => await GetFilterAndSortSettings();
}
@ -160,7 +162,7 @@ namespace Ombi.UI.Modules
}
}
var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests);
var viewModel = dbMovies.Select(movie => new RequestViewModel
{
@ -185,7 +187,7 @@ namespace Ombi.UI.Modules
IssueId = movie.IssueId,
Denied = movie.Denied,
DeniedReason = movie.DeniedReason,
Qualities = qualities.ToArray()
Qualities = qualities.ToArray(),
}).ToList();
return Response.AsJson(viewModel);
@ -193,32 +195,39 @@ namespace Ombi.UI.Modules
private async Task<Response> GetTvShows()
{
var settingsTask = PrSettings.GetSettingsAsync();
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.Type == RequestType.TvShow);
var dbTv = requests;
var settings = await settingsTask;
if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) && !IsAdmin)
{
dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList();
}
IEnumerable<QualityModel> qualities = new List<QualityModel>();
IEnumerable<RootFolderModel> rootFolders = new List<RootFolderModel>();
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (IsAdmin)
{
try
{
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (sonarrSettings.Enabled)
{
var result = Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () =>
var result = await Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () =>
{
return await Task.Run(() => SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
qualities = result.Result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList();
}
qualities = result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList();
var rootFoldersResult =await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () =>
{
return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
rootFolders = rootFoldersResult.Select(x => new RootFolderModel { Id = x.id.ToString(), Path = x.path, FreeSpace = x.freespace}).ToList();
}
else
{
var sickRageSettings = await SickRageSettings.GetSettingsAsync();
@ -235,6 +244,8 @@ namespace Ombi.UI.Modules
}
var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests);
var viewModel = dbTv.Select(tv => new RequestViewModel
{
@ -243,7 +254,7 @@ namespace Ombi.UI.Modules
Status = tv.Status,
ImdbId = tv.ImdbId,
Id = tv.Id,
PosterPath = tv.PosterPath,
PosterPath = tv.PosterPath.Contains("http:") ? tv.PosterPath.Replace("http:", "https:") : tv.PosterPath, // We make the poster path https on request, but this is just incase
ReleaseDate = tv.ReleaseDate,
ReleaseDateTicks = tv.ReleaseDate.Ticks,
RequestedDate = tv.RequestedDate,
@ -262,11 +273,30 @@ namespace Ombi.UI.Modules
TvSeriesRequestType = tv.SeasonsRequested,
Qualities = qualities.ToArray(),
Episodes = tv.Episodes.ToArray(),
RootFolders = rootFolders.ToArray(),
HasRootFolders = rootFolders.Any(),
CurrentRootPath = sonarrSettings.Enabled ? GetRootPath(tv.RootFolderSelected, sonarrSettings).Result : null
}).ToList();
return Response.AsJson(viewModel);
}
private async Task<string> GetRootPath(int pathId, SonarrSettings sonarrSettings)
{
var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () =>
{
return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
foreach (var r in rootFoldersResult.Where(r => r.id == pathId))
{
return r.path;
}
// Return default path
return rootFoldersResult.FirstOrDefault(x => x.id.Equals(int.Parse(sonarrSettings.RootPath)))?.path ?? string.Empty;
}
private async Task<Response> GetAlbumRequests()
{
var settings = PrSettings.GetSettings();
@ -430,5 +460,34 @@ namespace Ombi.UI.Modules
return Response.AsJson(vm);
}
}
private async Task<Response> ChangeRootFolder(int id, int rootFolderId)
{
// Get all root folders
var settings = await SonarrSettings.GetSettingsAsync();
var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
// Get Request
var allRequests = await Service.GetAllAsync();
var request = allRequests.FirstOrDefault(x => x.Id == id);
if (request == null)
{
return Response.AsJson(new JsonResponseModel {Result = false});
}
foreach (var folder in rootFolders)
{
if (folder.id.Equals(rootFolderId))
{
request.RootFolderSelected = folder.id;
break;
}
}
await Service.UpdateRequestAsync(request);
return Response.AsJson(new JsonResponseModel {Result = true});
}
}
}

@ -0,0 +1,62 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: SearchExtensionModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
using Nancy;
using Ombi.Api.Interfaces;
using Ombi.Core;
using Ombi.Core.SettingModels;
namespace Ombi.UI.Modules
{
public class SearchExtensionModule : BaseAuthModule
{
public SearchExtensionModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security, INetflixApi netflix) : base("searchextension",pr, security)
{
NetflixApi = netflix;
Get["/netflix/{searchTerm}", true] = async (x, ctx) => await Netflix(x.searchTerm);
}
private INetflixApi NetflixApi { get; }
public async Task<Response> Netflix(string title)
{
var result = NetflixApi.CheckNetflix(title);
if (!string.IsNullOrEmpty(result.Message))
{
return Response.AsJson(new { Result = false });
}
return Response.AsJson(new { Result = true, NetflixId = result.ShowId });
}
}
}

@ -58,6 +58,7 @@ using Ombi.Store.Repository;
using Ombi.UI.Helpers;
using Ombi.UI.Models;
using TMDbLib.Objects.General;
using TraktApiSharp.Objects.Get.Shows;
using Action = Ombi.Helpers.Analytics.Action;
using EpisodesModel = Ombi.Store.EpisodesModel;
using ISecurityExtensions = Ombi.Core.ISecurityExtensions;
@ -76,7 +77,7 @@ namespace Ombi.UI.Modules
ISettingsService<PlexSettings> plexService, ISettingsService<AuthenticationSettings> auth,
IRepository<UsersToNotify> u, ISettingsService<EmailNotificationSettings> email,
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl, ITransientFaultQueue tfQueue, IRepository<PlexContent> content,
ISecurityExtensions security, IMovieSender movieSender)
ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService<CustomizationSettings> cus)
: base("search", prSettings, security)
{
Auth = auth;
@ -108,6 +109,9 @@ namespace Ombi.UI.Modules
PlexContentRepository = content;
MovieSender = movieSender;
WatcherCacher = watcherCacher;
RadarrCacher = radarrCacher;
TraktApi = traktApi;
CustomizationSettings = cus;
Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad();
@ -119,6 +123,13 @@ namespace Ombi.UI.Modules
Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies();
Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies();
Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular);
Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending);
Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched);
Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated);
Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id);
Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId);
Post["request/tv", true] =
async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
@ -128,6 +139,7 @@ namespace Ombi.UI.Modules
Get["/seasons"] = x => GetSeasons();
Get["/episodes", true] = async (x, ct) => await GetEpisodes();
}
private ITraktApi TraktApi { get; }
private IWatcherCacher WatcherCacher { get; }
private IMovieSender MovieSender { get; }
private IRepository<PlexContent> PlexContentRepository { get; }
@ -157,14 +169,23 @@ namespace Ombi.UI.Modules
private IAnalytics Analytics { get; }
private ITransientFaultQueue FaultQueue { get; }
private IRepository<RequestLimit> RequestLimitRepo { get; }
private IRadarrCacher RadarrCacher { get; }
private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Negotiator> RequestLoad()
{
var settings = await PrService.GetSettingsAsync();
var custom = await CustomizationSettings.GetSettingsAsync();
var searchViewModel = new SearchLoadViewModel
{
Settings = settings,
CustomizationSettings = custom
};
return View["Search/Index", settings];
return View["Search/Index", searchViewModel];
}
private async Task<Response> UpcomingMovies()
@ -188,6 +209,17 @@ namespace Ombi.UI.Modules
return await ProcessMovies(MovieSearchType.Search, searchTerm);
}
private Response GetTvPoster(int theTvDbId)
{
var result = TvApi.ShowLookupByTheTvDbId(theTvDbId);
var banner = result.image?.medium;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
return banner;
}
private async Task<Response> ProcessMovies(MovieSearchType searchType, string searchTerm)
{
List<MovieResult> apiMovies;
@ -236,22 +268,13 @@ namespace Ombi.UI.Modules
var cpCached = CpCacher.QueuedIds();
var watcherCached = WatcherCacher.QueuedIds();
var radarrCached = RadarrCacher.QueuedIds();
var content = PlexContentRepository.GetAll();
var plexMovies = Checker.GetPlexMovies(content);
var viewMovies = new List<SearchMovieViewModel>();
var counter = 0;
foreach (var movie in apiMovies)
{
var imdbId = string.Empty;
if (counter <= 5) // Let's only do it for the first 5 items
{
var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false);
// TODO needs to be careful about this, it's adding extra time to search...
// https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2
imdbId = movieInfoTask?.ImdbId;
counter++;
}
var viewMovie = new SearchMovieViewModel
{
Adult = movie.Adult,
@ -269,6 +292,28 @@ namespace Ombi.UI.Modules
VoteAverage = movie.VoteAverage,
VoteCount = movie.VoteCount
};
var imdbId = string.Empty;
if (counter <= 5) // Let's only do it for the first 5 items
{
var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id);
// TODO needs to be careful about this, it's adding extra time to search...
// https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2
viewMovie.ImdbId = movieInfo?.imdb_id;
viewMovie.Homepage = movieInfo?.homepage;
var videoId = movieInfo?.video ?? false
? movieInfo?.videos?.results?.FirstOrDefault()?.key
: string.Empty;
viewMovie.Trailer = string.IsNullOrEmpty(videoId)
? string.Empty
: $"https://www.youtube.com/watch?v={videoId}";
counter++;
}
var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies);
var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(),
imdbId);
@ -287,13 +332,19 @@ namespace Ombi.UI.Modules
}
else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db
{
viewMovie.Approved = true;
viewMovie.Requested = true;
}
else if(watcherCached.Contains(imdbId) && canSee) // compare to the watcher db
{
viewMovie.Approved = true;
viewMovie.Requested = true;
}
else if (radarrCached.Contains(movie.Id) && canSee)
{
viewMovie.Approved = true;
viewMovie.Requested = true;
}
viewMovies.Add(viewMovie);
}
@ -312,6 +363,186 @@ namespace Ombi.UI.Modules
return true;
}
private async Task<Response> ProcessShows(ShowSearchType type)
{
var shows = new List<SearchTvShowViewModel>();
var prSettings = await PrService.GetSettingsAsync();
switch (type)
{
case ShowSearchType.Popular:
Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies));
var popularShows = await TraktApi.GetPopularShows();
foreach (var popularShow in popularShows)
{
var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString());
var model = new SearchTvShowViewModel
{
FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"),
Id = theTvDbId,
ImdbId = popularShow.Ids.Imdb,
Network = popularShow.Network,
Overview = popularShow.Overview.RemoveHtml(),
Rating = popularShow.Rating.ToString(),
Runtime = popularShow.Runtime.ToString(),
SeriesName = popularShow.Title,
Status = popularShow.Status.DisplayName,
DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode,
DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason,
EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason),
Trailer = popularShow.Trailer,
Homepage = popularShow.Homepage
};
shows.Add(model);
}
shows = await MapToTvModel(shows, prSettings);
break;
case ShowSearchType.Anticipated:
Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies));
var anticipated = await TraktApi.GetAnticipatedShows();
foreach (var anticipatedShow in anticipated)
{
var show = anticipatedShow.Show;
var theTvDbId = int.Parse(show.Ids.Tvdb.ToString());
var model = new SearchTvShowViewModel
{
FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"),
Id = theTvDbId,
ImdbId = show.Ids.Imdb,
Network = show.Network ?? string.Empty,
Overview = show.Overview?.RemoveHtml() ?? string.Empty,
Rating = show.Rating.ToString(),
Runtime = show.Runtime.ToString(),
SeriesName = show.Title,
Status = show.Status?.DisplayName ?? string.Empty,
DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode,
DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason,
EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason),
Trailer = show.Trailer,
Homepage = show.Homepage
};
shows.Add(model);
}
shows = await MapToTvModel(shows, prSettings);
break;
case ShowSearchType.MostWatched:
Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies));
var mostWatched = await TraktApi.GetMostWatchesShows();
foreach (var watched in mostWatched)
{
var show = watched.Show;
var theTvDbId = int.Parse(show.Ids.Tvdb.ToString());
var model = new SearchTvShowViewModel
{
FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"),
Id = theTvDbId,
ImdbId = show.Ids.Imdb,
Network = show.Network,
Overview = show.Overview.RemoveHtml(),
Rating = show.Rating.ToString(),
Runtime = show.Runtime.ToString(),
SeriesName = show.Title,
Status = show.Status.DisplayName,
DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode,
DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason,
EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason),
Trailer = show.Trailer,
Homepage = show.Homepage
};
shows.Add(model);
}
shows = await MapToTvModel(shows, prSettings);
break;
case ShowSearchType.Trending:
Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies));
var trending = await TraktApi.GetTrendingShows();
foreach (var watched in trending)
{
var show = watched.Show;
var theTvDbId = int.Parse(show.Ids.Tvdb.ToString());
var model = new SearchTvShowViewModel
{
FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"),
Id = theTvDbId,
ImdbId = show.Ids.Imdb,
Network = show.Network,
Overview = show.Overview.RemoveHtml(),
Rating = show.Rating.ToString(),
Runtime = show.Runtime.ToString(),
SeriesName = show.Title,
Status = show.Status.DisplayName,
DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode,
DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason,
EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason),
Trailer = show.Trailer,
Homepage = show.Homepage
};
shows.Add(model);
}
shows = await MapToTvModel(shows, prSettings);
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
return Response.AsJson(shows);
}
private async Task<List<SearchTvShowViewModel>> MapToTvModel(List<SearchTvShowViewModel> shows, PlexRequestSettings prSettings)
{
var plexSettings = await PlexService.GetSettingsAsync();
var providerId = string.Empty;
// Get the requests
var allResults = await RequestService.GetAllAsync();
allResults = allResults.Where(x => x.Type == RequestType.TvShow);
var distinctResults = allResults.DistinctBy(x => x.ProviderId);
var dbTv = distinctResults.ToDictionary(x => x.ProviderId);
// Check the external applications
var sonarrCached = SonarrCacher.QueuedIds().ToList();
var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays
var content = PlexContentRepository.GetAll();
var plexTvShows = Checker.GetPlexTvShows(content).ToList();
foreach (var show in shows)
{
if (plexSettings.AdvancedSearch)
{
providerId = show.Id.ToString();
}
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4),
providerId);
if (plexShow != null)
{
show.Available = true;
show.PlexUrl = plexShow.Url;
}
else
{
if (dbTv.ContainsKey(show.Id))
{
var dbt = dbTv[show.Id];
show.Requested = true;
show.Episodes = dbt.Episodes.ToList();
show.Approved = dbt.Approved;
}
if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id))
// compare to the sonarr/sickrage db
{
show.Requested = true;
}
}
}
return shows;
}
private async Task<Response> SearchTvShow(string searchTerm)
{
@ -345,6 +576,10 @@ namespace Ombi.UI.Modules
var viewTv = new List<SearchTvShowViewModel>();
foreach (var t in apiTv)
{
if (!(t.show.externals?.thetvdb.HasValue) ?? false)
{
continue;
}
var banner = t.show.image?.medium;
if (!string.IsNullOrEmpty(banner))
{
@ -574,7 +809,17 @@ namespace Ombi.UI.Modules
if (result.Result)
{
return await AddRequest(model, settings,
$"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
$"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
if (result.Error)
{
return
Response.AsJson(new JsonResponseModel
{
Message = "Could not add movie, please contract your administrator",
Result = false
});
}
if (!result.MovieSendingEnabled)
{
@ -679,11 +924,13 @@ namespace Ombi.UI.Modules
DateTime.TryParse(showInfo.premiered, out firstAir);
string fullShowName = $"{showInfo.name} ({firstAir.Year})";
// For some reason the poster path is always http
var posterPath = showInfo.image?.medium.Replace("http:", "https:");
var model = new RequestedModel
{
Type = RequestType.TvShow,
Overview = showInfo.summary.RemoveHtml(),
PosterPath = showInfo.image?.medium,
PosterPath = posterPath,
Title = showInfo.name,
ReleaseDate = firstAir,
Status = showInfo.status,
@ -762,6 +1009,11 @@ namespace Ombi.UI.Modules
existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty<EpisodesModel>());
// It's technically a new request now, so set the status to not approved.
var autoApprove = ShouldAutoApprove(RequestType.TvShow);
if (autoApprove)
{
return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings);
}
existingRequest.Approved = false;
return await AddUserToRequest(existingRequest, settings, fullShowName, true);
@ -888,54 +1140,7 @@ namespace Ombi.UI.Modules
{
if (ShouldAutoApprove(RequestType.TvShow))
{
model.Approved = true;
var s = await sonarrSettings;
var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back
if (s.Enabled)
{
var result = await sender.SendToSonarr(s, model);
if (!string.IsNullOrEmpty(result?.title))
{
if (existingRequest != null)
{
return await UpdateRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
await
AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
Log.Debug("Error with sending to sonarr.");
return
Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List<string>()));
}
var srSettings = SickRageService.GetSettings();
if (srSettings.Enabled)
{
var result = sender.SendToSickRage(srSettings, model);
if (result?.result == "success")
{
return await AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = result?.message ?? Resources.UI.Search_SickrageError
});
}
if (!srSettings.Enabled && !s.Enabled)
{
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp });
return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings);
}
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
@ -1385,5 +1590,64 @@ namespace Ombi.UI.Modules
return false;
}
}
private enum ShowSearchType
{
Popular,
Anticipated,
MostWatched,
Trending
}
private async Task<Response> SendTv(RequestedModel model, Task<SonarrSettings> sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings)
{
model.Approved = true;
var s = await sonarrSettings;
var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back
if (s.Enabled)
{
var result = await sender.SendToSonarr(s, model);
if (!string.IsNullOrEmpty(result?.title))
{
if (existingRequest != null)
{
return await UpdateRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
await
AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
Log.Debug("Error with sending to sonarr.");
return
Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List<string>()));
}
var srSettings = SickRageService.GetSettings();
if (srSettings.Enabled)
{
var result = sender.SendToSickRage(srSettings, model);
if (result?.result == "success")
{
return await AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = result?.message ?? Resources.UI.Search_SickrageError
});
}
if (!srSettings.Enabled && !s.Enabled)
{
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp });
}
}
}

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

Loading…
Cancel
Save