Merge pull request #3678 from tidusjar/develop

Develop
This commit is contained in:
Jamie 2020-07-20 19:48:01 +01:00 committed by GitHub
commit 6162e438e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 654 additions and 136 deletions

View file

@ -75,6 +75,7 @@ Supported notifications:
* Pushover * Pushover
* Mattermost * Mattermost
* Telegram * Telegram
* Webhook
### The difference between Version 3 and 2 ### The difference between Version 3 and 2

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Ombi.Api.Webhook
{
public interface IWebhookApi
{
Task PushAsync(string endpoint, string accessToken, IDictionary<string, string> parameters);
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version></Version>
<PackageVersion></PackageVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,40 @@
using Newtonsoft.Json.Serialization;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Ombi.Api.Webhook
{
public class WebhookApi : IWebhookApi
{
private static readonly CamelCasePropertyNamesContractResolver _nameResolver = new CamelCasePropertyNamesContractResolver();
public WebhookApi(IApi api)
{
_api = api;
}
private readonly IApi _api;
public async Task PushAsync(string baseUrl, string accessToken, IDictionary<string, string> parameters)
{
var request = new Request("/", baseUrl, HttpMethod.Post);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.AddHeader("Access-Token", accessToken);
}
var body = parameters.ToDictionary(
x => _nameResolver.GetResolvedPropertyName(x.Key),
x => x.Value
);
request.ApplicationJsonContentType();
request.AddJsonBody(body);
await _api.Request(request);
}
}
}

View file

@ -56,7 +56,7 @@ namespace Ombi.Core.Engine.Demo
{ {
continue; continue;
} }
retVal.Add(ProcessResult(tvMazeSearch)); retVal.Add(await ProcessResult(tvMazeSearch));
} }
return retVal; return retVal;
} }
@ -78,7 +78,7 @@ namespace Ombi.Core.Engine.Demo
} }
var movieResult = await TvMazeApi.ShowLookup(tv); var movieResult = await TvMazeApi.ShowLookup(tv);
responses.Add(ProcessResult(movieResult)); responses.Add(await ProcessResult(movieResult));
} }
return responses; return responses;

View file

