mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-20 21:33:15 -07:00
feat: Added the ability for the Watchlist to automatically refresh the users token. This will reduce the need for the user to log in
This commit is contained in:
parent
cfe2b6ac0f
commit
067c029f42
5 changed files with 101 additions and 1 deletions
|
@ -29,5 +29,6 @@ namespace Ombi.Api.Plex
|
||||||
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
|
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
|
||||||
Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken);
|
Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken);
|
||||||
Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
|
Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
|
||||||
|
Task<bool> Ping(string authToken, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -320,6 +320,30 @@ namespace Ombi.Api.Plex
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pings the Plex API to validate if a token is still valid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authToken">The authentication token to validate</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>True if the token is valid, false otherwise</returns>
|
||||||
|
public async Task<bool> Ping(string authToken, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new Request("api/v2/ping", "https://plex.tv/", HttpMethod.Get);
|
||||||
|
await AddHeaders(request, authToken);
|
||||||
|
|
||||||
|
// We don't need to parse the response, just check if the request succeeds
|
||||||
|
await Api.Request(request, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If the request fails (401, 403, etc.), the token is invalid
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the required headers and also the authorization header
|
/// Adds the required headers and also the authorization header
|
||||||
|
|
|
@ -107,6 +107,7 @@ namespace Ombi.DependencyInjection
|
||||||
services.AddTransient<IMusicSender, MusicSender>();
|
services.AddTransient<IMusicSender, MusicSender>();
|
||||||
services.AddTransient<IMassEmailSender, MassEmailSender>();
|
services.AddTransient<IMassEmailSender, MassEmailSender>();
|
||||||
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
|
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
|
||||||
|
services.AddTransient<IPlexTokenKeepAliveService, PlexTokenKeepAliveService>();
|
||||||
services.AddTransient<IVoteEngine, VoteEngine>();
|
services.AddTransient<IVoteEngine, VoteEngine>();
|
||||||
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
|
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
|
||||||
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
|
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
|
||||||
|
|
|
@ -24,6 +24,7 @@ using Ombi.Notifications.Models;
|
||||||
using Ombi.Core.Notifications;
|
using Ombi.Core.Notifications;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
using Ombi.Core;
|
using Ombi.Core;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
|
||||||
namespace Ombi.Schedule.Tests
|
namespace Ombi.Schedule.Tests
|
||||||
{
|
{
|
||||||
|
@ -43,6 +44,8 @@ namespace Ombi.Schedule.Tests
|
||||||
_mocker.Use(um);
|
_mocker.Use(um);
|
||||||
_context = _mocker.GetMock<IJobExecutionContext>();
|
_context = _mocker.GetMock<IJobExecutionContext>();
|
||||||
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||||
|
// Mock the keep-alive service to return true by default
|
||||||
|
_mocker.Use<IPlexTokenKeepAliveService>(Mock.Of<IPlexTokenKeepAliveService>(s => s.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()) == Task.FromResult(true)));
|
||||||
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||||
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
|
||||||
_mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()));
|
_mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()));
|
||||||
|
@ -838,5 +841,43 @@ namespace Ombi.Schedule.Tests
|
||||||
// Assert
|
// Assert
|
||||||
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never);
|
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SkipsUserIfTokenKeepAliveFails()
|
||||||
|
{
|
||||||
|
// Arrange: Set up the keep-alive service to return false (token invalid/expired)
|
||||||
|
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
|
||||||
|
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
||||||
|
_mocker.Use(keepAliveMock.Object);
|
||||||
|
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||||
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
|
// Act
|
||||||
|
await _subject.Execute(_context.Object);
|
||||||
|
// Assert: Should not attempt to import watchlist if keep-alive fails
|
||||||
|
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never); // or Times.Once if notification is expected
|
||||||
|
}
|
||||||
|
[Test]
|
||||||
|
public async Task CallsKeepAliveForEachPlexUser()
|
||||||
|
{
|
||||||
|
// Arrange: Multiple Plex users
|
||||||
|
var users = new List<OmbiUser>
|
||||||
|
{
|
||||||
|
new OmbiUser { Id = "abc1", UserType = UserType.PlexUser, MediaServerToken = "abc1", UserName = "abc1", NormalizedUserName = "ABC1" },
|
||||||
|
new OmbiUser { Id = "abc2", UserType = UserType.PlexUser, MediaServerToken = "abc2", UserName = "abc2", NormalizedUserName = "ABC2" },
|
||||||
|
};
|
||||||
|
var um = MockHelper.MockUserManager(users);
|
||||||
|
_mocker.Use(um);
|
||||||
|
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
|
||||||
|
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||||
|
_mocker.Use(keepAliveMock.Object);
|
||||||
|
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||||
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
|
// Act
|
||||||
|
await _subject.Execute(_context.Object);
|
||||||
|
// Assert: KeepAlive should be called for each user
|
||||||
|
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(users.Count));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ using Ombi.Core.Notifications;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Ombi.Store.Repository.Requests;
|
using Ombi.Store.Repository.Requests;
|
||||||
using Ombi.Core;
|
using Ombi.Core;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
|
||||||
namespace Ombi.Schedule.Jobs.Plex
|
namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
|
@ -43,11 +44,12 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
private readonly IRepository<PlexWatchlistUserError> _userError;
|
private readonly IRepository<PlexWatchlistUserError> _userError;
|
||||||
private readonly IMovieDbApi _movieDbApi;
|
private readonly IMovieDbApi _movieDbApi;
|
||||||
private readonly INotificationHelper _notificationHelper;
|
private readonly INotificationHelper _notificationHelper;
|
||||||
|
private readonly IPlexTokenKeepAliveService _tokenKeepAliveService;
|
||||||
|
|
||||||
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
|
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
|
||||||
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
|
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
|
||||||
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError,
|
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError,
|
||||||
IMovieDbApi movieDbApi, INotificationHelper notificationHelper)
|
IMovieDbApi movieDbApi, INotificationHelper notificationHelper, IPlexTokenKeepAliveService tokenKeepAliveService)
|
||||||
{
|
{
|
||||||
_plexApi = plexApi;
|
_plexApi = plexApi;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
@ -60,6 +62,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
_userError = userError;
|
_userError = userError;
|
||||||
_movieDbApi = movieDbApi;
|
_movieDbApi = movieDbApi;
|
||||||
_notificationHelper = notificationHelper;
|
_notificationHelper = notificationHelper;
|
||||||
|
_tokenKeepAliveService = tokenKeepAliveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
@ -97,6 +100,36 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
||||||
|
|
||||||
|
// Keep the token alive before attempting watchlist import
|
||||||
|
var keepAliveSuccess = await _tokenKeepAliveService.KeepTokenAliveAsync(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||||
|
if (!keepAliveSuccess)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Token for user '{user.UserName}' is invalid or expired (keep-alive failed). Recording error and skipping.");
|
||||||
|
await _userError.Add(new PlexWatchlistUserError
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
MediaServerToken = user.MediaServerToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send notification to user about token expiration
|
||||||
|
if (settings.NotifyOnWatchlistTokenExpiration && !string.IsNullOrEmpty(user.Email))
|
||||||
|
{
|
||||||
|
var notificationModel = new NotificationOptions
|
||||||
|
{
|
||||||
|
NotificationType = NotificationType.PlexWatchlistTokenExpired,
|
||||||
|
Recipient = user.Email,
|
||||||
|
DateTime = DateTime.Now,
|
||||||
|
Substitutes = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "UserName", user.UserName }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await _notificationHelper.Notify(notificationModel);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||||
if (watchlist?.AuthError ?? false)
|
if (watchlist?.AuthError ?? false)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue