diff --git a/src/Ombi.Api.Plex/Models/PlexWatchlistContainer.cs b/src/Ombi.Api.Plex/Models/PlexWatchlistContainer.cs index b208c189f..2eab1d255 100644 --- a/src/Ombi.Api.Plex/Models/PlexWatchlistContainer.cs +++ b/src/Ombi.Api.Plex/Models/PlexWatchlistContainer.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Ombi.Api.Plex.Models { public class PlexWatchlistContainer { public PlexWatchlist MediaContainer { get; set; } + [JsonIgnore] + public bool AuthError { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index 9f91c540c..fae4a78b9 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -295,9 +296,18 @@ namespace Ombi.Api.Plex var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get); await AddHeaders(request, plexToken); - var result = await Api.Request(request, cancellationToken); + var result = await Api.Request(request, cancellationToken); - return result; + if (result.StatusCode.Equals(HttpStatusCode.Unauthorized)) + { + return new PlexWatchlistContainer + { + AuthError = true + }; + } + + var receivedString = await result.Content.ReadAsStringAsync(cancellationToken); + return JsonConvert.DeserializeObject(receivedString); } public async Task GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken) diff --git a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs index 950f6f431..b11a3d48b 100644 --- a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs +++ b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs @@ -1,4 +1,5 @@ -using Moq; +using MockQueryable.Moq; +using Moq; using Moq.AutoMock; using NUnit.Framework; using Ombi.Api.Plex; @@ -8,6 +9,7 @@ using Ombi.Core.Engine.Interfaces; using Ombi.Core.Models.Requests; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; +using Ombi.Core.Tests; using Ombi.Schedule.Jobs.Plex; using Ombi.Store.Entities; using Ombi.Store.Repository; @@ -32,11 +34,12 @@ namespace Ombi.Schedule.Tests public void Setup() { _mocker = new AutoMocker(); - var um = MockHelper.MockUserManager(new List { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "abc", UserName = "abc", NormalizedUserName = "ABC" } }); + var um = MockHelper.MockUserManager(new List { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" } }); _mocker.Use(um); _context = _mocker.GetMock(); _context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); _subject = _mocker.CreateInstance(); + _mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock().Object); } [Test] @@ -53,6 +56,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TerminatesWhenWatchlistIsNotEnabled() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false }); await _subject.Execute(null); _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); @@ -75,9 +79,74 @@ namespace Ombi.Schedule.Tests _mocker.Verify>(x => x.Add(It.IsAny()), Times.Never); } + [Test] + public async Task AuthenticationError() + { + + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { AuthError = true }); + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify(x => x.GetWatchlist(It.IsAny(), It.IsAny()), Times.Once); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.GetAll(), Times.Never); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.Add(It.Is(x => x.UserId == "abc")), Times.Once); + } + + [Test] + public async Task FailedWatchListUser_NewToken_ShouldBeRemoved() + { + _mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List + { + new PlexWatchlistUserError + { + UserId = "abc", + MediaServerToken = "dead" + } + }.AsQueryable().BuildMock().Object); + + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false }); + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify(x => x.GetWatchlist(It.IsAny(), It.IsAny()), Times.Once); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.GetAll(), Times.Never); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.Add(It.Is(x => x.UserId == "abc")), Times.Never); + _mocker.Verify>(x => x.Delete(It.Is(x => x.UserId == "abc")), Times.Once); + } + + [Test] + public async Task FailedWatchListUser_OldToken_ShouldSkip() + { + _mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List + { + new PlexWatchlistUserError + { + UserId = "abc", + MediaServerToken = "token1" + } + }.AsQueryable().BuildMock().Object); + + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false }); + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify(x => x.GetWatchlist(It.IsAny(), It.IsAny()), Times.Never); + _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.GetAll(), Times.Never); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Never); + _mocker.Verify>(x => x.Add(It.Is(x => x.UserId == "abc")), Times.Never); + _mocker.Verify>(x => x.Delete(It.Is(x => x.UserId == "abc")), Times.Never); + } + + [Test] public async Task NoPlexUsersWithToken() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); var um = MockHelper.MockUserManager(new List { @@ -102,6 +171,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MultipleUsers() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); var um = MockHelper.MockUserManager(new List { @@ -125,6 +195,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_NoGuid() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -175,6 +246,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TvRequestFromWatchList_NoGuid() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -224,6 +296,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_AlreadyRequested() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -273,6 +346,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TvRequestFromWatchList_AlreadyRequested() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -322,6 +396,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_NoTmdbGuid() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -371,6 +446,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TvRequestFromWatchList_NoTmdbGuid() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -420,6 +496,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_AlreadyImported() { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs index 8e6b443ef..69b42213b 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; @@ -32,10 +33,11 @@ namespace Ombi.Schedule.Jobs.Plex private readonly IHubContext _hub; private readonly ILogger _logger; private readonly IExternalRepository _watchlistRepo; + private readonly IExternalRepository _userError; public PlexWatchlistImport(IPlexApi plexApi, ISettingsService settings, OmbiUserManager ombiUserManager, IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, IHubContext hub, - ILogger logger, IExternalRepository watchlistRepo) + ILogger logger, IExternalRepository watchlistRepo, IExternalRepository userError) { _plexApi = plexApi; _settings = settings; @@ -45,6 +47,7 @@ namespace Ombi.Schedule.Jobs.Plex _hub = hub; _logger = logger; _watchlistRepo = watchlistRepo; + _userError = userError; } public async Task Execute(IJobExecutionContext context) @@ -64,9 +67,35 @@ namespace Ombi.Schedule.Jobs.Plex { try { + // Check if the user has errors and the token is the same (not refreshed) + var failedUser = await _userError.GetAll().Where(x => x.UserId == user.Id).FirstOrDefaultAsync(); + if (failedUser != null) + { + if (failedUser.MediaServerToken.Equals(user.MediaServerToken)) + { + _logger.LogInformation($"Skipping Plex Watchlist Import for user '{user.UserName}' as they failed previously and the token has not yet been refreshed"); + continue; + } + else + { + // remove that guy + await _userError.Delete(failedUser); + failedUser = null; + } + } _logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}"); var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None); + if (watchlist?.AuthError ?? false) + { + _logger.LogError($"Auth failed for user '{user.UserName}'. Need to re-authenticate with Ombi."); + await _userError.Add(new PlexWatchlistUserError + { + UserId = user.Id, + MediaServerToken = user.MediaServerToken, + }); + continue; + } if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false)) { _logger.LogDebug($"No watchlist found for {user.UserName}"); diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index f13c1e74f..5926aae9d 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -28,6 +28,7 @@ namespace Ombi.Store.Context public DbSet PlexSeasonsContent { get; set; } public DbSet PlexEpisode { get; set; } public DbSet PlexWatchlistHistory { get; set; } + public DbSet PlexWatchListUserError { get; set; } public DbSet RadarrCache { get; set; } public DbSet CouchPotatoCache { get; set; } public DbSet EmbyContent { get; set; } diff --git a/src/Ombi.Store/Entities/PlexWatchlistUserError.cs b/src/Ombi.Store/Entities/PlexWatchlistUserError.cs new file mode 100644 index 000000000..6a94c226f --- /dev/null +++ b/src/Ombi.Store/Entities/PlexWatchlistUserError.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Store.Entities +{ + [Table(nameof(PlexWatchlistUserError))] + public class PlexWatchlistUserError : Entity + { + public string UserId { get; set; } + public string MediaServerToken { get; set; } + + [ForeignKey(nameof(UserId))] + public OmbiUser User { get; set; } + } +} diff --git a/src/Ombi.Core.Tests/DbHelper.cs b/src/Ombi.Test.Common/DbHelper.cs similarity index 100% rename from src/Ombi.Core.Tests/DbHelper.cs rename to src/Ombi.Test.Common/DbHelper.cs