@ -61,7 +61,7 @@ namespace Ombi.Core.Engine
{ {
continue; continue;
} }
retVal.Add(ProcessResult(tvMazeSearch)); retVal.Add(await ProcessResult(tvMazeSearch));
} }
return retVal; return retVal;
} }
@ -123,7 +123,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Popular() public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{ {
var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
@ -131,37 +131,38 @@ namespace Ombi.Core.Engine
{ {
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches() public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches()
{ {
var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Trending() public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{ {
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = ProcessResults(result); var processed = await ProcessResults(result);
return processed; return processed;
} }
protected IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items) protected async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults<T>(IEnumerable<T> items)
{ {
var retVal = new List<SearchTvShowViewModel>(); var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items) foreach (var tvMazeSearch in items)
{ {
retVal.Add(ProcessResult(tvMazeSearch)); retVal.Add(await ProcessResult(tvMazeSearch));
} }
return retVal; return retVal;
} }
protected SearchTvShowViewModel ProcessResult<T>(T tvMazeSearch) protected async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch)
{ {
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch); var viewTv = Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
return await ProcessResult(viewTv);
} }
private async Task<SearchTvShowViewModel> ProcessResult(SearchTvShowViewModel item) private async Task<SearchTvShowViewModel> ProcessResult(SearchTvShowViewModel item)

View file

@ -7,6 +7,8 @@ namespace Ombi.Core.Models.Search
{ {
public int Id { get; set; } public int Id { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public bool? Denied { get; set; }
public string DeniedReason { get; set; }
public bool Requested { get; set; } public bool Requested { get; set; }
public int RequestId { get; set; } public int RequestId { get; set; }
public bool Available { get; set; } public bool Available { get; set; }

View file

@ -0,0 +1,15 @@

using System.Collections.Generic;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
/// <summary>
/// The view model for the notification settings page
/// </summary>
/// <seealso cref="WebhookSettings" />
public class WebhookNotificationViewModel : WebhookSettings
{
}
}

View file

@ -34,6 +34,8 @@ namespace Ombi.Core.Rule.Rules.Search
obj.Requested = true; obj.Requested = true;
obj.RequestId = movieRequests.Id; obj.RequestId = movieRequests.Id;
obj.Approved = movieRequests.Approved; obj.Approved = movieRequests.Approved;
obj.Denied = movieRequests.Denied ?? false;
obj.DeniedReason = movieRequests.DeniedReason;
obj.Available = movieRequests.Available; obj.Available = movieRequests.Available;
return Success(); return Success();
@ -60,6 +62,7 @@ namespace Ombi.Core.Rule.Rules.Search
request.Requested = true; request.Requested = true;
request.Approved = tvRequests.ChildRequests.Any(x => x.Approved); request.Approved = tvRequests.ChildRequests.Any(x => x.Approved);
request.Denied = tvRequests.ChildRequests.Any(x => x.Denied ?? false);
// Let's modify the seasonsrequested to reflect what we have requested... // Let's modify the seasonsrequested to reflect what we have requested...
foreach (var season in request.SeasonRequests) foreach (var season in request.SeasonRequests)
@ -108,6 +111,8 @@ namespace Ombi.Core.Rule.Rules.Search
obj.Requested = true; obj.Requested = true;
obj.RequestId = albumRequest.Id; obj.RequestId = albumRequest.Id;
obj.Approved = albumRequest.Approved; obj.Approved = albumRequest.Approved;
obj.Denied = albumRequest.Denied;
obj.DeniedReason = albumRequest.DeniedReason;
obj.Available = albumRequest.Available; obj.Available = albumRequest.Available;
return Success(); return Success();

View file

@ -32,6 +32,7 @@ using Ombi.Api.DogNzb;
using Ombi.Api.FanartTv; using Ombi.Api.FanartTv;
using Ombi.Api.Github; using Ombi.Api.Github;
using Ombi.Api.Gotify; using Ombi.Api.Gotify;
using Ombi.Api.Webhook;
using Ombi.Api.Lidarr; using Ombi.Api.Lidarr;
using Ombi.Api.Mattermost; using Ombi.Api.Mattermost;
using Ombi.Api.Notifications; using Ombi.Api.Notifications;
@ -122,6 +123,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IFanartTvApi, FanartTvApi>(); services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>(); services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IGotifyApi, GotifyApi>(); services.AddTransient<IGotifyApi, GotifyApi>();
services.AddTransient<IWebhookApi, WebhookApi>();
services.AddTransient<IMattermostApi, MattermostApi>(); services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>(); services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
services.AddTransient<IDogNzbApi, DogNzbApi>(); services.AddTransient<IDogNzbApi, DogNzbApi>();
@ -173,6 +175,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMattermostNotification, MattermostNotification>(); services.AddTransient<IMattermostNotification, MattermostNotification>();
services.AddTransient<IPushoverNotification, PushoverNotification>(); services.AddTransient<IPushoverNotification, PushoverNotification>();
services.AddTransient<IGotifyNotification, GotifyNotification>(); services.AddTransient<IGotifyNotification, GotifyNotification>();
services.AddTransient<IWebhookNotification, WebhookNotification>();
services.AddTransient<ITelegramNotification, TelegramNotification>(); services.AddTransient<ITelegramNotification, TelegramNotification>();
services.AddTransient<IMobileNotification, MobileNotification>(); services.AddTransient<IMobileNotification, MobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>(); services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();

View file

@ -35,6 +35,7 @@
<ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" /> <ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" />
<ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" /> <ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" /> <ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Api.Webhook\Ombi.Api.Webhook.csproj" />
<ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" /> <ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" /> <ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />

View file

@ -0,0 +1,46 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Helpers.Tests
{
[TestFixture]
public class EmbyHelperTests
{
[TestCaseSource(nameof(UrlData))]
public string TestUrl(string mediaId, string url)
{
return EmbyHelper.GetEmbyMediaUrl(mediaId, url);
}
[TestCaseSource(nameof(JellyfinUrlData))]
public string TestJellyfinUrl(string mediaId, string url)
{
return EmbyHelper.GetEmbyMediaUrl(mediaId, url, true);
}
public static IEnumerable<TestCaseData> UrlData
{
get
{
var mediaId = 1;
yield return new TestCaseData(mediaId.ToString(), "http://google.com").Returns($"http://google.com/#!/item?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash");
yield return new TestCaseData(mediaId.ToString(), "http://google.com/").Returns($"http://google.com/#!/item?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain");
yield return new TestCaseData(mediaId.ToString(), "https://google.com/").Returns($"https://google.com/#!/item?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithCustomDomain_Https");
yield return new TestCaseData(mediaId.ToString(), string.Empty).Returns($"https://app.emby.media/#!/item?id={mediaId}").SetName("EmbyHelper_GetMediaUrl_WithOutCustomDomain");
}
}
public static IEnumerable<TestCaseData> JellyfinUrlData
{
get
{
var mediaId = 1;
yield return new TestCaseData(mediaId.ToString(), "http://google.com").Returns($"http://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash");
yield return new TestCaseData(mediaId.ToString(), "http://google.com/").Returns($"http://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain");
yield return new TestCaseData(mediaId.ToString(), "https://google.com/").Returns($"https://google.com/#!/itemdetails.html?id={mediaId}").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_Https");
}
}
}
}

View file

@ -39,6 +39,8 @@ namespace Ombi.Helpers.Tests
yield return new TestCaseData("com.plexapp.agents.agent47://tt2543456?lang=en", ProviderIdType.Imdb).Returns("tt2543456").SetName("Unknown IMDB agent"); yield return new TestCaseData("com.plexapp.agents.agent47://tt2543456?lang=en", ProviderIdType.Imdb).Returns("tt2543456").SetName("Unknown IMDB agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/1/1?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent"); yield return new TestCaseData("com.plexapp.agents.agent47://456822/1/1?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/999/999?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent, large episode and season"); yield return new TestCaseData("com.plexapp.agents.agent47://456822/999/999?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent, large episode and season");
yield return new TestCaseData("com.plexapp.agents.xbmcnfotv://153021/2/1?lang=xn", ProviderIdType.TvDb).Returns("153021").SetName("xmbc agent, tv episode");
yield return new TestCaseData("com.plexapp.agents.xbmcnfotv://153021?lang=xn", ProviderIdType.TvDb).Returns("153021").SetName("xmbc agent, tv show");
} }
} }

View file

@ -4,18 +4,22 @@
{ {
public static string GetEmbyMediaUrl(string mediaId, string customerServerUrl = null, bool isJellyfin = false) public static string GetEmbyMediaUrl(string mediaId, string customerServerUrl = null, bool isJellyfin = false)
{ {
string path = "item/item"; string path = "item";
if (isJellyfin) if (isJellyfin)
{ {
path = "itemdetails"; path = "itemdetails.html";
} }
if (customerServerUrl.HasValue()) if (customerServerUrl.HasValue())
{ {
return $"{customerServerUrl}#!/{path}.html?id={mediaId}"; if (!customerServerUrl.EndsWith("/"))
{
return $"{customerServerUrl}/#!/{path}?id={mediaId}";
}
return $"{customerServerUrl}#!/{path}?id={mediaId}";
} }
else else
{ {
return $"https://app.emby.media/#!/{path}.html?id={mediaId}"; return $"https://app.emby.media/#!/{path}?id={mediaId}";
} }
} }
} }

View file

@ -33,6 +33,7 @@ namespace Ombi.Helpers
public static EventId PushoverNotification => new EventId(4005); public static EventId PushoverNotification => new EventId(4005);
public static EventId TelegramNotifcation => new EventId(4006); public static EventId TelegramNotifcation => new EventId(4006);
public static EventId GotifyNotification => new EventId(4007); public static EventId GotifyNotification => new EventId(4007);
public static EventId WebhookNotification => new EventId(4008);
public static EventId TvSender => new EventId(5000); public static EventId TvSender => new EventId(5000);
public static EventId SonarrSender => new EventId(5001); public static EventId SonarrSender => new EventId(5001);

View file

@ -11,5 +11,6 @@
Mattermost = 6, Mattermost = 6,
Mobile = 7, Mobile = 7,
Gotify = 8, Gotify = 8,
Webhook = 9,
} }
} }

View file

