diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c21c3693..115942b8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,59 @@
+## [4.49.3](https://github.com/Ombi-app/Ombi/compare/v4.49.2...v4.49.3) (2025-08-17)
+
+
+### Bug Fixes
+
+* **plex-api:** update Plex Watchlist URL ([11fd7a5](https://github.com/Ombi-app/Ombi/commit/11fd7a5fc853da75974a16bf4fdecd72a836f54b))
+
+
+
+## [4.49.2](https://github.com/Ombi-app/Ombi/compare/v4.49.1...v4.49.2) (2025-07-12)
+
+
+### Performance Improvements
+
+* **discover:** :zap: Improve the loading performance on the discover page ([97d5167](https://github.com/Ombi-app/Ombi/commit/97d5167db6c9f915021f32b96b281d7db3741d7f))
+
+
+
+## [4.49.1](https://github.com/Ombi-app/Ombi/compare/v4.49.0...v4.49.1) (2025-07-12)
+
+
+### Bug Fixes
+
+* **auth:** Fixed an issue where refreshing the page as a power user would stop the application from loading [#5242](https://github.com/Ombi-app/Ombi/issues/5242) ([cee4014](https://github.com/Ombi-app/Ombi/commit/cee40146ee02f7fb79e2019d6fe2f9d5c5dbdfc8))
+
+
+
+# [4.49.0](https://github.com/Ombi-app/Ombi/compare/v4.48.5...v4.49.0) (2025-07-11)
+
+
+### Features
+
+* Added the ability for the Watchlist to automatically refresh the users token. This will reduce the need for the user to log in ([067c029](https://github.com/Ombi-app/Ombi/commit/067c029f42e9fd853d060fdb2093013b15ac14c0))
+
+
+
+## [4.48.5](https://github.com/Ombi-app/Ombi/compare/v4.48.4...v4.48.5) (2025-05-14)
+
+
+### Bug Fixes
+
+* filter out excluded notification agents from user preferences ([c9ab4f4](https://github.com/Ombi-app/Ombi/commit/c9ab4f4f9faa66dbf263da693db1eefcf68beeec)), closes [#5196](https://github.com/Ombi-app/Ombi/issues/5196)
+
+
+
+## [4.48.4](https://github.com/Ombi-app/Ombi/compare/v4.48.3...v4.48.4) (2025-05-14)
+
+
+### Bug Fixes
+
+* **translations:** 🌐 New translations from Crowdin [skip ci] ([dbbfdd9](https://github.com/Ombi-app/Ombi/commit/dbbfdd926f0808f6d16f0b2cd8b5406e6b610c82))
+* **translations:** 🌐 New translations from Crowdin [skip ci] ([53a6a09](https://github.com/Ombi-app/Ombi/commit/53a6a092b14b8b8bdbff95d066926d3dbe6951f4))
+* **ui:** correct timezone handling in OmbiDatePipe ([f88c5ad](https://github.com/Ombi-app/Ombi/commit/f88c5ad818fadea7064e7dfbe46f07eae855109a)), closes [#5102](https://github.com/Ombi-app/Ombi/issues/5102)
+
+
+
## [4.48.3](https://github.com/Ombi-app/Ombi/compare/v4.48.2...v4.48.3) (2025-05-14)
@@ -2155,53 +2211,3 @@
-## [4.43.5](https://github.com/Ombi-app/Ombi/compare/v4.43.4...v4.43.5) (2023-08-24)
-
-
-
-## [4.43.4](https://github.com/Ombi-app/Ombi/compare/v4.43.3...v4.43.4) (2023-07-28)
-
-
-### Bug Fixes
-
-* **user-importer:** Fixed not importing all correct users [#4989](https://github.com/Ombi-app/Ombi/issues/4989) ([34c32f8](https://github.com/Ombi-app/Ombi/commit/34c32f8338705ea3f790d95b91c9ada21a41b9f2))
-
-
-
-## [4.43.3](https://github.com/Ombi-app/Ombi/compare/v4.43.2...v4.43.3) (2023-07-28)
-
-
-### Bug Fixes
-
-* switch back to the old plex friends API [#4989](https://github.com/Ombi-app/Ombi/issues/4989) ([c8ad12e](https://github.com/Ombi-app/Ombi/commit/c8ad12eb5f53889609d1793ae907afd33ba6ef38))
-
-
-
-## [4.43.2](https://github.com/Ombi-app/Ombi/compare/v4.43.1...v4.43.2) (2023-07-19)
-
-
-### Bug Fixes
-
-* **plex-api:** Switch over to the new API to avoid deprecation & save… ([#4986](https://github.com/Ombi-app/Ombi/issues/4986)) ([2f2d35e](https://github.com/Ombi-app/Ombi/commit/2f2d35ec867a8e5488e368db294bd37bcf92d843))
-* Remove old trending source ([#4987](https://github.com/Ombi-app/Ombi/issues/4987)) ([aacaa3e](https://github.com/Ombi-app/Ombi/commit/aacaa3e140b43f5d196da612f785cc4451717752))
-
-
-
-## [4.43.1](https://github.com/Ombi-app/Ombi/compare/v4.43.0...v4.43.1) (2023-07-16)
-
-
-### Bug Fixes
-
-* **user-importer:** don't delete admins in the cleanup ([895b9bf](https://github.com/Ombi-app/Ombi/commit/895b9bf6a060a678d4b0cca8083aa96c38e47b95))
-
-
-
-# [4.43.0](https://github.com/Ombi-app/Ombi/compare/v4.42.3...v4.43.0) (2023-07-14)
-
-
-### Features
-
-* Add Auto Approve 4K role ([#4982](https://github.com/Ombi-app/Ombi/issues/4982)) ([#4983](https://github.com/Ombi-app/Ombi/issues/4983)) ([ac05495](https://github.com/Ombi-app/Ombi/commit/ac054954254b9d77a42e057f1065570c7fdc1093)), closes [#4957](https://github.com/Ombi-app/Ombi/issues/4957)
-
-
-
diff --git a/README.md b/README.md
index 0e9350d59..8fb37b8f8 100644
--- a/README.md
+++ b/README.md
@@ -122,10 +122,10 @@ Here are some of the features Ombi has:
-
-
+
+
- Matt Jeanes
+ Amy Jeanes
|
diff --git a/src/Ombi.Api.Plex/IPlexApi.cs b/src/Ombi.Api.Plex/IPlexApi.cs
index 6632da875..be6a61c16 100644
--- a/src/Ombi.Api.Plex/IPlexApi.cs
+++ b/src/Ombi.Api.Plex/IPlexApi.cs
@@ -29,5 +29,6 @@ namespace Ombi.Api.Plex
Task AddUser(string emailAddress, string serverId, string authToken, int[] libs);
Task GetWatchlist(string plexToken, CancellationToken cancellationToken);
Task GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
+ Task Ping(string authToken, CancellationToken cancellationToken = default);
}
}
\ No newline at end of file
diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs
index cc0d13aaa..8babba05d 100644
--- a/src/Ombi.Api.Plex/PlexApi.cs
+++ b/src/Ombi.Api.Plex/PlexApi.cs
@@ -68,7 +68,7 @@ namespace Ombi.Api.Plex
private const string FriendsUri = "https://plex.tv/api/users";
private const string GetAccountUri = "https://plex.tv/users/account.json";
private const string ServerUri = "https://plex.tv/pms/servers.xml";
- private const string WatchlistUri = "https://metadata.provider.plex.tv/";
+ private const string WatchlistUri = "https://discover.provider.plex.tv/";
///
/// Sign into the Plex API
@@ -320,6 +320,30 @@ namespace Ombi.Api.Plex
return result;
}
+ ///
+ /// Pings the Plex API to validate if a token is still valid
+ ///
+ /// The authentication token to validate
+ /// Cancellation token
+ /// True if the token is valid, false otherwise
+ public async Task 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;
+ }
+ }
+
///
/// Adds the required headers and also the authorization header
diff --git a/src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs b/src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
new file mode 100644
index 000000000..d29da4a5a
--- /dev/null
+++ b/src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Ombi.Api.Plex;
+
+namespace Ombi.Core.Authentication
+{
+ public interface IPlexTokenKeepAliveService
+ {
+ Task KeepTokenAliveAsync(string token, CancellationToken cancellationToken);
+ }
+
+ public class PlexTokenKeepAliveService : IPlexTokenKeepAliveService
+ {
+ private readonly IPlexApi _plexApi;
+ private readonly ILogger _logger;
+
+ public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger logger)
+ {
+ _plexApi = plexApi;
+ _logger = logger;
+ }
+
+ public async Task KeepTokenAliveAsync(string token, CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ _logger.LogWarning("Token is null or empty");
+ return false;
+ }
+
+ // 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");
+ }
+
+ return isValid;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while keeping token alive");
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs
index caceb9b0e..027717bbe 100644
--- a/src/Ombi.DependencyInjection/IocExtensions.cs
+++ b/src/Ombi.DependencyInjection/IocExtensions.cs
@@ -107,6 +107,7 @@ namespace Ombi.DependencyInjection
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs
index b1fec6d1b..87a1d5e28 100644
--- a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs
+++ b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs
@@ -24,6 +24,7 @@ using Ombi.Notifications.Models;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Core;
+using Ombi.Core.Authentication;
namespace Ombi.Schedule.Tests
{
@@ -43,6 +44,8 @@ namespace Ombi.Schedule.Tests
_mocker.Use(um);
_context = _mocker.GetMock();
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
+ // Mock the keep-alive service to return true by default
+ _mocker.Use(Mock.Of(s => s.KeepTokenAliveAsync(It.IsAny(), It.IsAny()) == Task.FromResult(true)));
_subject = _mocker.CreateInstance();
_mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock());
_mocker.Setup(x => x.Notify(It.IsAny()));
@@ -838,5 +841,43 @@ namespace Ombi.Schedule.Tests
// Assert
_mocker.Verify(x => x.Notify(It.IsAny()), Times.Never);
}
+
+ [Test]
+ public async Task SkipsUserIfTokenKeepAliveFails()
+ {
+ // Arrange: Set up the keep-alive service to return false (token invalid/expired)
+ var keepAliveMock = new Mock();
+ keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false);
+ _mocker.Use(keepAliveMock.Object);
+ _subject = _mocker.CreateInstance();
+ _mocker.Setup, Task>(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(), It.IsAny()), Times.Once);
+ _mocker.Verify(x => x.GetWatchlist(It.IsAny(), It.IsAny()), Times.Never);
+ _mocker.Verify(x => x.Notify(It.IsAny()), Times.Never); // or Times.Once if notification is expected
+ }
+ [Test]
+ public async Task CallsKeepAliveForEachPlexUser()
+ {
+ // Arrange: Multiple Plex users
+ var users = new List
+ {
+ 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();
+ keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true);
+ _mocker.Use(keepAliveMock.Object);
+ _subject = _mocker.CreateInstance();
+ _mocker.Setup, Task>(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(), It.IsAny()), Times.Exactly(users.Count));
+ }
}
}
diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs
index 947e54406..3de332879 100644
--- a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs
+++ b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs
@@ -43,11 +43,12 @@ namespace Ombi.Schedule.Jobs.Plex
private readonly IRepository _userError;
private readonly IMovieDbApi _movieDbApi;
private readonly INotificationHelper _notificationHelper;
+ private readonly IPlexTokenKeepAliveService _tokenKeepAliveService;
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService settings, OmbiUserManager ombiUserManager,
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
ILogger logger, IExternalRepository watchlistRepo, IRepository userError,
- IMovieDbApi movieDbApi, INotificationHelper notificationHelper)
+ IMovieDbApi movieDbApi, INotificationHelper notificationHelper, IPlexTokenKeepAliveService tokenKeepAliveService)
{
_plexApi = plexApi;
_settings = settings;
@@ -60,6 +61,7 @@ namespace Ombi.Schedule.Jobs.Plex
_userError = userError;
_movieDbApi = movieDbApi;
_notificationHelper = notificationHelper;
+ _tokenKeepAliveService = tokenKeepAliveService;
}
public async Task Execute(IJobExecutionContext context)
@@ -97,6 +99,36 @@ namespace Ombi.Schedule.Jobs.Plex
}
_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
+ {
+ { "UserName", user.UserName }
+ }
+ };
+ await _notificationHelper.Notify(notificationModel);
+ }
+ continue;
+ }
+
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
if (watchlist?.AuthError ?? false)
{
diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html
index a1b3bc81b..f8dbaf257 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html
+++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html
@@ -5,13 +5,17 @@
{{'Discovery.Tv' | translate}}
-@defer (when discoverResults.length > 0) {
+@defer (when discoverResults.length > 0; prefetch on idle) {
}
-@placeholder(minimum 500) {
-
+@placeholder(minimum 300) {
+
}
\ No newline at end of file
diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss
index 81c559a83..9b62c9256 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss
+++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.scss
@@ -105,6 +105,30 @@
padding: 5px;
}
+.loading-container {
+ display: flex;
+ gap: 10px;
+ padding: 0 20px;
+ margin-top: 20px;
+}
+
+.loading-container .col-2 {
+ flex: 0 0 auto;
+ width: calc(10% - 9px);
+}
+
+@media (max-width: 768px) {
+ .loading-container .col-2 {
+ width: calc(50% - 5px);
+ }
+}
+
+@media (max-width: 480px) {
+ .loading-container .col-2 {
+ width: calc(100% - 0px);
+ }
+}
+
@media (min-width:755px){
::ng-deep .p-carousel-item{
flex: 1 0 200px !important;
diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts
index 36b8122ff..2463af3c4 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts
+++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts
@@ -43,7 +43,7 @@ export class CarouselListComponent implements OnInit {
get mediaTypeStorageKey() {
return "DiscoverOptions" + this.discoverType.toString();
};
- private amountToLoad = 17;
+ private amountToLoad = 10;
private currentlyLoaded = 0;
private baseUrl: string = "";
@@ -148,6 +148,7 @@ export class CarouselListComponent implements OnInit {
}
public async ngOnInit() {
+
this.is4kEnabled = this.featureFacade.is4kEnabled();
this.currentlyLoaded = 0;
const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey);
@@ -155,11 +156,15 @@ export class CarouselListComponent implements OnInit {
this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]];
}
- let currentIteration = 0;
- while (this.discoverResults.length <= 14 && currentIteration <= 3) {
- currentIteration++;
+ // Load initial data - just enough to fill the first carousel page
+ // This reduces initial API calls and improves loading performance
+ await this.loadData(false);
+
+ // If we don't have enough results to fill the carousel, load one more batch
+ if (this.discoverResults.length < 10) {
await this.loadData(false);
}
+
}
public async toggleChanged(event: MatButtonToggleChange) {
diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html
index 16a46c0d6..dc4e33be1 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html
+++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html
@@ -1,46 +1,108 @@
-
- {{ 'Discovery.Genres' | translate }}
-
-
-
- {{ 'Discovery.RecentlyRequestedTab' | translate }}
-
-
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.Genres' | translate }}
+
-
-
-
-
- {{ 'Discovery.SeasonalTab' | translate }}
-
-
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.Genres' | translate }}
+
-
+ }
-
- {{ 'Discovery.PopularTab' | translate }}
-
-
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.RecentlyRequestedTab' | translate }}
+
+
+
-
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.RecentlyRequestedTab' | translate }}
+
+
+ }
-
- {{ 'Discovery.TrendingTab' | translate }}
-
-
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.SeasonalTab' | translate }}
+
+
+
-
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.SeasonalTab' | translate }}
+
+
+ }
-
- {{ 'Discovery.UpcomingTab' | translate }}
-
-
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.PopularTab' | translate }}
+
+
+
-
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.PopularTab' | translate }}
+
+
+ }
+
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.TrendingTab' | translate }}
+
+
+
+
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.TrendingTab' | translate }}
+
+
+ }
+
+ @defer (on viewport; prefetch on idle) {
+
+ {{ 'Discovery.UpcomingTab' | translate }}
+
+
+
+
+ } @placeholder(minimum 300) {
+
+ {{ 'Discovery.UpcomingTab' | translate }}
+
+
+ }
diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss
index d95586507..9ba892e01 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss
+++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.scss
@@ -9,4 +9,28 @@ h2{
margin-top:40px;
margin-left:40px;
font-size: 24px;
+}
+
+.loading-container {
+ display: flex;
+ gap: 10px;
+ padding: 0 20px;
+ margin-top: 20px;
+}
+
+.loading-container .col-2 {
+ flex: 0 0 auto;
+ width: calc(10% - 9px);
+}
+
+@media (max-width: 768px) {
+ .loading-container .col-2 {
+ width: calc(50% - 5px);
+ }
+}
+
+@media (max-width: 480px) {
+ .loading-container .col-2 {
+ width: calc(100% - 0px);
+ }
}
\ No newline at end of file
diff --git a/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.html b/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.html
index 6da28c744..a6db36865 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.html
+++ b/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.html
@@ -1,4 +1,4 @@
-@defer (when requests()) {
+@defer (when requests(); prefetch on idle) {
-}@placeholder(minimum 500) {
+}@placeholder(minimum 300) {
-
-
-
-
-
diff --git a/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.scss b/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.scss
index 01c68db4d..c7fef78f9 100644
--- a/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.scss
+++ b/src/Ombi/ClientApp/src/app/discover/components/recently-requested-list/recently-requested-list.component.scss
@@ -105,12 +105,32 @@
padding: 5px;
}
+.loading-container {
+ display: flex;
+ gap: 10px;
+ padding: 0 20px;
+ margin-top: 20px;
+}
+
+.loading-container .col-2 {
+ flex: 0 0 auto;
+ width: calc(20% - 8px);
+}
+
+@media (max-width: 768px) {
+ .loading-container .col-2 {
+ width: calc(50% - 5px);
+ }
+}
+
+@media (max-width: 480px) {
+ .loading-container .col-2 {
+ width: calc(100% - 0px);
+ }
+}
+
@media (min-width:755px){
::ng-deep .p-carousel-item{
flex: 1 0 200px !important;
}
-}
-
-.loading-container {
- margin-left: 10rem;
}
\ No newline at end of file
diff --git a/src/Ombi/ClientApp/src/app/pipes/OmbiDatePipe.ts b/src/Ombi/ClientApp/src/app/pipes/OmbiDatePipe.ts
index d7e902242..5c0cc5c3b 100644
--- a/src/Ombi/ClientApp/src/app/pipes/OmbiDatePipe.ts
+++ b/src/Ombi/ClientApp/src/app/pipes/OmbiDatePipe.ts
@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from "@angular/core";
import { FormatPipe } from 'ngx-date-fns';
+import { parseISO, format } from 'date-fns';
@Pipe({
name: "ombiDate",
@@ -10,8 +11,16 @@ export class OmbiDatePipe implements PipeTransform {
private FormatPipe: FormatPipe,
) {}
- public transform(value: string, format: string ) {
- const date = new Date(value);
- return this.FormatPipe.transform(date, format);
+ public transform(value: string, formatStr: string ) {
+ if (!value) {
+ return '';
+ }
+
+ // Parse the ISO string as UTC
+ const utcDate = parseISO(value);
+
+ // Format the date using date-fns format function
+ // This will automatically handle the UTC to local conversion
+ return format(utcDate, formatStr);
}
}
diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts
index 1110490b9..a2e1872f9 100644
--- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts
+++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.ts
@@ -37,6 +37,13 @@ export class UserManagementUserComponent implements OnInit {
private appUrl: string = this.customizationFacade.appUrl();
private accessToken: string;
+ // List of excluded notification agents that should not be shown in user preferences
+ private readonly excludedAgents = [
+ INotificationAgent.Email,
+ INotificationAgent.Mobile,
+ INotificationAgent.Webhook
+ ];
+
constructor(private identityService: IdentityService,
private notificationService: MessageService,
private router: Router,
@@ -74,9 +81,15 @@ export class UserManagementUserComponent implements OnInit {
}
});
if(this.edit) {
- this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => this.notificationPreferences = x);
+ this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => {
+ // Filter out excluded notification agents
+ this.notificationPreferences = x.filter(pref => !this.excludedAgents.includes(pref.agent));
+ });
} else {
- this.identityService.getNotificationPreferences().subscribe(x => this.notificationPreferences = x);
+ this.identityService.getNotificationPreferences().subscribe(x => {
+ // Filter out excluded notification agents
+ this.notificationPreferences = x.filter(pref => !this.excludedAgents.includes(pref.agent));
+ });
}
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(x => {
this.sonarrQualities = x;
diff --git a/src/Ombi/Controllers/V1/SettingsController.cs b/src/Ombi/Controllers/V1/SettingsController.cs
index ad714780b..a23d56124 100644
--- a/src/Ombi/Controllers/V1/SettingsController.cs
+++ b/src/Ombi/Controllers/V1/SettingsController.cs
@@ -40,7 +40,6 @@ namespace Ombi.Controllers.V1
///
/// The Settings Controller
///
- [Admin]
[ApiV1]
[Produces("application/json")]
[ApiController]
@@ -78,6 +77,7 @@ namespace Ombi.Controllers.V1
/// Gets the Ombi settings.
///
///
+ [Admin]
[HttpGet("ombi")]
public async Task OmbiSettings()
{
@@ -110,6 +110,7 @@ namespace Ombi.Controllers.V1
///
/// The ombi.
///
+ [Admin]
[HttpPost("ombi")]
public async Task OmbiSettings([FromBody]OmbiSettings ombi)
{
@@ -145,6 +146,7 @@ namespace Ombi.Controllers.V1
return model;
}
+ [Admin]
[HttpPost("ombi/resetApi")]
public async Task ResetApiKey()
{
@@ -159,6 +161,7 @@ namespace Ombi.Controllers.V1
/// Gets the Plex Settings.
///
///
+ [Admin]
[HttpGet("plex")]
public async Task PlexSettings()
{
@@ -185,6 +188,7 @@ namespace Ombi.Controllers.V1
///
/// The plex.
///
+ [Admin]
[HttpPost("plex")]
public async Task PlexSettings([FromBody]PlexSettings plex)
{
@@ -207,6 +211,7 @@ namespace Ombi.Controllers.V1
/// Gets the Emby Settings.
///
///
+ [Admin]
[HttpGet("emby")]
public async Task EmbySettings()
{
@@ -218,6 +223,7 @@ namespace Ombi.Controllers.V1
///
/// The emby.
///
+ [Admin]
[HttpPost("emby")]
public async Task EmbySettings([FromBody]EmbySettings emby)
{
@@ -243,6 +249,7 @@ namespace Ombi.Controllers.V1
/// Gets the Jellyfin Settings.
///
///
+ [Admin]
[HttpGet("jellyfin")]
public async Task JellyfinSettings()
{
@@ -254,6 +261,7 @@ namespace Ombi.Controllers.V1
///
/// The jellyfin.
///
+ [Admin]
[HttpPost("jellyfin")]
public async Task JellyfinSettings([FromBody]JellyfinSettings jellyfin)
{
@@ -291,6 +299,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("landingpage")]
public async Task LandingPageSettings([FromBody]LandingPageSettings settings)
{
@@ -326,6 +335,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("customization")]
public async Task CustomizationSettings([FromBody]CustomizationSettings settings)
{
@@ -344,6 +354,7 @@ namespace Ombi.Controllers.V1
/// Get's the preset themes available
///
///
+ [Admin]
[HttpGet("themes")]
public async Task> GetThemes()
{
@@ -389,6 +400,7 @@ namespace Ombi.Controllers.V1
/// The settings.
///
[HttpPost("sonarr")]
+ [Admin]
public async Task SonarrSettings([FromBody]SonarrSettings settings)
{
var result = await Save(settings);
@@ -418,6 +430,7 @@ namespace Ombi.Controllers.V1
/// Gets the Lidarr Settings.
///
///
+ [Admin]
[HttpGet("lidarr")]
public async Task LidarrSettings()
{
@@ -441,6 +454,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("lidarr")]
public async Task LidarrSettings([FromBody]LidarrSettings settings)
{
@@ -457,6 +471,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("authentication")]
public async Task AuthenticationsSettings([FromBody]AuthenticationSettings settings)
{
@@ -479,6 +494,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("radarr")]
public async Task RadarrSettings([FromBody]RadarrCombinedModel settings)
{
@@ -500,6 +516,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("Update")]
public async Task UpdateSettings([FromBody]UpdateSettings settings)
{
@@ -510,6 +527,7 @@ namespace Ombi.Controllers.V1
/// Gets the UserManagement Settings.
///
///
+ [Admin]
[HttpGet("UserManagement")]
public async Task UserManagementSettings()
{
@@ -521,6 +539,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("UserManagement")]
public async Task UserManagementSettings([FromBody]UserManagementSettings settings)
{
@@ -531,6 +550,7 @@ namespace Ombi.Controllers.V1
/// Gets the Update Settings.
///
///
+ [Admin]
[HttpGet("Update")]
public async Task UpdateSettings()
{
@@ -543,6 +563,7 @@ namespace Ombi.Controllers.V1
/// Gets the CouchPotatoSettings Settings.
///
///
+ [Admin]
[HttpGet("CouchPotato")]
public async Task CouchPotatoSettings()
{
@@ -554,6 +575,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("CouchPotato")]
public async Task CouchPotatoSettings([FromBody]CouchPotatoSettings settings)
{
@@ -564,6 +586,7 @@ namespace Ombi.Controllers.V1
/// Gets the DogNzbSettings Settings.
///
///
+ [Admin]
[HttpGet("DogNzb")]
public async Task DogNzbSettings()
{
@@ -575,6 +598,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("DogNzb")]
public async Task DogNzbSettings([FromBody]DogNzbSettings settings)
{
@@ -586,6 +610,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("SickRage")]
public async Task SickRageSettings([FromBody]SickRageSettings settings)
{
@@ -596,6 +621,7 @@ namespace Ombi.Controllers.V1
/// Gets the SickRage Settings.
///
///
+ [Admin]
[HttpGet("SickRage")]
public async Task SickRageSettings()
{
@@ -606,6 +632,7 @@ namespace Ombi.Controllers.V1
/// Gets the JobSettings Settings.
///
///
+ [Admin]
[HttpGet("jobs")]
public async Task JobSettings()
{
@@ -638,6 +665,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
///
+ [Admin]
[HttpPost("jobs")]
public async Task JobSettings([FromBody]JobSettings settings)
{
@@ -681,6 +709,7 @@ namespace Ombi.Controllers.V1
}
[HttpPost("testcron")]
+ [Admin]
public CronTestModel TestCron([FromBody] CronViewModelBody body)
{
var model = new CronTestModel();
@@ -714,6 +743,7 @@ namespace Ombi.Controllers.V1
/// The settings.
///
[HttpPost("Issues")]
+ [Admin]
public async Task IssueSettings([FromBody]IssueSettings settings)
{
return await Save(settings);
@@ -744,6 +774,7 @@ namespace Ombi.Controllers.V1
/// The settings.
///
[HttpPost("vote")]
+ [Admin]
public async Task VoteSettings([FromBody]VoteSettings settings)
{
return await Save(settings);
@@ -754,6 +785,7 @@ namespace Ombi.Controllers.V1
///
///
[HttpGet("vote")]
+ [Admin]
public async Task VoteSettings()
{
return await Get();
@@ -772,6 +804,7 @@ namespace Ombi.Controllers.V1
///
/// The settings.
[HttpPost("themoviedb")]
+ [Admin]
public async Task TheMovieDbSettings([FromBody]TheMovieDbSettings settings)
{
return await Save(settings);
@@ -780,6 +813,7 @@ namespace Ombi.Controllers.V1
///
/// Get The Movie DB settings.
///
+ [Admin]
[HttpGet("themoviedb")]
public async Task TheMovieDbSettings()
{
@@ -791,6 +825,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/email")]
public async Task EmailNotificationSettings([FromBody] EmailNotificationsViewModel model)
{
@@ -808,6 +843,7 @@ namespace Ombi.Controllers.V1
/// Gets the Email Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/email")]
public async Task EmailNotificationSettings()
{
@@ -838,6 +874,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/discord")]
public async Task DiscordNotificationSettings([FromBody] DiscordNotificationsViewModel model)
{
@@ -855,6 +892,7 @@ namespace Ombi.Controllers.V1
/// Gets the discord Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/discord")]
public async Task DiscordNotificationSettings()
{
@@ -873,6 +911,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/telegram")]
public async Task TelegramNotificationSettings([FromBody] TelegramNotificationsViewModel model)
{
@@ -890,6 +929,7 @@ namespace Ombi.Controllers.V1
/// Gets the telegram Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/telegram")]
public async Task TelegramNotificationSettings()
{
@@ -907,6 +947,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/pushbullet")]
public async Task PushbulletNotificationSettings([FromBody] PushbulletNotificationViewModel model)
{
@@ -924,6 +965,7 @@ namespace Ombi.Controllers.V1
/// Gets the pushbullet Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/pushbullet")]
public async Task PushbulletNotificationSettings()
{
@@ -941,6 +983,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/pushover")]
public async Task PushoverNotificationSettings([FromBody] PushoverNotificationViewModel model)
{
@@ -958,6 +1001,7 @@ namespace Ombi.Controllers.V1
/// Gets the pushover Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/pushover")]
public async Task PushoverNotificationSettings()
{
@@ -976,6 +1020,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/slack")]
public async Task SlacktNotificationSettings([FromBody] SlackNotificationsViewModel model)
{
@@ -993,6 +1038,7 @@ namespace Ombi.Controllers.V1
/// Gets the slack Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/slack")]
public async Task SlackNotificationSettings()
{
@@ -1010,6 +1056,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/mattermost")]
public async Task MattermostNotificationSettings([FromBody] MattermostNotificationsViewModel model)
{
@@ -1027,6 +1074,7 @@ namespace Ombi.Controllers.V1
/// Gets the Mattermost Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/mattermost")]
public async Task MattermostNotificationSettings()
{
@@ -1043,6 +1091,7 @@ namespace Ombi.Controllers.V1
/// Gets the Twilio Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/twilio")]
public async Task TwilioNotificationSettings()
{
@@ -1064,6 +1113,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/twilio")]
public async Task TwilioNotificationSettings([FromBody] TwilioSettingsViewModel model)
{
@@ -1082,6 +1132,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/mobile")]
public async Task MobileNotificationSettings([FromBody] MobileNotificationsViewModel model)
{
@@ -1099,6 +1150,7 @@ namespace Ombi.Controllers.V1
/// Gets the Mobile Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/mobile")]
public async Task MobileNotificationSettings()
{
@@ -1116,6 +1168,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/gotify")]
public async Task GotifyNotificationSettings([FromBody] GotifyNotificationViewModel model)
{
@@ -1133,6 +1186,7 @@ namespace Ombi.Controllers.V1
/// Gets the gotify Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/gotify")]
public async Task GotifyNotificationSettings()
{
@@ -1150,6 +1204,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/webhook")]
public async Task WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model)
{
@@ -1163,6 +1218,7 @@ namespace Ombi.Controllers.V1
/// Gets the webhook notification settings.
///
///
+ [Admin]
[HttpGet("notifications/webhook")]
public async Task WebhookNotificationSettings()
{
@@ -1177,6 +1233,7 @@ namespace Ombi.Controllers.V1
///
/// The model.
///
+ [Admin]
[HttpPost("notifications/newsletter")]
public async Task NewsletterSettings([FromBody] NewsletterNotificationViewModel model)
{
@@ -1191,6 +1248,7 @@ namespace Ombi.Controllers.V1
}
[ApiExplorerSettings(IgnoreApi = true)]
+ [Admin]
[HttpPost("notifications/newsletterdatabase")]
public async Task UpdateNewsletterDatabase()
{
@@ -1201,6 +1259,7 @@ namespace Ombi.Controllers.V1
/// Gets the Newsletter Notification Settings.
///
///
+ [Admin]
[HttpGet("notifications/newsletter")]
public async Task NewsletterSettings()
{
diff --git a/src/Ombi/wwwroot/translations/ca.json b/src/Ombi/wwwroot/translations/ca.json
index 2c7e21fc2..7ac957b11 100644
--- a/src/Ombi/wwwroot/translations/ca.json
+++ b/src/Ombi/wwwroot/translations/ca.json
@@ -159,7 +159,7 @@
"RequestedBy": "Sol·licitat per",
"Status": "Estat",
"RequestStatus": "Estat de la sol·licitud",
- "Watched": "Watched",
+ "Watched": "Vist",
"WatchedTooltip": "The user who made the request has watched it",
"WatchedProgressTooltip": "Shows how much the user who made the request has watched it",
"WatchedByUsersCount": "{{count}} users have watched this.",
@@ -408,7 +408,7 @@
"Movies": "Pel·lícules",
"Combined": "Combinat",
"Tv": "TV",
- "Genres": "Genres",
+ "Genres": "Gèneres",
"CardDetails": {
"Availability": "Disponibilitat",
"Studio": "Estudi",
diff --git a/src/Ombi/wwwroot/translations/nl.json b/src/Ombi/wwwroot/translations/nl.json
index 4a3dab266..fb78ae68f 100644
--- a/src/Ombi/wwwroot/translations/nl.json
+++ b/src/Ombi/wwwroot/translations/nl.json
@@ -159,10 +159,10 @@
"RequestedBy": "Verzocht Door",
"Status": "Status",
"RequestStatus": "Aanvraagstatus",
- "Watched": "Watched",
- "WatchedTooltip": "The user who made the request has watched it",
- "WatchedProgressTooltip": "Shows how much the user who made the request has watched it",
- "WatchedByUsersCount": "{{count}} users have watched this.",
+ "Watched": "Bekeken",
+ "WatchedTooltip": "De gebruiker die het verzoek heeft ingediend, heeft het bekeken",
+ "WatchedProgressTooltip": "Laat zien hoeveel de gebruiker die het verzoek heeft gemaakt het heeft bekeken",
+ "WatchedByUsersCount": "{{count}} gebruikers hebben dit bekeken.",
"Denied": " Geweigerd:",
"TheatricalRelease": "Cinema Uitgave: {{date}}",
"ReleaseDate": "Uitgekomen: {{date}}",
@@ -225,7 +225,7 @@
"Denied": "Geselecteerde items succesvol afgekeurd"
},
"SuccessfullyApproved": "Succesvol goedgekeurd",
- "SuccessfullyDenied": "Successfully Denied",
+ "SuccessfullyDenied": "Succesvol Geweigerd",
"SuccessfullyDeleted": "Verzoek succesvol verwijderd",
"NowAvailable": "Verzoek is nu beschikbaar",
"NowUnavailable": "Verzoek is nu niet beschikbaar",
@@ -241,7 +241,7 @@
"NoPermissionsOnBehalf": "Je hebt niet de juiste rechten om namens gebruikers aan te vragen!",
"NoPermissions": "Je hebt de juiste rechten niet!",
"RequestDoesNotExist": "Verzoek bestaat niet",
- "ChildRequestDoesNotExist": "Child Request does not exist",
+ "ChildRequestDoesNotExist": "Kindverzoek bestaat niet",
"NoPermissionsRequestMovie": "Je bent niet gemachtigd om een film aan te vragen",
"NoPermissionsRequestTV": "Je bent niet gemachtigd om een serie aan te vragen",
"NoPermissionsRequestAlbum": "Je bent niet gemachtigd om een album aan te vragen",
diff --git a/version.json b/version.json
index 0650edbbe..dc01ed894 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "version": "4.48.3"
+ "version": "4.49.3"
}
|