diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bde41062..115942b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,81 @@ +## [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) + + +### Bug Fixes + +* Correct 4K movie request existence check ([ba6e708](https://github.com/Ombi-app/Ombi/commit/ba6e708e189f52f2ff4ebc073fa38a4f53f1061c)), closes [#4798](https://github.com/Ombi-app/Ombi/issues/4798) + + + +## [4.48.2](https://github.com/Ombi-app/Ombi/compare/v4.48.1...v4.48.2) (2025-05-14) + + +### Bug Fixes + +* **radarr:** ensure RequestedUser is loaded when creating tags ([f8658fe](https://github.com/Ombi-app/Ombi/commit/f8658fe6d56488aa5caa68093245cbf021a31810)), closes [#5045](https://github.com/Ombi-app/Ombi/issues/5045) + + + +## [4.48.1](https://github.com/Ombi-app/Ombi/compare/v4.48.0...v4.48.1) (2025-05-14) + + + # [4.48.0](https://github.com/Ombi-app/Ombi/compare/v4.47.3...v4.48.0) (2025-05-14) @@ -2133,89 +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) - - - -## [4.42.3](https://github.com/Ombi-app/Ombi/compare/v4.42.2...v4.42.3) (2023-07-13) - - -### Bug Fixes - -* **user-importer:** Do not delete the Plex Admin as part of the user Importer cleanup [#4870](https://github.com/Ombi-app/Ombi/issues/4870) ([#4981](https://github.com/Ombi-app/Ombi/issues/4981)) ([4e80e7b](https://github.com/Ombi-app/Ombi/commit/4e80e7b7c3239a46a645ab6d1054993734ad4dd6)) - - - -## [4.42.2](https://github.com/Ombi-app/Ombi/compare/v4.42.1...v4.42.2) (2023-07-03) - - -### Bug Fixes - -* Remove Angular TSLint ([#4973](https://github.com/Ombi-app/Ombi/issues/4973)) ([93969b5](https://github.com/Ombi-app/Ombi/commit/93969b5a2d82f442299bee418fae43cb590d7743)) -* upgrade jquery from 3.6.1 to 3.7.0 ([#4974](https://github.com/Ombi-app/Ombi/issues/4974)) ([f2552ef](https://github.com/Ombi-app/Ombi/commit/f2552ef6ede011080a8d5499e11930c4d41d04c2)) -* upgrade multiple dependencies with Snyk ([#4961](https://github.com/Ombi-app/Ombi/issues/4961)) ([3c3edf6](https://github.com/Ombi-app/Ombi/commit/3c3edf6273fa98c420989ebcebfee52b2545e402)) -* upgrade zone.js from 0.11.8 to 0.13.0 ([#4975](https://github.com/Ombi-app/Ombi/issues/4975)) ([37f6564](https://github.com/Ombi-app/Ombi/commit/37f65648a2f8742020b0954acec4168aee048942)) - - - -## [4.42.1](https://github.com/Ombi-app/Ombi/compare/v4.42.0...v4.42.1) (2023-06-20) - - -### Bug Fixes - -* More automation tests mainly around the Plex Settings page ([#4821](https://github.com/Ombi-app/Ombi/issues/4821)) ([21bfc5a](https://github.com/Ombi-app/Ombi/commit/21bfc5a45adf6da6a80854e19494a8ffdc9c0761)) -* src/Ombi.Notifications/Ombi.Notifications.csproj to reduce vulnerabilities ([#4969](https://github.com/Ombi-app/Ombi/issues/4969)) [skip ci] ([8584ad4](https://github.com/Ombi-app/Ombi/commit/8584ad46053c51f5da40b24f3efd1b9e5a031ddd)) -* upgrade @fortawesome/fontawesome-free from 6.1.2 to 6.4.0 ([#4965](https://github.com/Ombi-app/Ombi/issues/4965)) [skip ci] ([84454e5](https://github.com/Ombi-app/Ombi/commit/84454e53c00c808e8a393c7750bdc418a7593e91)) -* upgrade @microsoft/signalr from 6.0.11 to 6.0.16 ([#4964](https://github.com/Ombi-app/Ombi/issues/4964)) [skip ci] ([a0201e3](https://github.com/Ombi-app/Ombi/commit/a0201e3f585dc52f717e33c46ede35a4eccac736)) -* upgrade cypress-real-events from 1.7.4 to 1.8.1 ([#4968](https://github.com/Ombi-app/Ombi/issues/4968)) [skip ci] ([8a24b56](https://github.com/Ombi-app/Ombi/commit/8a24b56299c3bc98bf0d719ba448972aaa7f7461)) -* upgrade multiple dependencies with Snyk ([#4963](https://github.com/Ombi-app/Ombi/issues/4963)) [skip ci] ([6025c5e](https://github.com/Ombi-app/Ombi/commit/6025c5ed757438d3a5d79bd36fd789ef0297ce70)) -* upgrade primeng from 15.0.0-rc.1 to 15.4.1 ([#4962](https://github.com/Ombi-app/Ombi/issues/4962)) [skip ci] ([23a4fed](https://github.com/Ombi-app/Ombi/commit/23a4fede69898a25b342aed78a8cda553c1fd18d)) - - - 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: - - MattJeanes + + AmyJeanes
- 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.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 4d3cd2cf9..82a6bce21 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -598,13 +598,13 @@ namespace Ombi.Core.Engine public async Task ApproveMovieById(int requestId, bool is4K) { - var request = await MovieRepository.Find(requestId); + var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId); return await ApproveMovie(request, is4K); } public async Task DenyMovieById(int modelId, string denyReason, bool is4K) { - var request = await MovieRepository.Find(modelId); + var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult @@ -790,7 +790,7 @@ namespace Ombi.Core.Engine public async Task ReProcessRequest(int requestId, bool is4K, CancellationToken cancellationToken) { - var request = await MovieRepository.Find(requestId); + var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId); if (request == null) { return new RequestEngineResult @@ -805,7 +805,7 @@ namespace Ombi.Core.Engine public async Task MarkUnavailable(int modelId, bool is4K) { - var request = await MovieRepository.Find(modelId); + var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult @@ -834,7 +834,7 @@ namespace Ombi.Core.Engine public async Task MarkAvailable(int modelId, bool is4K) { - var request = await MovieRepository.Find(modelId); + var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index 26da3465c..a7ae4a6e7 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -182,7 +182,10 @@ namespace Ombi.Core.Senders if (settings.SendUserTags) { var userTag = await GetOrCreateTag(model, settings); - tags.Add(userTag.id); + if (userTag != null) + { + tags.Add(userTag.id); + } } // Overrides on the request take priority @@ -198,7 +201,9 @@ namespace Ombi.Core.Senders List movies; // Check if the movie already exists? Since it could be unmonitored - movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri); + // Get the appropriate Radarr instance settings for existence check + var existenceCheckSettings = is4k ? await _radarr4KSettings.GetSettingsAsync() : settings; + movies = await _radarrV3Api.GetMovies(existenceCheckSettings.ApiKey, existenceCheckSettings.FullUri); var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId); if (existingMovie == null) @@ -246,6 +251,12 @@ namespace Ombi.Core.Senders private async Task GetOrCreateTag(MovieRequests model, RadarrSettings s) { + if (model.RequestedUser == null) + { + _log.LogWarning("Cannot create tag - RequestedUser is null for movie request {MovieTitle}", model.Title); + return null; + } + var tagName = model.RequestedUser.UserName; // Does tag exist? diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index 247ce8b6e..1928ecea9 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -133,7 +133,14 @@ namespace Ombi.Core.Senders string seriesType; int? tagToUse = null; + Logger.LogInformation("Starting SendToSonarr for series {Title} (TvDbId: {TvDbId})", model.ParentRequest.Title, model.ParentRequest.TvDbId); + Logger.LogInformation("Series type: {SeriesType}", model.SeriesType); + var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId); + if (profiles != null) + { + Logger.LogInformation("Found user quality profile for user {UserId}", model.RequestedUserId); + } if (model.SeriesType == SeriesType.Anime) { @@ -141,8 +148,10 @@ namespace Ombi.Core.Senders // For some reason, if we haven't got one use the first root folder in Sonarr if (!int.TryParse(s.RootPathAnime, out int animePath)) { + Logger.LogWarning("Failed to parse RootPathAnime: {RootPathAnime}, falling back to main root path", s.RootPathAnime); animePath = int.Parse(s.RootPath); // Set it to the main root folder if we have no anime folder. } + Logger.LogInformation("Using anime path ID: {AnimePath}", animePath); rootFolderPath = await GetSonarrRootPath(animePath, s); languageProfileId = s.LanguageProfileAnime > 0 ? s.LanguageProfileAnime : s.LanguageProfile; @@ -154,6 +163,7 @@ namespace Ombi.Core.Senders { if (profiles.SonarrRootPathAnime > 0) { + Logger.LogInformation("Using user's anime root path override: {RootPath}", profiles.SonarrRootPathAnime); rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPathAnime, s); } if (profiles.SonarrQualityProfileAnime > 0) @@ -169,11 +179,13 @@ namespace Ombi.Core.Senders int.TryParse(s.QualityProfile, out qualityToUse); // Get the root path from the rootfolder selected. // For some reason, if we haven't got one use the first root folder in Sonarr + Logger.LogInformation("Using standard path ID: {RootPath}", s.RootPath); rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPath), s); if (profiles != null) { if (profiles.SonarrRootPath > 0) { + Logger.LogInformation("Using user's standard root path override: {RootPath}", profiles.SonarrRootPath); rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPath, s); } if (profiles.SonarrQualityProfile > 0) @@ -193,6 +205,7 @@ namespace Ombi.Core.Senders if (model.ParentRequest.RootFolder.HasValue && model.ParentRequest.RootFolder.Value > 0) { + Logger.LogInformation("Using request root folder override: {RootFolder}", model.ParentRequest.RootFolder.Value); rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder.Value, s); } @@ -201,6 +214,8 @@ namespace Ombi.Core.Senders languageProfileId = model.ParentRequest.LanguageProfile.Value; } + Logger.LogInformation("Final root folder path: {RootFolderPath}", rootFolderPath); + try { if (tagToUse.HasValue) @@ -520,17 +535,36 @@ namespace Ombi.Core.Senders private async Task GetSonarrRootPath(int pathId, SonarrSettings sonarrSettings) { + Logger.LogInformation("Getting Sonarr root path for ID: {PathId}", pathId); var rootFoldersResult = await SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri); + + if (rootFoldersResult == null || !rootFoldersResult.Any()) + { + Logger.LogError("No root folders returned from Sonarr API"); + return string.Empty; + } + + Logger.LogInformation("Found {Count} root folders in Sonarr", rootFoldersResult.Count()); + foreach (var folder in rootFoldersResult) + { + Logger.LogDebug("Root folder - ID: {Id}, Path: {Path}", folder.id, folder.path); + } if (pathId == 0) { - return rootFoldersResult.FirstOrDefault().path; + var defaultPath = rootFoldersResult.FirstOrDefault()?.path; + Logger.LogInformation("Using first root folder as default: {Path}", defaultPath); + return defaultPath; } - foreach (var r in rootFoldersResult?.Where(r => r.id == pathId)) + var matchingFolder = rootFoldersResult.FirstOrDefault(r => r.id == pathId); + if (matchingFolder != null) { - return r.path; + Logger.LogInformation("Found matching root folder for ID {PathId}: {Path}", pathId, matchingFolder.path); + return matchingFolder.path; } + + Logger.LogError("No matching root folder found for ID: {PathId}", pathId); return string.Empty; } } 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) {
@@ -13,21 +13,9 @@
-}@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 d30c9adfb..dc01ed894 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "4.48.0" + "version": "4.49.3" }