@ -33,14 +33,15 @@ namespace Ombi.Helpers
{ {
public class PlexHelper public class PlexHelper
{ {
private const string ImdbMatchExpression = "tt([0-9]{1,10})"; private const string ImdbMatchExpression = "tt([0-9]{1,10})";
private const string TvDbIdMatchExpression = "//[0-9]+/([0-9]{1,3})/([0-9]{1,3})"; private const string TvDbIdMatchExpression = "//[0-9]+/?([0-9]{1,3})/?([0-9]{1,3})";
public static ProviderId GetProviderIdFromPlexGuid(string guid) public static ProviderId GetProviderIdFromPlexGuid(string guid)
{ {
//com.plexapp.agents.thetvdb://269586/2/8?lang=en //com.plexapp.agents.thetvdb://269586/2/8?lang=en
//com.plexapp.agents.themoviedb://390043?lang=en //com.plexapp.agents.themoviedb://390043?lang=en
//com.plexapp.agents.imdb://tt2543164?lang=en //com.plexapp.agents.imdb://tt2543164?lang=en
// https://github.com/tidusjar/Ombi/issues/3277
if (string.IsNullOrEmpty(guid)) if (string.IsNullOrEmpty(guid))
{ {
return new ProviderId(); return new ProviderId();
@ -55,7 +56,7 @@ namespace Ombi.Helpers
{ {
TheTvDb = guidSplit[1] TheTvDb = guidSplit[1]
}; };
} else }
if (guid.Contains("themoviedb", CompareOptions.IgnoreCase)) if (guid.Contains("themoviedb", CompareOptions.IgnoreCase))
{ {
return new ProviderId return new ProviderId
@ -63,7 +64,6 @@ namespace Ombi.Helpers
TheMovieDb = guidSplit[1] TheMovieDb = guidSplit[1]
}; };
} }
else
if (guid.Contains("imdb", CompareOptions.IgnoreCase)) if (guid.Contains("imdb", CompareOptions.IgnoreCase))
{ {
return new ProviderId return new ProviderId
@ -71,74 +71,31 @@ namespace Ombi.Helpers
ImdbId = guidSplit[1] ImdbId = guidSplit[1]
}; };
} }
else
var imdbRegex = new Regex(ImdbMatchExpression, RegexOptions.Compiled);
var tvdbRegex = new Regex(TvDbIdMatchExpression, RegexOptions.Compiled);
var imdbMatch = imdbRegex.IsMatch(guid);
if (imdbMatch)
{ {
var imdbRegex = new Regex(ImdbMatchExpression, RegexOptions.Compiled); return new ProviderId
var tvdbRegex = new Regex(TvDbIdMatchExpression, RegexOptions.Compiled);
var imdbMatch = imdbRegex.IsMatch(guid);
if (imdbMatch)
{ {
return new ProviderId ImdbId = guidSplit[1]
{ };
ImdbId = guidSplit[1] }
};
} // Check if it matches the TvDb pattern
else var tvdbMatch = tvdbRegex.IsMatch(guid);
if (tvdbMatch)
{
return new ProviderId
{ {
// Check if it matches the TvDb pattern TheTvDb = guidSplit[1]
var tvdbMatch = tvdbRegex.IsMatch(guid); };
if (tvdbMatch)
{
return new ProviderId
{
TheTvDb = guidSplit[1]
};
}
}
} }
} }
return new ProviderId(); return new ProviderId();
} }
public static EpisodeModelHelper GetSeasonsAndEpisodesFromPlexGuid(string guid)
{
var ep = new EpisodeModelHelper();
//com.plexapp.agents.thetvdb://269586/2/8?lang=en
//com.plexapp.agents.themoviedb://390043?lang=en
//com.plexapp.agents.imdb://tt2543164?lang=en
if (string.IsNullOrEmpty(guid))
return null;
try
{
var guidSplit = guid.Split(new[] { '/', '?' }, StringSplitOptions.RemoveEmptyEntries);
if (guidSplit.Length > 2)
{
if (guid.Contains("thetvdb", CompareOptions.IgnoreCase))
{
ep.ProviderId = new ProviderId {TheTvDb = guidSplit[1]};
}
if (guid.Contains("themoviedb", CompareOptions.IgnoreCase))
{
ep.ProviderId = new ProviderId { TheMovieDb = guidSplit[1] };
}
if (guid.Contains("imdb", CompareOptions.IgnoreCase))
{
ep.ProviderId = new ProviderId { ImdbId = guidSplit[1] };
}
ep.SeasonNumber = int.Parse(guidSplit[2]);
ep.EpisodeNumber = int.Parse(guidSplit[3]);
}
return ep;
}
catch (Exception)
{
return ep;
}
}
public static string GetPlexMediaUrl(string machineId, int mediaId) public static string GetPlexMediaUrl(string machineId, int mediaId)
{ {
var url = var url =
@ -147,13 +104,6 @@ namespace Ombi.Helpers
} }
} }
public class EpisodeModelHelper
{
public ProviderId ProviderId { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
}
public class ProviderId public class ProviderId
{ {
public string TheTvDb { get; set; } public string TheTvDb { get; set; }

View file

@ -20,6 +20,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap(); CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap(); CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap(); CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap();
CreateMap<WebhookNotificationViewModel, WebhookSettings>().ReverseMap();
} }
} }
} }

View file

@ -0,0 +1,6 @@
namespace Ombi.Notifications.Agents
{
public interface IWebhookNotification : INotification
{
}
}

View file

@ -0,0 +1,123 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Webhook;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Notifications.Agents
{
public class WebhookNotification : BaseNotification<WebhookSettings>, IWebhookNotification
{
public WebhookNotification(IWebhookApi api, ISettingsService<WebhookSettings> sn, ILogger<WebhookNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;
}
public override string NotificationName => "WebhookNotification";
private IWebhookApi Api { get; }
private ILogger<WebhookNotification> Logger { get; }
protected override bool ValidateConfiguration(WebhookSettings settings)
{
return settings.Enabled && !string.IsNullOrEmpty(settings.WebhookUrl);
}
protected override async Task NewRequest(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, WebhookSettings settings)
{
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, WebhookSettings settings)
{
try
{
await Api.PushAsync(settings.WebhookUrl, settings.ApplicationToken, model.Data);
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.WebhookNotification, e, "Failed to send webhook notification");
}
}
protected override async Task Test(NotificationOptions model, WebhookSettings settings)
{
var c = new NotificationMessageCurlys();
var testData = c.Curlys.ToDictionary(x => x.Key, x => x.Value);
testData[nameof(NotificationType)] = NotificationType.Test.ToString();
var notification = new NotificationMessage
{
Data = testData,
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, WebhookSettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Webhook, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Webhook}");
return;
}
var notificationData = parsed.Data.ToDictionary(x => x.Key, x => x.Value);
notificationData[nameof(NotificationType)] = type.ToString();
var notification = new NotificationMessage
{
Data = notificationData,
};
await Send(notification, settings);
}
}
}

