Compare commits

..

20 commits

Author SHA1 Message Date
Conventional Changelog Action
b72f47470c chore(release): 🚀 v4.49.3 [skip ci] 2025-08-17 16:01:24 +00:00
Jamie Rees
72d4115378
Merge pull request #5248 from emma-the-rock/patch-1
fix(plex-api): update Plex Watchlist URL
2025-08-17 17:58:51 +02:00
emmatherock
11fd7a5fc8
fix(plex-api): update Plex Watchlist URL 2025-08-14 21:17:10 -03:00
Conventional Changelog Action
d2be48a921 chore(release): 🚀 v4.49.2 [skip ci] 2025-07-12 21:47:40 +00:00
tidusjar
a92c76021a Merge branch 'develop' of https://github.com/tidusjar/ombi into develop 2025-07-12 22:45:50 +01:00
tidusjar
97d5167db6 perf(discover): Improve the loading performance on the discover page 2025-07-12 22:35:11 +01:00
Conventional Changelog Action
2519cca9f6 chore(release): 🚀 v4.49.1 [skip ci] 2025-07-12 21:27:34 +00:00
tidusjar
cfeee39978 Merge remote-tracking branch 'origin/develop' into develop 2025-07-12 22:25:45 +01:00
tidusjar
cee40146ee fix(auth): Fixed an issue where refreshing the page as a power user would stop the application from loading #5242 2025-07-12 22:25:31 +01:00
Conventional Changelog Action
1eff48e58e chore(release): 🚀 v4.49.0 [skip ci] 2025-07-11 21:51:32 +00:00
tidusjar
3b2a0d84be fix 2025-07-11 22:49:21 +01:00
contrib-readme-bot
ed5bc3f873 chore: 👥 Updated Contributors [skip ci] 2025-07-11 21:20:26 +00:00
tidusjar
067c029f42 feat: Added the ability for the Watchlist to automatically refresh the users token. This will reduce the need for the user to log in 2025-07-11 22:19:10 +01:00
Conventional Changelog Action
cfe2b6ac0f chore(release): 🚀 v4.48.5 [skip ci] 2025-05-14 21:18:32 +00:00
tidusjar
c9ab4f4f9f fix: filter out excluded notification agents from user preferences
The webhook notification field was inconsistently showing up for some users despite being excluded in the backend. This was happening because the frontend was displaying all notification preferences without filtering out the excluded agents.

Changes:
- Added excludedAgents array to match backend's excluded notification types
- Filter notification preferences in both edit and create user flows
- Prevents webhook, email, and mobile notification fields from appearing in user preferences

This change aligns the frontend behavior with the backend's intended design where webhook notifications are managed globally rather than per-user.

Fixes #5196
2025-05-14 22:16:34 +01:00
Conventional Changelog Action
acb679f99d chore(release): 🚀 v4.48.4 [skip ci] 2025-05-14 21:11:38 +00:00
tidusjar
f88c5ad818 fix(ui): correct timezone handling in OmbiDatePipe
- Replace native Date constructor with date-fns parseISO for proper UTC parsing
- Use date-fns format function for consistent timezone conversion
- Add null check for input value
- Fix issue where request times were showing incorrect timezone offset

This fixes GitHub issue #5102 where request times were showing different times than the host machine.
2025-05-14 22:09:46 +01:00
Jamie Rees
b3e8ca6950
Merge pull request #5192 from Ombi-app/translations
[skip ci]
2025-05-14 22:03:07 +01:00
Jamie Rees
dbbfdd926f fix(translations): 🌐 New translations from Crowdin [skip ci] 2025-05-13 15:38:31 +01:00
Jamie Rees
53a6a092b1 fix(translations): 🌐 New translations from Crowdin [skip ci] 2024-11-30 19:09:51 +00:00
21 changed files with 497 additions and 132 deletions

View file

@ -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) ## [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)

View file

@ -122,10 +122,10 @@ Here are some of the features Ombi has:
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/MattJeanes"> <a href="https://github.com/AmyJeanes">
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="MattJeanes"/> <img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="AmyJeanes"/>
<br /> <br />
<sub><b>Matt Jeanes</b></sub> <sub><b>Amy Jeanes</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">

View file

@ -29,5 +29,6 @@ namespace Ombi.Api.Plex
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs); Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken); Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken);
Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken); Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
Task<bool> Ping(string authToken, CancellationToken cancellationToken = default);
} }
} }

View file

@ -68,7 +68,7 @@ namespace Ombi.Api.Plex
private const string FriendsUri = "https://plex.tv/api/users"; private const string FriendsUri = "https://plex.tv/api/users";
private const string GetAccountUri = "https://plex.tv/users/account.json"; private const string GetAccountUri = "https://plex.tv/users/account.json";
private const string ServerUri = "https://plex.tv/pms/servers.xml"; 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/";
/// <summary> /// <summary>
/// Sign into the Plex API /// Sign into the Plex API
@ -320,6 +320,30 @@ namespace Ombi.Api.Plex
return result; return result;
} }
/// <summary>
/// Pings the Plex API to validate if a token is still valid
/// </summary>
/// <param name="authToken">The authentication token to validate</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the token is valid, false otherwise</returns>
public async Task<bool> Ping(string authToken, CancellationToken cancellationToken = default)
{
try
{
var request = new Request("api/v2/ping", "https://plex.tv/", HttpMethod.Get);
await AddHeaders(request, authToken);
// We don't need to parse the response, just check if the request succeeds
await Api.Request(request, cancellationToken);
return true;
}
catch
{
// If the request fails (401, 403, etc.), the token is invalid
return false;
}
}
/// <summary> /// <summary>
/// Adds the required headers and also the authorization header /// Adds the required headers and also the authorization header

