Compare commits

..

22 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
Conventional Changelog Action
15a97794f6 chore(release): 🚀 v4.48.3 [skip ci] 2025-05-14 20:59:03 +00:00
tidusjar
ba6e708e18 fix: Correct 4K movie request existence check
When requesting a 4K movie, Ombi was incorrectly checking for existence in the base Radarr instance instead of the 4K instance. This caused "already exists" errors when trying to request 4K versions of movies that only existed in the standard instance.

Changes:
- Modified SendToRadarr to use the correct Radarr instance (4K or standard) when checking for existing movies
- Added existenceCheckSettings to properly handle instance-specific checks
- Maintains original settings for movie addition/update operations

Fixes #4798
2025-05-14 21:57:06 +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
22 changed files with 509 additions and 142 deletions

View file

@ -1,3 +1,68 @@
## [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)
@ -2146,62 +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))

View file

@ -122,10 +122,10 @@ Here are some of the features Ombi has:
</a>
</td>
<td align="center">
<a href="https://github.com/MattJeanes">
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="MattJeanes"/>
<a href="https://github.com/AmyJeanes">
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="AmyJeanes"/>
<br />
<sub><b>Matt Jeanes</b></sub>
<sub><b>Amy Jeanes</b></sub>
</a>
</td>
<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<PlexWatchlistContainer> GetWatchlist(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 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/";
/// <summary>
/// Sign into the Plex API
@ -320,6 +320,30 @@ namespace Ombi.Api.Plex
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>
/// 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

@ -201,7 +201,9 @@ namespace Ombi.Core.Senders
List<MovieResponse> 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)

View file

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

View file

@ -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<IJobExecutionContext>();
_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>();
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock());
_mocker.Setup<INotificationHelper>(x => x.Notify(It.IsAny<NotificationOptions>()));
@ -838,5 +841,43 @@ namespace Ombi.Schedule.Tests
// Assert
_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 IMovieDbApi _movieDbApi;
private readonly INotificationHelper _notificationHelper;
private readonly IPlexTokenKeepAliveService _tokenKeepAliveService;
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService,
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> 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<string, string>
{
{ "UserName", user.UserName }
}
};
await _notificationHelper.Notify(notificationModel);
}
continue;
}
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
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-group>
</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()">
<ng-template let-result pTemplate="item">
<discover-card [discoverType]="discoverType" [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
</ng-template>
</p-carousel>
}
@placeholder(minimum 500) {
<p-skeleton width="100%" height="18rem"></p-skeleton>
@placeholder(minimum 300) {
<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;
}
.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;

View file

@ -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) {

View file

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

View file

@ -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);
}
}

View file

@ -1,4 +1,4 @@
@defer (when requests()) {
@defer (when requests(); prefetch on idle) {
<div *ngIf="requests().length > 0">
<p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1"
[responsiveOptions]="responsiveOptions" [page]="0">
@ -13,21 +13,9 @@
</ng-template>
</p-carousel>
</div>
}@placeholder(minimum 500) {
}@placeholder(minimum 300) {
<div class="row loading-container">
<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>
</div>
<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>

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;

View file

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

View file

@ -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",

View file

@ -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",

View file

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