View file

@ -9,5 +9,6 @@ namespace Ombi.Notifications.Models
public string To { get; set; } public string To { get; set; }
public Dictionary<string, string> Other { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> Other { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> Data { get; set; }
} }
} }

View file

@ -1,4 +1,6 @@
namespace Ombi.Notifications using System.Collections.Generic;
namespace Ombi.Notifications
{ {
public class NotificationMessageContent public class NotificationMessageContent
{ {
@ -6,5 +8,6 @@
public string Subject { get; set; } public string Subject { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string Image { get; set; } public string Image { get; set; }
public IReadOnlyDictionary<string, string> Data { get; set; }
} }
} }

View file

@ -18,6 +18,8 @@ namespace Ombi.Notifications
{ {
LoadIssues(opts); LoadIssues(opts);
RequestId = req?.Id.ToString();
string title; string title;
if (req == null) if (req == null)
{ {
@ -68,6 +70,8 @@ namespace Ombi.Notifications
{ {
LoadIssues(opts); LoadIssues(opts);
RequestId = req?.Id.ToString();
string title; string title;
if (req == null) if (req == null)
{ {
@ -114,6 +118,9 @@ namespace Ombi.Notifications
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref) public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref)
{ {
LoadIssues(opts); LoadIssues(opts);
RequestId = req?.ParentRequestId.ToString();
string title; string title;
if (req == null) if (req == null)
{ {
@ -216,6 +223,7 @@ namespace Ombi.Notifications
} }
// User Defined // User Defined
public string RequestId { get; set; }
public string RequestedUser { get; set; } public string RequestedUser { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string IssueUser => UserName; public string IssueUser => UserName;
@ -248,6 +256,7 @@ namespace Ombi.Notifications
public Dictionary<string, string> Curlys => new Dictionary<string, string> public Dictionary<string, string> Curlys => new Dictionary<string, string>
{ {
{nameof(RequestId), RequestId },
{nameof(RequestedUser), RequestedUser }, {nameof(RequestedUser), RequestedUser },
{nameof(Title), Title }, {nameof(Title), Title },
{nameof(RequestedDate), RequestedDate }, {nameof(RequestedDate), RequestedDate },

View file

@ -47,7 +47,7 @@ namespace Ombi.Notifications
body = ReplaceFields(bodyFields, parameters, body); body = ReplaceFields(bodyFields, parameters, body);
subject = ReplaceFields(subjectFields, parameters, subject); subject = ReplaceFields(subjectFields, parameters, subject);
return new NotificationMessageContent { Message = body ?? string.Empty, Subject = subject ?? string.Empty}; return new NotificationMessageContent { Message = body ?? string.Empty, Subject = subject ?? string.Empty, Data = parameters };
} }
public IEnumerable<string> ProcessConditions(IEnumerable<string> conditionalFields, IReadOnlyDictionary<string, string> parameters) public IEnumerable<string> ProcessConditions(IEnumerable<string> conditionalFields, IReadOnlyDictionary<string, string> parameters)

View file

@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" /> <ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" />
<ProjectReference Include="..\Ombi.Api.Gotify\Ombi.Api.Gotify.csproj" /> <ProjectReference Include="..\Ombi.Api.Gotify\Ombi.Api.Gotify.csproj" />
<ProjectReference Include="..\Ombi.Api.Webhook\Ombi.Api.Webhook.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" /> <ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" /> <ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />

View file

@ -730,7 +730,7 @@ namespace Ombi.Schedule.Jobs.Ombi
finalsb.Append("<br />"); finalsb.Append("<br />");
} }
var summary = info.summary; var summary = info?.summary ?? string.Empty;
if (summary.Length > 280) if (summary.Length > 280)
{ {
summary = summary.Remove(280); summary = summary.Remove(280);
@ -946,4 +946,4 @@ namespace Ombi.Schedule.Jobs.Ombi
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }
} }

View file

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
@ -67,13 +69,13 @@ namespace Ombi.Schedule.Jobs.Ombi
private async Task StartPlex() private async Task StartPlex()
{ {
// Ensure we check that we have not linked this item to a request // Ensure we check that we have not linked this item to a request
var allMovies = _plexRepo.GetAll().Where(x => var allMovies = await _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Movie && !x.RequestId.HasValue && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())); x.Type == PlexMediaTypeEntity.Movie && !x.RequestId.HasValue && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())).ToListAsync();
await StartPlexMovies(allMovies); await StartPlexMovies(allMovies);
// Now Tv // Now Tv
var allTv = _plexRepo.GetAll().Where(x => var allTv = await _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Show && !x.RequestId.HasValue && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())); x.Type == PlexMediaTypeEntity.Show && !x.RequestId.HasValue && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())).ToListAsync();
await StartPlexTv(allTv); await StartPlexTv(allTv);
} }
@ -83,7 +85,7 @@ namespace Ombi.Schedule.Jobs.Ombi
await StartEmbyTv(); await StartEmbyTv();
} }
private async Task StartPlexTv(IQueryable<PlexServerContent> allTv) private async Task StartPlexTv(List<PlexServerContent> allTv)
{ {
foreach (var show in allTv) foreach (var show in allTv)
{ {
@ -121,8 +123,8 @@ namespace Ombi.Schedule.Jobs.Ombi
private async Task StartEmbyTv() private async Task StartEmbyTv()
{ {
var allTv = _embyRepo.GetAll().Where(x => var allTv = await _embyRepo.GetAll().Where(x =>
x.Type == EmbyMediaType.Series && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())); x.Type == EmbyMediaType.Series && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue())).ToListAsync();;
foreach (var show in allTv) foreach (var show in allTv)
{ {
@ -154,7 +156,7 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
} }
private async Task StartPlexMovies(IQueryable<PlexServerContent> allMovies) private async Task StartPlexMovies(List<PlexServerContent> allMovies)
{ {
foreach (var movie in allMovies) foreach (var movie in allMovies)
{ {
@ -186,8 +188,8 @@ namespace Ombi.Schedule.Jobs.Ombi
private async Task StartEmbyMovies(EmbySettings settings) private async Task StartEmbyMovies(EmbySettings settings)
{ {
var allMovies = _embyRepo.GetAll().Where(x => var allMovies = await _embyRepo.GetAll().Where(x =>
x.Type == EmbyMediaType.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())); x.Type == EmbyMediaType.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue())).ToListAsync();
foreach (var movie in allMovies) foreach (var movie in allMovies)
{ {
movie.ImdbId.HasValue(); movie.ImdbId.HasValue();

View file

@ -0,0 +1,10 @@
using Ombi.Store.Entities;
namespace Ombi.Schedule.Jobs.Plex.Models
{
public class AvailabilityModel
{
public int Id { get; set; }
public string RequestedUser { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -6,6 +7,7 @@ using Microsoft.Extensions.Logging;
using Ombi.Core; using Ombi.Core;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Plex.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -47,13 +49,13 @@ namespace Ombi.Schedule.Jobs.Plex
private Task ProcessTv() private Task ProcessTv()
{ {
var tv = _tvRepo.GetChild().Where(x => !x.Available); var tv = _tvRepo.GetChild().Where(x => !x.Available).AsNoTracking();
return ProcessTv(tv); return ProcessTv(tv);
} }
private async Task ProcessTv(IQueryable<ChildRequests> tv) private async Task ProcessTv(IQueryable<ChildRequests> tv)
{ {
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series).AsNoTracking();
foreach (var child in tv) foreach (var child in tv)
{ {
@ -95,6 +97,7 @@ namespace Ombi.Schedule.Jobs.Plex
} }
var availableEpisode = new List<AvailabilityModel>();
foreach (var season in child.SeasonRequests) foreach (var season in child.SeasonRequests)
{ {
foreach (var episode in season.Episodes) foreach (var episode in season.Episodes)
@ -109,20 +112,28 @@ namespace Ombi.Schedule.Jobs.Plex
if (foundEp != null) if (foundEp != null)
{ {
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id
});
episode.Available = true; episode.Available = true;
} }
} }
} }
//TODO Partial avilability notifications here
foreach(var c in availableEpisode)
{
await _tvRepo.MarkEpisodeAsAvailable(c.Id);
}
// Check to see if all of the episodes in all seasons are available for this request // Check to see if all of the episodes in all seasons are available for this request
var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available)); var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable) if (allAvailable)
{ {
_log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}"); _log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
// We have ful-fulled this request! // We have ful-fulled this request!
child.Available = true; await _tvRepo.MarkChildAsAvailable(child.Id);
child.MarkedAsAvailable = DateTime.Now;
await _notificationService.Notify(new NotificationOptions await _notificationService.Notify(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
@ -140,10 +151,16 @@ namespace Ombi.Schedule.Jobs.Plex
private async Task ProcessMovies() private async Task ProcessMovies()
{ {
// Get all non available // Get all non available
var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available); var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available).AsNoTracking();
var itemsForAvailbility = new List<AvailabilityModel>();
foreach (var movie in movies) foreach (var movie in movies)
{ {
if (movie.Available)
{
return;
}
PlexServerContent item = null; PlexServerContent item = null;
if (movie.ImdbId.HasValue()) if (movie.ImdbId.HasValue())
{ {
@ -161,24 +178,29 @@ namespace Ombi.Schedule.Jobs.Plex
// We don't yet have this // We don't yet have this
continue; continue;
} }
movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;
item.RequestId = movie.Id;
_log.LogInformation("[PAC] - Movie request {0} is now available, sending notification", $"{movie.Title} - {movie.Id}"); _log.LogInformation("[PAC] - Movie request {0} is now available, sending notification", $"{movie.Title} - {movie.Id}");
itemsForAvailbility.Add(new AvailabilityModel
{
Id = movie.Id,
RequestedUser = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty
});
}
foreach (var i in itemsForAvailbility)
{
await _movieRepo.MarkAsAvailable(i.Id);
await _notificationService.Notify(new NotificationOptions await _notificationService.Notify(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable, NotificationType = NotificationType.RequestAvailable,
RequestId = movie.Id, RequestId = i.Id,
RequestType = RequestType.Movie, RequestType = RequestType.Movie,
Recipient = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty Recipient = i.RequestedUser
}); });
} }
await _movieRepo.Save();
await _repo.SaveChangesAsync(); await _repo.SaveChangesAsync();
} }

View file

@ -55,7 +55,7 @@ namespace Ombi.Schedule.Jobs.Plex
_log.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed"); _log.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed");
} }
//await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex"); await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex");
} }
@ -217,4 +217,4 @@ namespace Ombi.Schedule.Jobs.Plex
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }
} }