View file

@ -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<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken);
}
public class PlexTokenKeepAliveService : IPlexTokenKeepAliveService
{
private readonly IPlexApi _plexApi;
private readonly ILogger<PlexTokenKeepAliveService> _logger;
public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger<PlexTokenKeepAliveService> logger)
{
_plexApi = plexApi;
_logger = logger;
}
public async Task<bool> 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;
}
}
}
}

View file

@ -107,6 +107,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicSender, MusicSender>(); services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>(); services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>(); services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
services.AddTransient<IPlexTokenKeepAliveService, PlexTokenKeepAliveService>();
services.AddTransient<IVoteEngine, VoteEngine>(); services.AddTransient<IVoteEngine, VoteEngine>();
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>(); services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>(); services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();

View file

@ -24,6 +24,7 @@ using Ombi.Notifications.Models;
using Ombi.Core.Notifications; using Ombi.Core.Notifications;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Core; using Ombi.Core;
using Ombi.Core.Authentication;
namespace Ombi.Schedule.Tests namespace Ombi.Schedule.Tests
{ {
@ -43,6 +44,8 @@ namespace Ombi.Schedule.Tests
_mocker.Use(um); _mocker.Use(um);
_context = _mocker.GetMock<IJobExecutionContext>(); _context = _mocker.GetMock<IJobExecutionContext>();
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None); _context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
// Mock the keep-alive service to return true by default
_mocker.Use<IPlexTokenKeepAliveService>(Mock.Of<IPlexTokenKeepAliveService>(s => s.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()) == Task.FromResult(true)));
_subject = _mocker.CreateInstance<PlexWatchlistImport>(); _subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock()); _mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
_mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>())); _mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()));
@ -838,5 +841,43 @@ namespace Ombi.Schedule.Tests
// Assert // Assert
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never); _mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never);
} }
[Test]
public async Task SkipsUserIfTokenKeepAliveFails()
{
// Arrange: Set up the keep-alive service to return false (token invalid/expired)
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
_mocker.Use(keepAliveMock.Object);
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
// Act
await _subject.Execute(_context.Object);
// Assert: Should not attempt to import watchlist if keep-alive fails
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
_mocker.Verify<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()), Times.Never); // or Times.Once if notification is expected
}
[Test]
public async Task CallsKeepAliveForEachPlexUser()
{
// Arrange: Multiple Plex users
var users = new List<OmbiUser>
{
new OmbiUser { Id = "abc1", UserType = UserType.PlexUser, MediaServerToken = "abc1", UserName = "abc1", NormalizedUserName = "ABC1" },
new OmbiUser { Id = "abc2", UserType = UserType.PlexUser, MediaServerToken = "abc2", UserName = "abc2", NormalizedUserName = "ABC2" },
};
var um = MockHelper.MockUserManager(users);
_mocker.Use(um);
var keepAliveMock = new Mock<IPlexTokenKeepAliveService>();
keepAliveMock.Setup(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(true);
_mocker.Use(keepAliveMock.Object);
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
// Act
await _subject.Execute(_context.Object);
// Assert: KeepAlive should be called for each user
keepAliveMock.Verify(x => x.KeepTokenAliveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(users.Count));
}
} }
} }

View file

