refactor(watchlist): 🔊 Added some more logging around the watchlist authentication #5246

This commit is contained in:
tidusjar 2025-10-04 21:08:49 +01:00
parent 711f84ce63
commit d07fade874
6 changed files with 159 additions and 95 deletions

View File

@ -2,6 +2,7 @@
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="singleClickDiffPreview" value="1" />
<option name="unhandledExceptionsIgnoreList" value="1" />
<option name="vcsConfiguration" value="3" />
</component>
</project>

View File

@ -4,7 +4,14 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="" />
<list default="true" id="57001998-efde-494a-80b3-d7acfc91f770" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/projectSettingsUpdater.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/projectSettingsUpdater.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Ombi/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Api.External/MediaServers/Plex/PlexApi.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Api.External/MediaServers/Plex/PlexApi.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/components/watchlist/plex-watchlist.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/Ombi/ClientApp/src/app/settings/plex/components/watchlist/plex-watchlist.component.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -271,20 +278,11 @@
</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/ombi-app/ombi&quot;,
&quot;accountId&quot;: &quot;22dd09fe-fb9e-48a4-bfcc-3c152edf3f25&quot;
&quot;url&quot;: &quot;https://github.com/tidusjar/ombi&quot;,
&quot;accountId&quot;: &quot;22715a9f-5998-4231-b224-cb858185e803&quot;
}
}</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/990126b794024fe2bd16aebdd37eba1d7b600/93/25662f04/ServerVersion.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/3bd4df5aff92cabbc4d630be64227073db1b8539b3a1e47786b4b189d7cdb7/DbContext.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/449b441523c469ed34ff5a5e14f0bafcd8f097aa463655303dc19048fa44ac3/EntityFrameworkServiceCollectionExtensions.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7d81b2d4f22bee75e5438c707251ae43cb0974c207db91ffc159118c84b4eb9/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a424e6912048b4cd25715f158e789aae24db5c2911d9e622d39bc6ac3246c6/MySqlConnectionStringBuilder.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/e9881a453a581134c1a18331ac1f8f1201a5382a685bf2a40777fa22619/DbContextOptions`.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/MusicSearchEngine.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi.Core/Engine/RecentlyAddedEngine.cs" root0="FORCE_HIGHLIGHTING" />
@ -299,6 +297,7 @@
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi/Controllers/V2/SearchController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Ombi/Program.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/db7395f4add94e6d10e515b3e55373f2821f8323de7dc8e314d78feefacf5584/ActionMethodExecutor.cs" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="IdeDocumentHistory">
<option name="CHANGED_PATHS">
@ -313,6 +312,11 @@
</list>
</option>
</component>
<component name="JsbtTreeLayoutManager">
<layout place="tools.popupGrunt">
<scroll-view-position x="0" y="0" />
</layout>
</component>
<component name="PackageJsonUpdateNotifier">
<dismissed value="$PROJECT_DIR$/Ombi/ClientApp/package.json" />
</component>
@ -400,11 +404,13 @@
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"fb34c741-04ca-4b4f-8ea1-651a011b42c8.executor": "Debug",
"git-widget-placeholder": "angular-migration-standalone",
"git-widget-placeholder": "develop",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "yarn",
"settings.editor.selected.configurable": "SpellingScopeSettingsId",
"ts.external.directory.path": "/Users/tidusjar/Developer/ombi/src/Ombi/ClientApp/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
@ -497,6 +503,9 @@
<workItem from="1745681294313" duration="1814000" />
<workItem from="1747080279165" duration="838000" />
<workItem from="1747082180432" duration="1994000" />
<workItem from="1758915804840" duration="171000" />
<workItem from="1758916150497" duration="1079000" />
<workItem from="1759605037837" duration="2197000" />
</task>
<servers />
</component>
@ -565,70 +574,43 @@
<option name="timeStamp" value="2" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs</url>
<line>59</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" containingFunctionPresentation="Method 'MultiSearch'">
<url>file://$PROJECT_DIR$/Ombi.Core/Senders/TvSender.cs</url>
<line>123</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Senders/TvSender.cs" containingFunctionPresentation="Method 'SendToSonarr'">
<startOffsets>
<option value="2457" />
<option value="4687" />
</startOffsets>
<endOffsets>
<option value="2664" />
<option value="4722" />
</endOffsets>
</properties>
<option name="timeStamp" value="4" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs</url>
<line>49</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs" containingFunctionPresentation="Method 'MultiSearch'">
<startOffsets>
<option value="1991" />
</startOffsets>
<endOffsets>
<option value="2033" />
</endOffsets>
</properties>
<option name="timeStamp" value="5" />
<option name="timeStamp" value="17" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>110</line>
<line>68</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="5211" />
<option value="2978" />
</startOffsets>
<endOffsets>
<option value="5294" />
<option value="3028" />
</endOffsets>
</properties>
<option name="timeStamp" value="10" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>77</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="3412" />
</startOffsets>
<endOffsets>
<option value="3453" />
</endOffsets>
</properties>
<option name="timeStamp" value="11" />
</line-breakpoint>
<line-breakpoint enabled="true" type="DotNet Breakpoints">
<url>file://$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs</url>
<line>100</line>
<properties documentPath="$PROJECT_DIR$/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs" containingFunctionPresentation="Method 'Execute'">
<startOffsets>
<option value="4690" />
</startOffsets>
<endOffsets>
<option value="4724" />
</endOffsets>
</properties>
<option name="timeStamp" value="12" />
<option name="timeStamp" value="18" />
</line-breakpoint>
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
<properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
<option name="timeStamp" value="13" />
</breakpoint>
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
<properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
<option name="timeStamp" value="14" />
</breakpoint>
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
<properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
<option name="timeStamp" value="15" />
</breakpoint>
</breakpoints>
</breakpoint-manager>
<watches-manager>
@ -639,6 +621,10 @@
</configuration>
</watches-manager>
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
<component name="debuggerHistoryManager">
<expressions id="watch">
<expression>

View File

@ -321,7 +321,10 @@ namespace Ombi.Api.External.MediaServers.Plex
var result = await Api.Request(request, cancellationToken);
if (result.StatusCode.Equals(HttpStatusCode.Unauthorized))
// Check for all possible authentication errors
if (result.StatusCode == HttpStatusCode.Unauthorized ||
result.StatusCode == HttpStatusCode.Forbidden ||
result.StatusCode == HttpStatusCode.PaymentRequired)
{
return new PlexWatchlistContainer
{
@ -345,6 +348,7 @@ namespace Ombi.Api.External.MediaServers.Plex
/// <summary>
/// Pings the Plex API to validate if a token is still valid
/// Uses the same endpoint as watchlist to ensure consistent validation
/// </summary>
/// <param name="authToken">The authentication token to validate</param>
/// <param name="cancellationToken">Cancellation token</param>
@ -353,16 +357,23 @@ namespace Ombi.Api.External.MediaServers.Plex
{
try
{
var request = new Request("api/v2/ping", "https://plex.tv/", HttpMethod.Get);
// Use the same endpoint as watchlist to ensure consistent token validation
var request = new Request("library/sections/watchlist/all", WatchlistUri, 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);
var result = await Api.Request(request, cancellationToken);
// Check for authentication errors (401, 403, etc.)
if (result.StatusCode == HttpStatusCode.Unauthorized || result.StatusCode == HttpStatusCode.Forbidden)
{
return false;
}
return true;
}
catch
{
// If the request fails (401, 403, etc.), the token is invalid
// If the request fails, the token is invalid
return false;
}
}

View File

@ -3,23 +3,28 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.External.MediaServers.Plex;
using Ombi.Store.Entities;
using Microsoft.AspNetCore.Identity;
namespace Ombi.Core.Authentication
{
public interface IPlexTokenKeepAliveService
{
Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken);
Task<bool> TryRefreshTokenAsync(OmbiUser user, CancellationToken cancellationToken);
}
public class PlexTokenKeepAliveService : IPlexTokenKeepAliveService
{
private readonly IPlexApi _plexApi;
private readonly ILogger<PlexTokenKeepAliveService> _logger;
private readonly UserManager<OmbiUser> _userManager;
public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger<PlexTokenKeepAliveService> logger)
public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger<PlexTokenKeepAliveService> logger, UserManager<OmbiUser> userManager)
{
_plexApi = plexApi;
_logger = logger;
_userManager = userManager;
}
public async Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken)
@ -28,23 +33,63 @@ namespace Ombi.Core.Authentication
{
if (string.IsNullOrEmpty(token))
{
_logger.LogWarning("Token is null or empty");
_logger.LogWarning("Plex token is null or empty - cannot validate");
return false;
}
_logger.LogDebug("Validating Plex token using watchlist endpoint");
// Use the Ping method to validate the token
var isValid = await _plexApi.Ping(token, cancellationToken);
if (!isValid)
{
_logger.LogWarning("Token validation failed - token may be expired or invalid");
_logger.LogWarning("Plex token validation failed - token may be expired, invalid, or lacks watchlist permissions. User will need to re-authenticate.");
}
else
{
_logger.LogDebug("Plex token validation successful");
}
return isValid;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while keeping token alive");
_logger.LogError(ex, "Unexpected error occurred while validating Plex token - treating as invalid");
return false;
}
}
public async Task<bool> TryRefreshTokenAsync(OmbiUser user, CancellationToken cancellationToken)
{
try
{
if (user == null)
{
_logger.LogWarning("Cannot refresh token - user is null");
return false;
}
_logger.LogInformation($"Attempting to refresh Plex token for user '{user.UserName}'");
// Try to get account info with current token to see if we can get a fresh token
var account = await _plexApi.GetAccount(user.MediaServerToken);
if (account?.user?.authentication_token != null &&
account.user.authentication_token != user.MediaServerToken)
{
_logger.LogInformation($"Successfully refreshed Plex token for user '{user.UserName}'");
user.MediaServerToken = account.user.authentication_token;
await _userManager.UpdateAsync(user);
return true;
}
_logger.LogWarning($"Could not refresh Plex token for user '{user.UserName}' - account info returned same or null token");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error occurred while trying to refresh Plex token for user '{user.UserName}'");
return false;
}
}

View File

@ -98,41 +98,58 @@ 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?.Substring(0, Math.Min(8, user.MediaServerToken?.Length ?? 0))}...");
// Keep the token alive before attempting watchlist import
_logger.LogDebug($"Validating token for user '{user.UserName}' before 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
_logger.LogWarning($"Token validation failed for user '{user.UserName}' - attempting to refresh token");
// Try to refresh the token before giving up
var refreshSuccess = await _tokenKeepAliveService.TryRefreshTokenAsync(user, context?.CancellationToken ?? CancellationToken.None);
if (refreshSuccess)
{
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);
_logger.LogInformation($"Successfully refreshed token for user '{user.UserName}', retrying watchlist import");
// Re-validate the refreshed token
keepAliveSuccess = await _tokenKeepAliveService.KeepTokenAliveAsync(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
}
if (!keepAliveSuccess)
{
_logger.LogWarning($"Token validation and refresh failed for user '{user.UserName}' - token may be expired, invalid, or lacks watchlist permissions. Recording error and skipping user.");
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))
{
_logger.LogInformation($"Sending token expiration notification to user '{user.UserName}' at {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;
}
continue;
}
_logger.LogDebug($"Token validation successful for user '{user.UserName}', proceeding with watchlist import");
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.");
_logger.LogError($"Authentication error occurred during watchlist API call for user '{user.UserName}' - this should not happen after successful token validation. User needs to re-authenticate.");
await _userError.Add(new PlexWatchlistUserError
{
UserId = user.Id,
@ -142,6 +159,7 @@ namespace Ombi.Schedule.Jobs.Plex
// Send notification to user about token expiration
if (settings.NotifyOnWatchlistTokenExpiration && !string.IsNullOrEmpty(user.Email))
{
_logger.LogInformation($"Sending token expiration notification to user '{user.UserName}' at {user.Email}");
var notificationModel = new NotificationOptions
{
NotificationType = NotificationType.PlexWatchlistTokenExpired,

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { MatTableDataSource } from "@angular/material/table";
import { MatTableDataSource, MatTableModule } from "@angular/material/table";
import { take } from "rxjs";
import { IPlexWatchlistUsers, WatchlistSyncStatus } from "../../../../interfaces";
import { PlexService } from "../../../../services";
@ -7,6 +7,7 @@ import { MatButtonModule } from "@angular/material/button";
import { MatIconModule } from "@angular/material/icon";
import { MatCardModule } from "@angular/material/card";
import { CommonModule } from "@angular/common";
import { MatDialogModule } from "@angular/material/dialog";
@Component({
standalone: true,
@ -17,6 +18,8 @@ import { CommonModule } from "@angular/common";
MatIconModule,
MatCardModule,
MatButtonModule,
MatDialogModule,
MatTableModule
]
})
export class PlexWatchlistComponent implements OnInit{