View file

@ -0,0 +1,9 @@
namespace Ombi.Settings.Settings.Models.Notifications
{
public class WebhookSettings : Settings
{
public bool Enabled { get; set; }
public string WebhookUrl { get; set; }
public string ApplicationToken { get; set; }
}
}

View file

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Store.Context namespace Ombi.Store.Context
{ {
@ -35,6 +36,7 @@ namespace Ombi.Store.Context
public DbSet<AlbumRequest> AlbumRequests { get; set; } public DbSet<AlbumRequest> AlbumRequests { get; set; }
public DbSet<TvRequests> TvRequests { get; set; } public DbSet<TvRequests> TvRequests { get; set; }
public DbSet<ChildRequests> ChildRequests { get; set; } public DbSet<ChildRequests> ChildRequests { get; set; }
public DbSet<EpisodeRequests> EpisodeRequests { get; set; }
public DbSet<Issues> Issues { get; set; } public DbSet<Issues> Issues { get; set; }
public DbSet<IssueCategory> IssueCategories { get; set; } public DbSet<IssueCategory> IssueCategories { get; set; }
@ -116,7 +118,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! Your request for {Title} on {ApplicationName}! This is now available! :)", Message = "Hello! Your request for {Title} on {ApplicationName} is now available.",
Subject = "{ApplicationName}: {Title} is now available!", Subject = "{ApplicationName}: {Title} is now available!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -138,7 +140,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! Your request for {Title} has been declined, Sorry!", Message = "Hello! Your request for {Title} has been declined.",
Subject = "{ApplicationName}: your request has been declined", Subject = "{ApplicationName}: your request has been declined",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -148,7 +150,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! The user '{UserName}' has requested {Title} but it could not be added. This has been added into the requests queue and will keep retrying", Message = "Hello! The user '{UserName}' has requested {Title} but it could not be added. This has been added into the requests queue and it will keep retrying",
Subject = "Item Added To Retry Queue", Subject = "Item Added To Retry Queue",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -158,7 +160,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! You have been invited to use {ApplicationName}! You can login here: {ApplicationUrl}", Message = "Hello! You have been invited to use {ApplicationName}. You can login here: {ApplicationUrl}",
Subject = "Invite to {ApplicationName}", Subject = "Invite to {ApplicationName}",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -168,7 +170,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello {UserName} Your issue for {Title} has now been resolved.", Message = "Hello {UserName}, your issue for {Title} has now been resolved.",
Subject = "{ApplicationName}: Issue has been resolved for {Title}!", Subject = "{ApplicationName}: Issue has been resolved for {Title}!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -179,7 +181,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello, There is a new comment on your issue {IssueSubject}, The comment is: {NewIssueComment}", Message = "Hello, There is a new comment on your issue {IssueSubject}. The comment is: {NewIssueComment}",
Subject = "{ApplicationName}: New comment on issue {IssueSubject}!", Subject = "{ApplicationName}: New comment on issue {IssueSubject}!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -191,7 +193,7 @@ namespace Ombi.Store.Context
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Here is a list of Movies and TV Shows that have recently been added!", Message = "Here is a list of Movies and TV Shows that have recently been added:",
Subject = "{ApplicationName}: Recently Added Content!", Subject = "{ApplicationName}: Recently Added Content!",
Agent = agent, Agent = agent,
Enabled = true, Enabled = true,
@ -215,4 +217,4 @@ namespace Ombi.Store.Context
} }
} }
} }
} }