@ -43,11 +43,12 @@ namespace Ombi.Schedule.Jobs.Plex
private readonly IRepository<PlexWatchlistUserError> _userError; private readonly IRepository<PlexWatchlistUserError> _userError;
private readonly IMovieDbApi _movieDbApi; private readonly IMovieDbApi _movieDbApi;
private readonly INotificationHelper _notificationHelper; private readonly INotificationHelper _notificationHelper;
private readonly IPlexTokenKeepAliveService _tokenKeepAliveService;
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager, public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService, IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError, ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError,
IMovieDbApi movieDbApi, INotificationHelper notificationHelper) IMovieDbApi movieDbApi, INotificationHelper notificationHelper, IPlexTokenKeepAliveService tokenKeepAliveService)
{ {
_plexApi = plexApi; _plexApi = plexApi;
_settings = settings; _settings = settings;
@ -60,6 +61,7 @@ namespace Ombi.Schedule.Jobs.Plex
_userError = userError; _userError = userError;
_movieDbApi = movieDbApi; _movieDbApi = movieDbApi;
_notificationHelper = notificationHelper; _notificationHelper = notificationHelper;
_tokenKeepAliveService = tokenKeepAliveService;
} }
public async Task Execute(IJobExecutionContext context) public async Task Execute(IJobExecutionContext context)
@ -97,6 +99,36 @@ namespace Ombi.Schedule.Jobs.Plex
} }
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}"); _logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
// Keep the token alive before attempting watchlist import
var keepAliveSuccess = await _tokenKeepAliveService.KeepTokenAliveAsync(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
if (!keepAliveSuccess)
{
_logger.LogWarning($"Token for user '{user.UserName}' is invalid or expired (keep-alive failed). Recording error and skipping.");
await _userError.Add(new PlexWatchlistUserError
{
UserId = user.Id,
MediaServerToken = user.MediaServerToken,
});
// Send notification to user about token expiration
if (settings.NotifyOnWatchlistTokenExpiration && !string.IsNullOrEmpty(user.Email))
{
var notificationModel = new NotificationOptions
{
NotificationType = NotificationType.PlexWatchlistTokenExpired,
Recipient = user.Email,
DateTime = DateTime.Now,
Substitutes = new Dictionary<string, string>
{
{ "UserName", user.UserName }
}
};
await _notificationHelper.Notify(notificationModel);
}
continue;
}
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None); var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
if (watchlist?.AuthError ?? false) if (watchlist?.AuthError ?? false)
{ {

View file

@ -5,13 +5,17 @@
<mat-button-toggle id="{{id}}Tv" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Tv}" value="{{DiscoverOption.Tv}}" class="discover-filter-button">{{'Discovery.Tv' | translate}}</mat-button-toggle> <mat-button-toggle id="{{id}}Tv" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Tv}" value="{{DiscoverOption.Tv}}" class="discover-filter-button">{{'Discovery.Tv' | translate}}</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
@defer (when discoverResults.length > 0) { @defer (when discoverResults.length > 0; prefetch on idle) {
<p-carousel #carousel [numVisible]="10" [numScroll]="10" [page]="0" [value]="discoverResults" [responsiveOptions]="responsiveOptions" (onPage)="newPage()"> <p-carousel #carousel [numVisible]="10" [numScroll]="10" [page]="0" [value]="discoverResults" [responsiveOptions]="responsiveOptions" (onPage)="newPage()">
<ng-template let-result pTemplate="item"> <ng-template let-result pTemplate="item">
<discover-card [discoverType]="discoverType" [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card> <discover-card [discoverType]="discoverType" [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
</ng-template> </ng-template>
</p-carousel> </p-carousel>
} }
@placeholder(minimum 500) { @placeholder(minimum 300) {
<p-skeleton width="100%" height="18rem"></p-skeleton> <div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
} }

View file

@ -105,6 +105,30 @@
padding: 5px; 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){ @media (min-width:755px){
::ng-deep .p-carousel-item{ ::ng-deep .p-carousel-item{
flex: 1 0 200px !important; flex: 1 0 200px !important;

View file

@ -43,7 +43,7 @@ export class CarouselListComponent implements OnInit {
get mediaTypeStorageKey() { get mediaTypeStorageKey() {
return "DiscoverOptions" + this.discoverType.toString(); return "DiscoverOptions" + this.discoverType.toString();
}; };
private amountToLoad = 17; private amountToLoad = 10;
private currentlyLoaded = 0; private currentlyLoaded = 0;
private baseUrl: string = ""; private baseUrl: string = "";
@ -148,6 +148,7 @@ export class CarouselListComponent implements OnInit {
} }
public async ngOnInit() { public async ngOnInit() {
this.is4kEnabled = this.featureFacade.is4kEnabled(); this.is4kEnabled = this.featureFacade.is4kEnabled();
this.currentlyLoaded = 0; this.currentlyLoaded = 0;
const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey);
@ -155,11 +156,15 @@ export class CarouselListComponent implements OnInit {
this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]];
} }
let currentIteration = 0; // Load initial data - just enough to fill the first carousel page
while (this.discoverResults.length <= 14 && currentIteration <= 3) { // This reduces initial API calls and improves loading performance
currentIteration++; 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); await this.loadData(false);
} }
} }
public async toggleChanged(event: MatButtonToggleChange) { public async toggleChanged(event: MatButtonToggleChange) {

View file

@ -1,46 +1,108 @@
<div class="small-middle-container"> <div class="small-middle-container">
<div class="section"> @defer (on viewport; prefetch on idle) {
<h2 id="genreHeading" data-toggle="collapse" href="#genreCollapse" role="button">{{ 'Discovery.Genres' | translate }}</h2> <div class="section">
<genre-button-select class="collapse show" id="genreCollapse"></genre-button-select> <h2 id="genreHeading" data-toggle="collapse" href="#genreCollapse" role="button">{{ 'Discovery.Genres' | translate }}</h2>
</div> <genre-button-select class="collapse show" id="genreCollapse"></genre-button-select>
<div class="section">
<h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
<div>
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
</div> </div>
</div> } @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.Genres' | translate }}</h2>
<div class="section" [hidden]="!showSeasonal"> <p-skeleton width="100%" height="60px"></p-skeleton>
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
<div>
<carousel-list
[id]="'seasonal'"
[isAdmin]="isAdmin"
[discoverType]="DiscoverType.Seasonal"
(movieCount)="setSeasonalMovieCount($event)"
></carousel-list>
</div> </div>
</div> }
<div class="section"> @defer (on viewport; prefetch on idle) {
<h2>{{ 'Discovery.PopularTab' | translate }}</h2> <div class="section">
<div> <h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
<carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list> <div>
<ombi-recently-list [id]="'recentlyRequested'"></ombi-recently-list>
</div>
</div> </div>
</div> } @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.RecentlyRequestedTab' | translate }}</h2>
<div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
</div>
}
<div class="section"> @defer (on viewport; prefetch on idle) {
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2> <div class="section" [hidden]="!showSeasonal">
<div> <h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
<carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list> <div>
<carousel-list
[id]="'seasonal'"
[isAdmin]="isAdmin"
[discoverType]="DiscoverType.Seasonal"
(movieCount)="setSeasonalMovieCount($event)"
></carousel-list>
</div>
</div> </div>
</div> } @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
<div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
</div>
}
<div class="section"> @defer (on viewport; prefetch on idle) {
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2> <div class="section">
<div> <h2>{{ 'Discovery.PopularTab' | translate }}</h2>
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list> <div>
<carousel-list [id]="'popular'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Popular"></carousel-list>
</div>
</div> </div>
</div> } @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.PopularTab' | translate }}</h2>
<div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
</div>
}
@defer (on viewport; prefetch on idle) {
<div class="section">
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2>
<div>
<carousel-list [id]="'trending'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Trending"></carousel-list>
</div>
</div>
} @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.TrendingTab' | translate }}</h2>
<div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
</div>
}
@defer (on viewport; prefetch on idle) {
<div class="section">
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2>
<div>
<carousel-list [id]="'upcoming'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Upcoming"></carousel-list>
</div>
</div>
} @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.UpcomingTab' | translate }}</h2>
<div class="row loading-container">
<div class="col-2" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
</div>
</div>
}
</div> </div>

View file

@ -9,4 +9,28 @@ h2{
margin-top:40px; margin-top:40px;
margin-left:40px; margin-left:40px;
font-size: 24px; 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);
}
} }

View file

@ -1,4 +1,4 @@
@defer (when requests()) { @defer (when requests(); prefetch on idle) {
<div *ngIf="requests().length > 0"> <div *ngIf="requests().length > 0">
<p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1" <p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1"
[responsiveOptions]="responsiveOptions" [page]="0"> [responsiveOptions]="responsiveOptions" [page]="0">
@ -13,21 +13,9 @@
</ng-template> </ng-template>
</p-carousel> </p-carousel>
</div> </div>
}@placeholder(minimum 500) { }@placeholder(minimum 300) {
<div class="row loading-container"> <div class="row loading-container">
<div class="col-2"> <div class="col-2" *ngFor="let item of [1,2,3,4,5]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton> <p-skeleton width="100%" height="270px"></p-skeleton>
</div> </div>
</div> </div>

View file

@ -105,12 +105,32 @@
padding: 5px; 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){ @media (min-width:755px){
::ng-deep .p-carousel-item{ ::ng-deep .p-carousel-item{
flex: 1 0 200px !important; flex: 1 0 200px !important;
} }
}
.loading-container {
margin-left: 10rem;
} }

View file

@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from "@angular/core"; import { Pipe, PipeTransform } from "@angular/core";
import { FormatPipe } from 'ngx-date-fns'; import { FormatPipe } from 'ngx-date-fns';
import { parseISO, format } from 'date-fns';
@Pipe({ @Pipe({
name: "ombiDate", name: "ombiDate",
@ -10,8 +11,16 @@ export class OmbiDatePipe implements PipeTransform {
private FormatPipe: FormatPipe, private FormatPipe: FormatPipe,
) {} ) {}
public transform(value: string, format: string ) { public transform(value: string, formatStr: string ) {
const date = new Date(value); if (!value) {
return this.FormatPipe.transform(date, format); 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);
} }
} }

View file

@ -37,6 +37,13 @@ export class UserManagementUserComponent implements OnInit {
private appUrl: string = this.customizationFacade.appUrl(); private appUrl: string = this.customizationFacade.appUrl();
private accessToken: string; 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, constructor(private identityService: IdentityService,
private notificationService: MessageService, private notificationService: MessageService,
private router: Router, private router: Router,
@ -74,9 +81,15 @@ export class UserManagementUserComponent implements OnInit {
} }
}); });
if(this.edit) { 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 { } 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.sonarrService.getQualityProfilesWithoutSettings().subscribe(x => {
this.sonarrQualities = x; this.sonarrQualities = x;

View file

@ -40,7 +40,6 @@ namespace Ombi.Controllers.V1
/// <summary> /// <summary>
/// The Settings Controller /// The Settings Controller
/// </summary> /// </summary>
[Admin]
[ApiV1] [ApiV1]
[Produces("application/json")] [Produces("application/json")]
[ApiController] [ApiController]
@ -78,6 +77,7 @@ namespace Ombi.Controllers.V1
/// Gets the Ombi settings. /// Gets the Ombi settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("ombi")] [HttpGet("ombi")]
public async Task<OmbiSettings> OmbiSettings() public async Task<OmbiSettings> OmbiSettings()
{ {
@ -110,6 +110,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="ombi">The ombi.</param> /// <param name="ombi">The ombi.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("ombi")] [HttpPost("ombi")]
public async Task<bool> OmbiSettings([FromBody]OmbiSettings ombi) public async Task<bool> OmbiSettings([FromBody]OmbiSettings ombi)
{ {
@ -145,6 +146,7 @@ namespace Ombi.Controllers.V1
return model; return model;
} }
[Admin]
[HttpPost("ombi/resetApi")] [HttpPost("ombi/resetApi")]
public async Task<string> ResetApiKey() public async Task<string> ResetApiKey()
{ {
@ -159,6 +161,7 @@ namespace Ombi.Controllers.V1
/// Gets the Plex Settings. /// Gets the Plex Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("plex")] [HttpGet("plex")]
public async Task<PlexSettings> PlexSettings() public async Task<PlexSettings> PlexSettings()
{ {
@ -185,6 +188,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="plex">The plex.</param> /// <param name="plex">The plex.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("plex")] [HttpPost("plex")]
public async Task<bool> PlexSettings([FromBody]PlexSettings plex) public async Task<bool> PlexSettings([FromBody]PlexSettings plex)
{ {
@ -207,6 +211,7 @@ namespace Ombi.Controllers.V1
/// Gets the Emby Settings. /// Gets the Emby Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("emby")] [HttpGet("emby")]
public async Task<EmbySettings> EmbySettings() public async Task<EmbySettings> EmbySettings()
{ {
@ -218,6 +223,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="emby">The emby.</param> /// <param name="emby">The emby.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("emby")] [HttpPost("emby")]
public async Task<bool> EmbySettings([FromBody]EmbySettings emby) public async Task<bool> EmbySettings([FromBody]EmbySettings emby)
{ {
@ -243,6 +249,7 @@ namespace Ombi.Controllers.V1
/// Gets the Jellyfin Settings. /// Gets the Jellyfin Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("jellyfin")] [HttpGet("jellyfin")]
public async Task<JellyfinSettings> JellyfinSettings() public async Task<JellyfinSettings> JellyfinSettings()
{ {
@ -254,6 +261,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="jellyfin">The jellyfin.</param> /// <param name="jellyfin">The jellyfin.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("jellyfin")] [HttpPost("jellyfin")]
public async Task<bool> JellyfinSettings([FromBody]JellyfinSettings jellyfin) public async Task<bool> JellyfinSettings([FromBody]JellyfinSettings jellyfin)
{ {
@ -291,6 +299,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("landingpage")] [HttpPost("landingpage")]
public async Task<bool> LandingPageSettings([FromBody]LandingPageSettings settings) public async Task<bool> LandingPageSettings([FromBody]LandingPageSettings settings)
{ {
@ -326,6 +335,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("customization")] [HttpPost("customization")]
public async Task<bool> CustomizationSettings([FromBody]CustomizationSettings settings) public async Task<bool> CustomizationSettings([FromBody]CustomizationSettings settings)
{ {
@ -344,6 +354,7 @@ namespace Ombi.Controllers.V1
/// Get's the preset themes available /// Get's the preset themes available
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("themes")] [HttpGet("themes")]
public async Task<IEnumerable<PresetThemeViewModel>> GetThemes() public async Task<IEnumerable<PresetThemeViewModel>> GetThemes()
{ {
@ -389,6 +400,7 @@ namespace Ombi.Controllers.V1
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("sonarr")] [HttpPost("sonarr")]
[Admin]
public async Task<bool> SonarrSettings([FromBody]SonarrSettings settings) public async Task<bool> SonarrSettings([FromBody]SonarrSettings settings)
{ {
var result = await Save(settings); var result = await Save(settings);
@ -418,6 +430,7 @@ namespace Ombi.Controllers.V1
/// Gets the Lidarr Settings. /// Gets the Lidarr Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("lidarr")] [HttpGet("lidarr")]
public async Task<LidarrSettings> LidarrSettings() public async Task<LidarrSettings> LidarrSettings()
{ {
@ -441,6 +454,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("lidarr")] [HttpPost("lidarr")]
public async Task<bool> LidarrSettings([FromBody]LidarrSettings settings) public async Task<bool> LidarrSettings([FromBody]LidarrSettings settings)
{ {
@ -457,6 +471,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("authentication")] [HttpPost("authentication")]
public async Task<bool> AuthenticationsSettings([FromBody]AuthenticationSettings settings) public async Task<bool> AuthenticationsSettings([FromBody]AuthenticationSettings settings)
{ {
@ -479,6 +494,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("radarr")] [HttpPost("radarr")]
public async Task<bool> RadarrSettings([FromBody]RadarrCombinedModel settings) public async Task<bool> RadarrSettings([FromBody]RadarrCombinedModel settings)
{ {
@ -500,6 +516,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("Update")] [HttpPost("Update")]
public async Task<bool> UpdateSettings([FromBody]UpdateSettings settings) public async Task<bool> UpdateSettings([FromBody]UpdateSettings settings)
{ {
@ -510,6 +527,7 @@ namespace Ombi.Controllers.V1
/// Gets the UserManagement Settings. /// Gets the UserManagement Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("UserManagement")] [HttpGet("UserManagement")]
public async Task<UserManagementSettings> UserManagementSettings() public async Task<UserManagementSettings> UserManagementSettings()
{ {
@ -521,6 +539,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("UserManagement")] [HttpPost("UserManagement")]
public async Task<bool> UserManagementSettings([FromBody]UserManagementSettings settings) public async Task<bool> UserManagementSettings([FromBody]UserManagementSettings settings)
{ {
@ -531,6 +550,7 @@ namespace Ombi.Controllers.V1
/// Gets the Update Settings. /// Gets the Update Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("Update")] [HttpGet("Update")]
public async Task<UpdateSettings> UpdateSettings() public async Task<UpdateSettings> UpdateSettings()
{ {
@ -543,6 +563,7 @@ namespace Ombi.Controllers.V1
/// Gets the CouchPotatoSettings Settings. /// Gets the CouchPotatoSettings Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("CouchPotato")] [HttpGet("CouchPotato")]
public async Task<CouchPotatoSettings> CouchPotatoSettings() public async Task<CouchPotatoSettings> CouchPotatoSettings()
{ {
@ -554,6 +575,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("CouchPotato")] [HttpPost("CouchPotato")]
public async Task<bool> CouchPotatoSettings([FromBody]CouchPotatoSettings settings) public async Task<bool> CouchPotatoSettings([FromBody]CouchPotatoSettings settings)
{ {
@ -564,6 +586,7 @@ namespace Ombi.Controllers.V1
/// Gets the DogNzbSettings Settings. /// Gets the DogNzbSettings Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("DogNzb")] [HttpGet("DogNzb")]
public async Task<DogNzbSettings> DogNzbSettings() public async Task<DogNzbSettings> DogNzbSettings()
{ {
@ -575,6 +598,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("DogNzb")] [HttpPost("DogNzb")]
public async Task<bool> DogNzbSettings([FromBody]DogNzbSettings settings) public async Task<bool> DogNzbSettings([FromBody]DogNzbSettings settings)
{ {
@ -586,6 +610,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("SickRage")] [HttpPost("SickRage")]
public async Task<bool> SickRageSettings([FromBody]SickRageSettings settings) public async Task<bool> SickRageSettings([FromBody]SickRageSettings settings)
{ {
@ -596,6 +621,7 @@ namespace Ombi.Controllers.V1
/// Gets the SickRage Settings. /// Gets the SickRage Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("SickRage")] [HttpGet("SickRage")]
public async Task<SickRageSettings> SickRageSettings() public async Task<SickRageSettings> SickRageSettings()
{ {
@ -606,6 +632,7 @@ namespace Ombi.Controllers.V1
/// Gets the JobSettings Settings. /// Gets the JobSettings Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("jobs")] [HttpGet("jobs")]
public async Task<JobSettings> JobSettings() public async Task<JobSettings> JobSettings()
{ {
@ -638,6 +665,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("jobs")] [HttpPost("jobs")]
public async Task<JobSettingsViewModel> JobSettings([FromBody]JobSettings settings) public async Task<JobSettingsViewModel> JobSettings([FromBody]JobSettings settings)
{ {
@ -681,6 +709,7 @@ namespace Ombi.Controllers.V1
} }
[HttpPost("testcron")] [HttpPost("testcron")]
[Admin]
public CronTestModel TestCron([FromBody] CronViewModelBody body) public CronTestModel TestCron([FromBody] CronViewModelBody body)
{ {
var model = new CronTestModel(); var model = new CronTestModel();
@ -714,6 +743,7 @@ namespace Ombi.Controllers.V1
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("Issues")] [HttpPost("Issues")]
[Admin]
public async Task<bool> IssueSettings([FromBody]IssueSettings settings) public async Task<bool> IssueSettings([FromBody]IssueSettings settings)
{ {
return await Save(settings); return await Save(settings);
@ -744,6 +774,7 @@ namespace Ombi.Controllers.V1
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("vote")] [HttpPost("vote")]
[Admin]
public async Task<bool> VoteSettings([FromBody]VoteSettings settings) public async Task<bool> VoteSettings([FromBody]VoteSettings settings)
{ {
return await Save(settings); return await Save(settings);
@ -754,6 +785,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[HttpGet("vote")] [HttpGet("vote")]
[Admin]
public async Task<VoteSettings> VoteSettings() public async Task<VoteSettings> VoteSettings()
{ {
return await Get<VoteSettings>(); return await Get<VoteSettings>();
@ -772,6 +804,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
[HttpPost("themoviedb")] [HttpPost("themoviedb")]
[Admin]
public async Task<bool> TheMovieDbSettings([FromBody]TheMovieDbSettings settings) public async Task<bool> TheMovieDbSettings([FromBody]TheMovieDbSettings settings)
{ {
return await Save(settings); return await Save(settings);
@ -780,6 +813,7 @@ namespace Ombi.Controllers.V1
/// <summary> /// <summary>
/// Get The Movie DB settings. /// Get The Movie DB settings.
/// </summary> /// </summary>
[Admin]
[HttpGet("themoviedb")] [HttpGet("themoviedb")]
public async Task<TheMovieDbSettings> TheMovieDbSettings() public async Task<TheMovieDbSettings> TheMovieDbSettings()
{ {
@ -791,6 +825,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/email")] [HttpPost("notifications/email")]
public async Task<bool> EmailNotificationSettings([FromBody] EmailNotificationsViewModel model) public async Task<bool> EmailNotificationSettings([FromBody] EmailNotificationsViewModel model)
{ {
@ -808,6 +843,7 @@ namespace Ombi.Controllers.V1
/// Gets the Email Notification Settings. /// Gets the Email Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/email")] [HttpGet("notifications/email")]
public async Task<EmailNotificationsViewModel> EmailNotificationSettings() public async Task<EmailNotificationsViewModel> EmailNotificationSettings()
{ {
@ -838,6 +874,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/discord")] [HttpPost("notifications/discord")]
public async Task<bool> DiscordNotificationSettings([FromBody] DiscordNotificationsViewModel model) public async Task<bool> DiscordNotificationSettings([FromBody] DiscordNotificationsViewModel model)
{ {
@ -855,6 +892,7 @@ namespace Ombi.Controllers.V1
/// Gets the discord Notification Settings. /// Gets the discord Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/discord")] [HttpGet("notifications/discord")]
public async Task<DiscordNotificationsViewModel> DiscordNotificationSettings() public async Task<DiscordNotificationsViewModel> DiscordNotificationSettings()
{ {
@ -873,6 +911,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/telegram")] [HttpPost("notifications/telegram")]
public async Task<bool> TelegramNotificationSettings([FromBody] TelegramNotificationsViewModel model) public async Task<bool> TelegramNotificationSettings([FromBody] TelegramNotificationsViewModel model)
{ {
@ -890,6 +929,7 @@ namespace Ombi.Controllers.V1
/// Gets the telegram Notification Settings. /// Gets the telegram Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/telegram")] [HttpGet("notifications/telegram")]
public async Task<TelegramNotificationsViewModel> TelegramNotificationSettings() public async Task<TelegramNotificationsViewModel> TelegramNotificationSettings()
{ {
@ -907,6 +947,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/pushbullet")] [HttpPost("notifications/pushbullet")]
public async Task<bool> PushbulletNotificationSettings([FromBody] PushbulletNotificationViewModel model) public async Task<bool> PushbulletNotificationSettings([FromBody] PushbulletNotificationViewModel model)
{ {
@ -924,6 +965,7 @@ namespace Ombi.Controllers.V1
/// Gets the pushbullet Notification Settings. /// Gets the pushbullet Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/pushbullet")] [HttpGet("notifications/pushbullet")]
public async Task<PushbulletNotificationViewModel> PushbulletNotificationSettings() public async Task<PushbulletNotificationViewModel> PushbulletNotificationSettings()
{ {
@ -941,6 +983,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/pushover")] [HttpPost("notifications/pushover")]
public async Task<bool> PushoverNotificationSettings([FromBody] PushoverNotificationViewModel model) public async Task<bool> PushoverNotificationSettings([FromBody] PushoverNotificationViewModel model)
{ {
@ -958,6 +1001,7 @@ namespace Ombi.Controllers.V1
/// Gets the pushover Notification Settings. /// Gets the pushover Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/pushover")] [HttpGet("notifications/pushover")]
public async Task<PushoverNotificationViewModel> PushoverNotificationSettings() public async Task<PushoverNotificationViewModel> PushoverNotificationSettings()
{ {
@ -976,6 +1020,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/slack")] [HttpPost("notifications/slack")]
public async Task<bool> SlacktNotificationSettings([FromBody] SlackNotificationsViewModel model) public async Task<bool> SlacktNotificationSettings([FromBody] SlackNotificationsViewModel model)
{ {
@ -993,6 +1038,7 @@ namespace Ombi.Controllers.V1
/// Gets the slack Notification Settings. /// Gets the slack Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/slack")] [HttpGet("notifications/slack")]
public async Task<SlackNotificationsViewModel> SlackNotificationSettings() public async Task<SlackNotificationsViewModel> SlackNotificationSettings()
{ {
@ -1010,6 +1056,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/mattermost")] [HttpPost("notifications/mattermost")]
public async Task<bool> MattermostNotificationSettings([FromBody] MattermostNotificationsViewModel model) public async Task<bool> MattermostNotificationSettings([FromBody] MattermostNotificationsViewModel model)
{ {
@ -1027,6 +1074,7 @@ namespace Ombi.Controllers.V1
/// Gets the Mattermost Notification Settings. /// Gets the Mattermost Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/mattermost")] [HttpGet("notifications/mattermost")]
public async Task<MattermostNotificationsViewModel> MattermostNotificationSettings() public async Task<MattermostNotificationsViewModel> MattermostNotificationSettings()
{ {
@ -1043,6 +1091,7 @@ namespace Ombi.Controllers.V1
/// Gets the Twilio Notification Settings. /// Gets the Twilio Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/twilio")] [HttpGet("notifications/twilio")]
public async Task<TwilioSettingsViewModel> TwilioNotificationSettings() public async Task<TwilioSettingsViewModel> TwilioNotificationSettings()
{ {
@ -1064,6 +1113,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/twilio")] [HttpPost("notifications/twilio")]
public async Task<bool> TwilioNotificationSettings([FromBody] TwilioSettingsViewModel model) public async Task<bool> TwilioNotificationSettings([FromBody] TwilioSettingsViewModel model)
{ {
@ -1082,6 +1132,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/mobile")] [HttpPost("notifications/mobile")]
public async Task<bool> MobileNotificationSettings([FromBody] MobileNotificationsViewModel model) public async Task<bool> MobileNotificationSettings([FromBody] MobileNotificationsViewModel model)
{ {
@ -1099,6 +1150,7 @@ namespace Ombi.Controllers.V1
/// Gets the Mobile Notification Settings. /// Gets the Mobile Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/mobile")] [HttpGet("notifications/mobile")]
public async Task<MobileNotificationsViewModel> MobileNotificationSettings() public async Task<MobileNotificationsViewModel> MobileNotificationSettings()
{ {
@ -1116,6 +1168,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/gotify")] [HttpPost("notifications/gotify")]
public async Task<bool> GotifyNotificationSettings([FromBody] GotifyNotificationViewModel model) public async Task<bool> GotifyNotificationSettings([FromBody] GotifyNotificationViewModel model)
{ {
@ -1133,6 +1186,7 @@ namespace Ombi.Controllers.V1
/// Gets the gotify Notification Settings. /// Gets the gotify Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/gotify")] [HttpGet("notifications/gotify")]
public async Task<GotifyNotificationViewModel> GotifyNotificationSettings() public async Task<GotifyNotificationViewModel> GotifyNotificationSettings()
{ {
@ -1150,6 +1204,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/webhook")] [HttpPost("notifications/webhook")]
public async Task<bool> WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model) public async Task<bool> WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model)
{ {
@ -1163,6 +1218,7 @@ namespace Ombi.Controllers.V1
/// Gets the webhook notification settings. /// Gets the webhook notification settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/webhook")] [HttpGet("notifications/webhook")]
public async Task<WebhookNotificationViewModel> WebhookNotificationSettings() public async Task<WebhookNotificationViewModel> WebhookNotificationSettings()
{ {
@ -1177,6 +1233,7 @@ namespace Ombi.Controllers.V1
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpPost("notifications/newsletter")] [HttpPost("notifications/newsletter")]
public async Task<bool> NewsletterSettings([FromBody] NewsletterNotificationViewModel model) public async Task<bool> NewsletterSettings([FromBody] NewsletterNotificationViewModel model)
{ {
@ -1191,6 +1248,7 @@ namespace Ombi.Controllers.V1
} }
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
[Admin]
[HttpPost("notifications/newsletterdatabase")] [HttpPost("notifications/newsletterdatabase")]
public async Task<bool> UpdateNewsletterDatabase() public async Task<bool> UpdateNewsletterDatabase()
{ {
@ -1201,6 +1259,7 @@ namespace Ombi.Controllers.V1
/// Gets the Newsletter Notification Settings. /// Gets the Newsletter Notification Settings.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Admin]
[HttpGet("notifications/newsletter")] [HttpGet("notifications/newsletter")]
public async Task<NewsletterNotificationViewModel> NewsletterSettings() public async Task<NewsletterNotificationViewModel> NewsletterSettings()
{ {

View file

@ -159,7 +159,7 @@
"RequestedBy": "Sol·licitat per", "RequestedBy": "Sol·licitat per",
"Status": "Estat", "Status": "Estat",
"RequestStatus": "Estat de la sol·licitud", "RequestStatus": "Estat de la sol·licitud",
"Watched": "Watched", "Watched": "Vist",
"WatchedTooltip": "The user who made the request has watched it", "WatchedTooltip": "The user who made the request has watched it",
"WatchedProgressTooltip": "Shows how much 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.", "WatchedByUsersCount": "{{count}} users have watched this.",
@ -408,7 +408,7 @@
"Movies": "Pel·lícules", "Movies": "Pel·lícules",
"Combined": "Combinat", "Combined": "Combinat",
"Tv": "TV", "Tv": "TV",
"Genres": "Genres", "Genres": "Gèneres",
"CardDetails": { "CardDetails": {
"Availability": "Disponibilitat", "Availability": "Disponibilitat",
"Studio": "Estudi", "Studio": "Estudi",

View file

@ -159,10 +159,10 @@
"RequestedBy": "Verzocht Door", "RequestedBy": "Verzocht Door",
"Status": "Status", "Status": "Status",
"RequestStatus": "Aanvraagstatus", "RequestStatus": "Aanvraagstatus",
"Watched": "Watched", "Watched": "Bekeken",
"WatchedTooltip": "The user who made the request has watched it", "WatchedTooltip": "De gebruiker die het verzoek heeft ingediend, heeft het bekeken",
"WatchedProgressTooltip": "Shows how much the user who made the request has watched it", "WatchedProgressTooltip": "Laat zien hoeveel de gebruiker die het verzoek heeft gemaakt het heeft bekeken",
"WatchedByUsersCount": "{{count}} users have watched this.", "WatchedByUsersCount": "{{count}} gebruikers hebben dit bekeken.",
"Denied": " Geweigerd:", "Denied": " Geweigerd:",
"TheatricalRelease": "Cinema Uitgave: {{date}}", "TheatricalRelease": "Cinema Uitgave: {{date}}",
"ReleaseDate": "Uitgekomen: {{date}}", "ReleaseDate": "Uitgekomen: {{date}}",
@ -225,7 +225,7 @@
"Denied": "Geselecteerde items succesvol afgekeurd" "Denied": "Geselecteerde items succesvol afgekeurd"
}, },
"SuccessfullyApproved": "Succesvol goedgekeurd", "SuccessfullyApproved": "Succesvol goedgekeurd",
"SuccessfullyDenied": "Successfully Denied", "SuccessfullyDenied": "Succesvol Geweigerd",
"SuccessfullyDeleted": "Verzoek succesvol verwijderd", "SuccessfullyDeleted": "Verzoek succesvol verwijderd",
"NowAvailable": "Verzoek is nu beschikbaar", "NowAvailable": "Verzoek is nu beschikbaar",
"NowUnavailable": "Verzoek is nu niet beschikbaar", "NowUnavailable": "Verzoek is nu niet beschikbaar",
@ -241,7 +241,7 @@
"NoPermissionsOnBehalf": "Je hebt niet de juiste rechten om namens gebruikers aan te vragen!", "NoPermissionsOnBehalf": "Je hebt niet de juiste rechten om namens gebruikers aan te vragen!",
"NoPermissions": "Je hebt de juiste rechten niet!", "NoPermissions": "Je hebt de juiste rechten niet!",
"RequestDoesNotExist": "Verzoek bestaat 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", "NoPermissionsRequestMovie": "Je bent niet gemachtigd om een film aan te vragen",
"NoPermissionsRequestTV": "Je bent niet gemachtigd om een serie 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", "NoPermissionsRequestAlbum": "Je bent niet gemachtigd om een album aan te vragen",

View file

@ -1,3 +1,3 @@
{ {
"version": "4.48.3" "version": "4.49.3"
} }