View file

@ -10,6 +10,7 @@ namespace Ombi.Store.Repository.Requests
MovieRequests GetRequest(int theMovieDbId); MovieRequests GetRequest(int theMovieDbId);
Task Update(MovieRequests request); Task Update(MovieRequests request);
Task Save(); Task Save();
Task MarkAsAvailable(int id);
IQueryable<MovieRequests> GetWithUser(); IQueryable<MovieRequests> GetWithUser();
IQueryable<MovieRequests> GetWithUser(string userId); IQueryable<MovieRequests> GetWithUser(string userId);
IQueryable<MovieRequests> GetAll(string userId); IQueryable<MovieRequests> GetAll(string userId);

View file

@ -23,6 +23,8 @@ namespace Ombi.Store.Repository.Requests
Task UpdateChild(ChildRequests request); Task UpdateChild(ChildRequests request);
IQueryable<ChildRequests> GetChild(); IQueryable<ChildRequests> GetChild();
IQueryable<ChildRequests> GetChild(string userId); IQueryable<ChildRequests> GetChild(string userId);
Task MarkEpisodeAsAvailable(int id);
Task MarkChildAsAvailable(int id);
Task Save(); Task Save();
Task DeleteChildRange(IEnumerable<ChildRequests> request); Task DeleteChildRange(IEnumerable<ChildRequests> request);
} }

View file

@ -54,6 +54,14 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public async Task MarkAsAvailable(int id)
{
var movieRequest = new MovieRequests{ Id = id, Available = true, MarkedAsAvailable = DateTime.UtcNow};
var attached = Db.MovieRequests.Attach(movieRequest);
attached.Property(x => x.Available).IsModified = true;
attached.Property(x => x.MarkedAsAvailable).IsModified = true;
await Db.SaveChangesAsync();
}
public IQueryable<MovieRequests> GetWithUser(string userId) public IQueryable<MovieRequests> GetWithUser(string userId)
{ {

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -100,6 +101,23 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public async Task MarkChildAsAvailable(int id)
{
var request = new ChildRequests { Id = id, Available = true, MarkedAsAvailable = DateTime.UtcNow };
var attached = Db.ChildRequests.Attach(request);
attached.Property(x => x.Available).IsModified = true;
attached.Property(x => x.MarkedAsAvailable).IsModified = true;
await Db.SaveChangesAsync();
}
public async Task MarkEpisodeAsAvailable(int id)
{
var request = new EpisodeRequests { Id = id, Available = true };
var attached = Db.EpisodeRequests.Attach(request);
attached.Property(x => x.Available).IsModified = true;
await Db.SaveChangesAsync();
}
public async Task Save() public async Task Save()
{ {
await InternalSaveChanges(); await InternalSaveChanges();
@ -128,10 +146,10 @@ namespace Ombi.Store.Repository.Requests
public async Task Update(TvRequests request) public async Task Update(TvRequests request)
{ {
Db.Update(request); Db.Update(request);
await InternalSaveChanges(); await InternalSaveChanges();
} }
public async Task UpdateChild(ChildRequests request) public async Task UpdateChild(ChildRequests request)
{ {
Db.Update(request); Db.Update(request);

View file

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio Version 16
VisualStudioVersion = 15.0.27130.2027 VisualStudioVersion = 16.0.29519.87
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}"
EndProject EndProject
@ -100,6 +100,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Helpers.Tests", "Ombi.
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Gotify", "Ombi.Api.Gotify\Ombi.Api.Gotify.csproj", "{105EA346-766E-45B8-928B-DE6991DCB7EB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Gotify", "Ombi.Api.Gotify\Ombi.Api.Gotify.csproj", "{105EA346-766E-45B8-928B-DE6991DCB7EB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Webhook", "Ombi.Api.Webhook\Ombi.Api.Webhook.csproj", "{E2186FDA-D827-4781-8663-130AC382F12C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -262,6 +264,10 @@ Global
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.Build.0 = Release|Any CPU {105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.Build.0 = Release|Any CPU
{E2186FDA-D827-4781-8663-130AC382F12C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2186FDA-D827-4781-8663-130AC382F12C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2186FDA-D827-4781-8663-130AC382F12C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2186FDA-D827-4781-8663-130AC382F12C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -300,6 +306,7 @@ Global
{4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5} {4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{105EA346-766E-45B8-928B-DE6991DCB7EB} = {9293CA11-360A-4C20-A674-B9E794431BF5} {105EA346-766E-45B8-928B-DE6991DCB7EB} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{E2186FDA-D827-4781-8663-130AC382F12C} = {9293CA11-360A-4C20-A674-B9E794431BF5}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869} SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

View file

@ -101,6 +101,11 @@ export interface IGotifyNotificationSettings extends INotificationSettings {
priority: number; priority: number;
} }
export interface IWebhookNotificationSettings extends INotificationSettings {
webhookUrl: string;
applicationToken: string;
}
export interface IMattermostNotifcationSettings extends INotificationSettings { export interface IMattermostNotifcationSettings extends INotificationSettings {
webhookUrl: string; webhookUrl: string;
username: string; username: string;

View file

@ -68,7 +68,7 @@
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
<div> <div>
<a *ngIf="node.imdbId" href="{{node.imdbId}}" target="_blank"> <a *ngIf="node.imdbId" href="https://www.imdb.com/title/{{node.imdbId}}/" target="_blank">
<h4>{{node.title}} ({{node.firstAired | amLocal | amDateFormat: 'YYYY'}})</h4> <h4>{{node.title}} ({{node.firstAired | amLocal | amDateFormat: 'YYYY'}})</h4>
</a> </a>

View file

@ -24,6 +24,7 @@ import {
ISlackNotificationSettings, ISlackNotificationSettings,
ISonarrSettings, ISonarrSettings,
ITelegramNotifcationSettings, ITelegramNotifcationSettings,
IWebhookNotificationSettings,
} from "../../interfaces"; } from "../../interfaces";
@Injectable() @Injectable()
@ -48,6 +49,10 @@ export class TesterService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers }); return this.http.post<boolean>(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers });
} }
public webhookTest(settings: IWebhookNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}webhook`, JSON.stringify(settings), { headers: this.headers });
}
public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> { public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers});
} }

View file

@ -36,6 +36,7 @@ import {
IUpdateSettings, IUpdateSettings,
IUserManagementSettings, IUserManagementSettings,
IVoteSettings, IVoteSettings,
IWebhookNotificationSettings,
} from "../interfaces"; } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -192,6 +193,14 @@ export class SettingsService extends ServiceHelpers {
.post<boolean>(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers }); .post<boolean>(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers });
} }
public getWebhookNotificationSettings(): Observable<IWebhookNotificationSettings> {
return this.http.get<IWebhookNotificationSettings>(`${this.url}/notifications/webhook`, { headers: this.headers });
}
public saveWebhookNotificationSettings(settings: IWebhookNotificationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/webhook`, JSON.stringify(settings), { headers: this.headers });
}
public getSlackNotificationSettings(): Observable<ISlackNotificationSettings> { public getSlackNotificationSettings(): Observable<ISlackNotificationSettings> {
return this.http.get<ISlackNotificationSettings>(`${this.url}/notifications/slack`, {headers: this.headers}); return this.http.get<ISlackNotificationSettings>(`${this.url}/notifications/slack`, {headers: this.headers});
} }

View file

@ -57,6 +57,13 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="subDir" class="control-label">Base URL</label>
<div>
<input type="text" class="form-control-custom form-control" id="subDir" [(ngModel)]="server.subDir" value="{{server.subDir}}">
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="authToken" class="control-label">Emby Api Key</label> <label for="authToken" class="control-label">Emby Api Key</label>
<div class=""> <div class="">

View file

@ -0,0 +1,49 @@

<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Webhook Notifications</legend>
<div class="col-md-6">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enabled</label>
</div>
</div>
<div class="form-group">
<label for="baseUrl" class="control-label">Base URL</label>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}" formControlName="webhookUrl" pTooltip="Enter the URL of your webhook server.">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook URL is required</small>
</div>
<div class="form-group">
<label for="applicationToken" class="control-label">Application Token
<i class="fa fa-question-circle" pTooltip="Optional authentication token. Will be sent as 'Access-Token' header."></i>
</label>
<input type="text" class="form-control form-control-custom " id="applicationToken" name="applicationToken" [ngClass]="{'form-error': form.get('applicationToken').hasError('required')}" formControlName="applicationToken" pTooltip="Enter your Application token from Webhook.">
<small *ngIf="form.get('applicationToken').hasError('required')" class="error-text">The Application Token is required</small>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="button" (click)="test(form)" class="btn btn-primary-outline">
Test
<div id="spinner"></div>
</button>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</form>
</div>
</fieldset>
</div>

View file

@ -0,0 +1,64 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { INotificationTemplates, IWebhookNotificationSettings, NotificationType } from "../../interfaces";
import { TesterService } from "../../services";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
templateUrl: "./webhook.component.html",
})
export class WebhookComponent implements OnInit {
public NotificationType = NotificationType;
public templates: INotificationTemplates[];
public form: FormGroup;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private fb: FormBuilder,
private testerService: TesterService) { }
public ngOnInit() {
this.settingsService.getWebhookNotificationSettings().subscribe(x => {
this.form = this.fb.group({
enabled: [x.enabled],
webhookUrl: [x.webhookUrl, [Validators.required]],
applicationToken: [x.applicationToken],
});
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
const settings = <IWebhookNotificationSettings> form.value;
this.settingsService.saveWebhookNotificationSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Webhook settings");
} else {
this.notificationService.success("There was an error when saving the Webhook settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.testerService.webhookTest(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully sent a Webhook message");
} else {
this.notificationService.error("There was an error when sending the Webhook message. Please check your settings");
}
});
}
}

View file

@ -37,6 +37,7 @@ import { PushbulletComponent } from "./notifications/pushbullet.component";
import { PushoverComponent } from "./notifications/pushover.component"; import { PushoverComponent } from "./notifications/pushover.component";
import { SlackComponent } from "./notifications/slack.component"; import { SlackComponent } from "./notifications/slack.component";
import { TelegramComponent } from "./notifications/telegram.component"; import { TelegramComponent } from "./notifications/telegram.component";
import { WebhookComponent } from "./notifications/webhook.component";
import { OmbiComponent } from "./ombi/ombi.component"; import { OmbiComponent } from "./ombi/ombi.component";
import { PlexComponent } from "./plex/plex.component"; import { PlexComponent } from "./plex/plex.component";
import { RadarrComponent } from "./radarr/radarr.component"; import { RadarrComponent } from "./radarr/radarr.component";
@ -67,6 +68,7 @@ const routes: Routes = [
{ path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] }, { path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] },
{ path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] },
{ path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] },
{ path: "Webhook", component: WebhookComponent, canActivate: [AuthGuard] },
{ path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "Update", component: UpdateComponent, canActivate: [AuthGuard] }, { path: "Update", component: UpdateComponent, canActivate: [AuthGuard] },
@ -124,6 +126,7 @@ const routes: Routes = [
MattermostComponent, MattermostComponent,
PushbulletComponent, PushbulletComponent,
GotifyComponent, GotifyComponent,
WebhookComponent,
UserManagementComponent, UserManagementComponent,
UpdateComponent, UpdateComponent,
AboutComponent, AboutComponent,

View file

@ -76,6 +76,7 @@
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Mattermost']">Mattermost</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Mattermost']">Mattermost</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Telegram']">Telegram</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Telegram']">Telegram</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Gotify']">Gotify</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Gotify']">Gotify</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Webhook']">Webhook</a></li>
</ul> </ul>
</li> </li>

View file

@ -40,7 +40,7 @@ namespace Ombi.Controllers.External
IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm,
IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider, IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider,
ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification,
ILidarrApi lidarrApi, IGotifyNotification gotifyNotification) ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWebhookNotification webhookNotification)
{ {
Service = service; Service = service;
DiscordNotification = notification; DiscordNotification = notification;
@ -62,6 +62,7 @@ namespace Ombi.Controllers.External
MobileNotification = mobileNotification; MobileNotification = mobileNotification;
LidarrApi = lidarrApi; LidarrApi = lidarrApi;
GotifyNotification = gotifyNotification; GotifyNotification = gotifyNotification;
WebhookNotification = webhookNotification;
} }
private INotificationService Service { get; } private INotificationService Service { get; }
@ -71,6 +72,7 @@ namespace Ombi.Controllers.External
private ISlackNotification SlackNotification { get; } private ISlackNotification SlackNotification { get; }
private IPushoverNotification PushoverNotification { get; } private IPushoverNotification PushoverNotification { get; }
private IGotifyNotification GotifyNotification { get; } private IGotifyNotification GotifyNotification { get; }
private IWebhookNotification WebhookNotification { get; }
private IMattermostNotification MattermostNotification { get; } private IMattermostNotification MattermostNotification { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private IRadarrApi RadarrApi { get; } private IRadarrApi RadarrApi { get; }
@ -181,6 +183,30 @@ namespace Ombi.Controllers.External
} }
/// <summary>
/// Sends a test message to configured webhook using the provided settings
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("webhook")]
public bool Webhook([FromBody] WebhookSettings settings)
{
try
{
settings.Enabled = true;
WebhookNotification.NotifyAsync(
new NotificationOptions { NotificationType = NotificationType.Test, RequestId = -1 }, settings);
return true;
}
catch (Exception e)
{
Log.LogError(LoggingEvents.Api, e, "Could not test your webhook");
return false;
}
}
/// <summary> /// <summary>
/// Sends a test message to mattermost using the provided settings /// Sends a test message to mattermost using the provided settings
/// </summary> /// </summary>

View file

@ -1007,6 +1007,33 @@ namespace Ombi.Controllers
return model; return model;
} }
/// <summary>
/// Saves the webhook notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/webhook")]
public async Task<bool> WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model)
{
var settings = Mapper.Map<WebhookSettings>(model);
var result = await Save(settings);
return result;
}
/// <summary>
/// Gets the webhook notification settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/webhook")]
public async Task<WebhookNotificationViewModel> WebhookNotificationSettings()
{
var settings = await Get<WebhookSettings>();
var model = Mapper.Map<WebhookNotificationViewModel>(settings);
return model;
}
/// <summary> /// <summary>
/// Saves the Newsletter notification settings. /// Saves the Newsletter notification settings.
/// </summary> /// </summary>

View file

@ -55,9 +55,9 @@
"UpdateAvailableTooltip": "Mise à jour disponible !", "UpdateAvailableTooltip": "Mise à jour disponible !",
"Settings": "Paramètres", "Settings": "Paramètres",
"Welcome": "Bienvenue {{username}}", "Welcome": "Bienvenue {{username}}",
"UpdateDetails": "Détails de la mise à jour", "UpdateDetails": "Identifiant et mot de passe",
"Logout": "Déconnexion", "Logout": "Déconnexion",
"OpenMobileApp": "Ouvrir l'application mobile", "OpenMobileApp": "Ouvrir l'application",
"RecentlyAdded": "Ajouts récents" "RecentlyAdded": "Ajouts récents"
}, },
"Search": { "Search": {

View file

@ -75,7 +75,7 @@
"RequestAdded": "Kérés sikeresen leadva erre: {{title}}", "RequestAdded": "Kérés sikeresen leadva erre: {{title}}",
"Similar": "Hasonló", "Similar": "Hasonló",
"Refine": "Finomítás", "Refine": "Finomítás",
"SearchBarPlaceholder": "Type Here to Search", "SearchBarPlaceholder": "A kereséshez írj be valamit",
"Movies": { "Movies": {
"PopularMovies": "Népszerű filmek", "PopularMovies": "Népszerű filmek",
"UpcomingMovies": "Közelgő filmek", "UpcomingMovies": "Közelgő filmek",

View file

@ -3,7 +3,7 @@
"SignInButton": "Войти", "SignInButton": "Войти",
"UsernamePlaceholder": "Имя пользователя", "UsernamePlaceholder": "Имя пользователя",
"PasswordPlaceholder": "Пароль", "PasswordPlaceholder": "Пароль",
"RememberMe": "Запомнить Меня", "RememberMe": "Запомнить меня",
"ForgottenPassword": "Забыли пароль?", "ForgottenPassword": "Забыли пароль?",
"Errors": { "Errors": {
"IncorrectCredentials": "Неверное имя пользователя или пароль" "IncorrectCredentials": "Неверное имя пользователя или пароль"