Compare commits

...

30 commits

Author SHA1 Message Date
Conventional Changelog Action
48c358eff0 chore(release): 🚀 v4.49.5 [skip ci] 2025-08-23 20:55:02 +00:00
Jamie Rees
65b96c3ea9
Merge pull request #5250 from lukeheals/tv-request-marked-as-approved
fix: set MarkedAsApproved on TV requests
2025-08-23 22:53:03 +02:00
Conventional Changelog Action
2a96b40756 chore(release): 🚀 v4.49.4 [skip ci] 2025-08-23 20:46:09 +00:00
contrib-readme-bot
f3964ef94a chore: 👥 Updated Contributors [skip ci] 2025-08-23 20:43:24 +00:00
Jamie Rees
736ff31566
refactor: Upgrade to latest angular version
Angular 20
2025-08-23 22:43:13 +02:00
tidusjar
9027401604 others 2025-08-23 21:32:54 +01:00
tidusjar
3d08c4af96 fixed node version 2025-08-23 21:32:33 +01:00
tidusjar
532ee7e0af update 2025-08-23 21:14:30 +01:00
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
Luke
57d3880115 fix: set MarkedAsApproved on TV requests 2025-08-16 00:56:36 -04:00
emmatherock
11fd7a5fc8
fix(plex-api): update Plex Watchlist URL 2025-08-14 21:17:10 -03:00
tidusjar
69c556929b Upgrade Angular to v20 and TypeScript to 5.8.3 2025-07-12 22:56:09 +01: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
160 changed files with 4613 additions and 2665 deletions

View file

@ -22,7 +22,7 @@ jobs:
dotnet-version: 6.0.x
- uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '20'
- uses: actions/cache@v4
with:

View file

@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '20'
- name: NodeModules Cache
uses: actions/cache@v4

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '20'
- name: NodeModules Cache
uses: actions/cache@v4

File diff suppressed because it is too large Load diff

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">
@ -407,14 +407,21 @@ Here are some of the features Ombi has:
<sub><b>Andrew Metzger</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/zobe123">
<img src="https://avatars.githubusercontent.com/u/13840542?v=4" width="50;" alt="zobe123"/>
<br />
<sub><b>Zobe123</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/tombomb">
<img src="https://avatars.githubusercontent.com/u/544509?v=4" width="50;" alt="tombomb"/>
<br />
<sub><b>Tom McClellan</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Tim-Trott">
<img src="https://avatars.githubusercontent.com/u/8249434?v=4" width="50;" alt="Tim-Trott"/>
@ -449,15 +456,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Sean Callinan</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/sambartik">
<img src="https://avatars.githubusercontent.com/u/63553146?v=4" width="50;" alt="sambartik"/>
<br />
<sub><b>Samuel Bartík</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/rob1998">
<img src="https://avatars.githubusercontent.com/u/1560707?v=4" width="50;" alt="rob1998"/>
@ -492,15 +499,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Micky</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/LMaxence">
<img src="https://avatars.githubusercontent.com/u/29194680?v=4" width="50;" alt="LMaxence"/>
<br />
<sub><b>Maxence Lecanu</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/mattmattmatt">
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/>
@ -523,17 +530,10 @@ Here are some of the features Ombi has:
</a>
</td>
<td align="center">
<a href="https://github.com/Lucane">
<img src="https://avatars.githubusercontent.com/u/7999446?v=4" width="50;" alt="Lucane"/>
<a href="https://github.com/Drewster727">
<img src="https://avatars.githubusercontent.com/u/4528753?v=4" width="50;" alt="Drewster727"/>
<br />
<sub><b>Lucane</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/zobe123">
<img src="https://avatars.githubusercontent.com/u/13840542?v=4" width="50;" alt="zobe123"/>
<br />
<sub><b>Zobe123</b></sub>
<sub><b>Drew</b></sub>
</a>
</td>
<td align="center">
@ -594,6 +594,13 @@ Here are some of the features Ombi has:
<sub><b>M4tta</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/emma-the-rock">
<img src="https://avatars.githubusercontent.com/u/16837067?v=4" width="50;" alt="emma-the-rock"/>
<br />
<sub><b>Emmatherock</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/echel0n">
<img src="https://avatars.githubusercontent.com/u/1128022?v=4" width="50;" alt="echel0n"/>
@ -621,15 +628,15 @@ Here are some of the features Ombi has:
<br />
<sub><b>Camjac251</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/x-limitless-x">
<img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/>
<br />
<sub><b>Blake Drumm</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/bazhip">
<img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="bazhip"/>
@ -658,13 +665,6 @@ Here are some of the features Ombi has:
<sub><b>Torkil</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/sussycatgirl">
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="sussycatgirl"/>
<br />
<sub><b>Lea</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/onedr0p">
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="50;" alt="onedr0p"/>
@ -787,6 +787,21 @@ Here are some of the features Ombi has:
<sub><b>Abe Kline</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Lucane">
<img src="https://avatars.githubusercontent.com/u/7999446?v=4" width="50;" alt="Lucane"/>
<br />
<sub><b>Lucane</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/sussycatgirl">
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="sussycatgirl"/>
<br />
<sub><b>Lea</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/kmlucy">
<img src="https://avatars.githubusercontent.com/u/13952475?v=4" width="50;" alt="kmlucy"/>
@ -800,8 +815,7 @@ Here are some of the features Ombi has:
<br />
<sub><b>Kris Klosterman</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/jonocairns">
<img src="https://avatars.githubusercontent.com/u/182836?v=4" width="50;" alt="jonocairns"/>
@ -829,7 +843,8 @@ Here are some of the features Ombi has:
<br />
<sub><b>Joe Harvey</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/frebib">
<img src="https://avatars.githubusercontent.com/u/775104?v=4" width="50;" alt="frebib"/>
@ -843,8 +858,7 @@ Here are some of the features Ombi has:
<br />
<sub><b>James White</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/JPyke3">
<img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/>
@ -872,7 +886,8 @@ Here are some of the features Ombi has:
<br />
<sub><b>Haries Ramdhani</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/ketsapiwiq">
<img src="https://avatars.githubusercontent.com/u/26697460?v=4" width="50;" alt="ketsapiwiq"/>
@ -886,8 +901,7 @@ Here are some of the features Ombi has:
<br />
<sub><b>Grygon</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Fish2">
<img src="https://avatars.githubusercontent.com/u/2311734?v=4" width="50;" alt="Fish2"/>
@ -901,13 +915,6 @@ Here are some of the features Ombi has:
<br />
<sub><b>Eli</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Drewster727">
<img src="https://avatars.githubusercontent.com/u/4528753?v=4" width="50;" alt="Drewster727"/>
<br />
<sub><b>Drew</b></sub>
</a>
</td></tr>
</table>
<!-- readme: collaborators,contributors -end -->

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

@ -696,6 +696,8 @@ namespace Ombi.Core.Engine
ErrorMessage = "Child Request does not exist"
};
}
request.MarkedAsApproved = DateTime.Now;
request.Approved = true;
request.Denied = false;

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

@ -13,17 +13,17 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.11",
"@angular/cdk": "16.2.14",
"@angular/common": "^17.3.11",
"@angular/compiler": "^17.3.11",
"@angular/core": "^17.3.11",
"@angular/forms": "^17.3.11",
"@angular/animations": "^20.0.0",
"@angular/cdk": "^16.2.14",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/material": "^14.2.7",
"@angular/platform-browser": "^17.3.11",
"@angular/platform-browser-dynamic": "^17.3.11",
"@angular/platform-server": "^17.3.11",
"@angular/router": "^17.3.11",
"@angular/platform-browser": "^20.0.0",
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/platform-server": "^20.0.0",
"@angular/router": "^20.0.0",
"@angularclass/hmr": "^3.0.0",
"@auth0/angular-jwt": "^5.0.2",
"@fortawesome/fontawesome-free": "^6.6.0",
@ -53,15 +53,15 @@
"zone.js": "0.14.7"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.3",
"@angular/cli": "^17.1.3",
"@angular/compiler-cli": "^17.1.3",
"@angular-devkit/build-angular": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@babel/core": "^7.18.9",
"@compodoc/compodoc": "^1.1.19",
"@storybook/angular": "7.6.14",
"@types/node": "^20.11.17",
"chromatic": "^6.7.1",
"typescript": "5.2.2"
"typescript": "5.8.3"
},
"optionalDependencies": {
"protractor": "~5.4.0",

View file

@ -17,6 +17,7 @@ import { CustomizationFacade } from './state/customization';
@Component({
standalone: false,
selector: "app-ombi",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],

View file

@ -4,6 +4,7 @@ import { CookieService } from "ng2-cookies";
import { StorageService } from "../shared/storage/storage-service";
@Component({
standalone: false,
templateUrl: "cookie.component.html",
})
export class CookieComponent implements OnInit {

View file

@ -5,6 +5,7 @@ import { AuthService } from "../auth/auth.service";
import { CustomPageService, NotificationService } from "../services";
@Component({
standalone: false,
templateUrl: "./custompage.component.html",
})
export class CustomPageComponent implements OnInit {

View file

@ -9,6 +9,7 @@ import { forkJoin } from "rxjs";
import { FeaturesFacade } from "../../../state/features/features.facade";
@Component({
standalone: false,
templateUrl: "./discover-actor.component.html",
styleUrls: ["./discover-actor.component.scss"],
})

View file

@ -12,6 +12,7 @@ import { IMovieRequestModel, RequestType } from "../../../interfaces";
import { TranslateService } from "@ngx-translate/core";
@Component({
standalone: false,
selector: "discover-card",
templateUrl: "./discover-card.component.html",
styleUrls: ["./discover-card.component.scss"],

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

@ -17,6 +17,7 @@ export enum DiscoverType {
}
@Component({
standalone: false,
selector: "carousel-list",
templateUrl: "./carousel-list.component.html",
styleUrls: ["./carousel-list.component.scss"],
@ -43,7 +44,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 +149,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 +157,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

@ -11,6 +11,7 @@ import { RequestType } from "../../../interfaces";
import { FeaturesFacade } from "../../../state/features/features.facade";
@Component({
standalone: false,
templateUrl: "./discover-collections.component.html",
styleUrls: ["./discover-collections.component.scss"],
})

View file

@ -1,16 +1,35 @@
<div class="small-middle-container">
@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>
} @placeholder(minimum 300) {
<div class="section">
<h2>{{ 'Discovery.Genres' | translate }}</h2>
<p-skeleton width="100%" height="60px"></p-skeleton>
</div>
}
@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>
} @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>
}
@defer (on viewport; prefetch on idle) {
<div class="section" [hidden]="!showSeasonal">
<h2>{{ 'Discovery.SeasonalTab' | translate }}</h2>
<div>
@ -22,25 +41,68 @@
></carousel-list>
</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>
}
@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>
} @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

@ -10,3 +10,27 @@ h2{
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

@ -4,6 +4,7 @@ import { AuthService } from "../../../auth/auth.service";
import { DiscoverType } from "../carousel-list/carousel-list.component";
@Component({
standalone: false,
templateUrl: "./discover.component.html",
styleUrls: ["./discover.component.scss"],
})

View file

@ -13,6 +13,7 @@ interface IGenreSelect {
type: "movie"|"tv";
}
@Component({
standalone: false,
selector: "genre-button-select",
templateUrl: "./genre-button-select.component.html",
styleUrls: ["./genre-button-select.component.scss"],

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

@ -20,6 +20,7 @@ export enum DiscoverType {
}
@Component({
standalone: false,
selector: "ombi-recently-list",
templateUrl: "./recently-requested-list.component.html",
styleUrls: ["./recently-requested-list.component.scss"],

View file

@ -13,6 +13,7 @@ import { isEqual } from "lodash";
import { FeaturesFacade } from "../../../state/features/features.facade";
@Component({
standalone: false,
templateUrl: "./search-results.component.html",
styleUrls: ["../discover/discover.component.scss"],
})

View file

@ -1,6 +1,7 @@
import { Component } from "@angular/core";
@Component({
standalone: false,
template: "<h2>{{ 'ErrorPages.NotFound' | translate }}</h2>",
})
export class PageNotFoundComponent { }

View file

@ -6,6 +6,7 @@ import { IssuesService, NotificationService } from "../../../services";
import { IssueChatComponent } from "../issue-chat/issue-chat.component";
@Component({
standalone: false,
selector: "issues-details-group",
templateUrl: "details-group.component.html",
styleUrls: ["details-group.component.scss"],

View file

@ -15,6 +15,7 @@ export interface IssuesDetailsGroupData {
}
@Component({
standalone: false,
selector: "issues-details",
templateUrl: "details.component.html",
styleUrls: ["details.component.scss"],

View file

@ -13,6 +13,7 @@ export interface ChatData {
}
@Component({
standalone: false,
selector: "issue-chat",
templateUrl: "issue-chat.component.html",
styleUrls: ["issue-chat.component.scss"],

View file

@ -9,6 +9,7 @@ import { DomSanitizer } from "@angular/platform-browser";
import { IIssues, IIssuesChat, IIssueSettings, INewIssueComments, IssueStatus } from "../interfaces";
@Component({
standalone: false,
templateUrl: "issueDetails.component.html",
styleUrls: ["./issueDetails.component.scss"],
})

View file

@ -8,6 +8,7 @@ import { PageEvent } from '@angular/material/paginator';
import { IssuesV2Service } from "../services/issuesv2.service";
@Component({
standalone: false,
templateUrl: "issues.component.html",
styleUrls: ['issues.component.scss']
})

View file

@ -4,6 +4,7 @@ import { MatDialog } from "@angular/material/dialog";
import { IIssuesSummary, IPagenator, IssueStatus } from "../interfaces";
@Component({
standalone: false,
selector: "issues-table",
templateUrl: "issuestable.component.html",
styleUrls: ['issuestable.component.scss']

View file

@ -9,6 +9,7 @@ import { SettingsService } from "../services";
import { CustomizationFacade } from "../state/customization";
@Component({
standalone: false,
templateUrl: "./landingpage.component.html",
styleUrls: ["./landingpage.component.scss"],
})

View file

@ -15,6 +15,7 @@ import { SonarrFacade } from "app/state/sonarr";
import { RadarrFacade } from "app/state/radarr";
@Component({
standalone: false,
templateUrl: "./login.component.html",
styleUrls: ["./login.component.scss"],
})

View file

@ -6,6 +6,7 @@ import { NotificationService } from "../services";
import { StorageService } from "../shared/storage/storage-service";
@Component({
standalone: false,
templateUrl: "./loginoauth.component.html",
})
export class LoginOAuthComponent implements OnInit {

View file

@ -8,6 +8,7 @@ import { IdentityService, NotificationService, SettingsService } from "../servic
import { CustomizationFacade } from "../state/customization";
@Component({
standalone: false,
templateUrl: "./resetpassword.component.html",
styleUrls: ["./login.component.scss"],
})

View file

@ -11,6 +11,7 @@ import { PlatformLocation } from "@angular/common";
import { Router } from "@angular/router";
@Component({
standalone: false,
templateUrl: "./tokenresetpassword.component.html",
styleUrls: ["./login.component.scss"],
})

View file

@ -11,6 +11,7 @@ import { IArtistSearchResult, IReleaseGroups } from "../../../interfaces/IMusicS
import { TranslateService } from "@ngx-translate/core";
@Component({
standalone: false,
templateUrl: "./artist-details.component.html",
styleUrls: ["../../media-details.component.scss"],
})

View file

@ -2,6 +2,7 @@ import { Component, Input, ViewEncapsulation } from "@angular/core";
import { ISearchArtistResult } from "../../../../../interfaces";
@Component({
standalone: false,
templateUrl: "./artist-information-panel.component.html",
styleUrls: ["../../../../media-details.component.scss"],
selector: "artist-information-panel",

View file

@ -3,6 +3,7 @@ import { IReleaseGroups } from "../../../../../interfaces/IMusicSearchResultV2";
import { SearchV2Service } from "../../../../../services/searchV2.service";
@Component({
standalone: false,
templateUrl: "./artist-release-panel.component.html",
styleUrls: ["../../../../media-details.component.scss", "./artist-release-panel.component.scss"],
selector: "artist-release-panel",

View file

@ -17,6 +17,7 @@ import { AdminRequestDialogComponent } from '../../../shared/admin-request-dialo
import { FeaturesFacade } from '../../../state/features/features.facade';
@Component({
standalone: false,
templateUrl: './movie-details.component.html',
styleUrls: ['../../media-details.component.scss'],
encapsulation: ViewEncapsulation.None,

View file

@ -4,6 +4,7 @@ import { IAdvancedData, IRadarrProfile, IRadarrRootFolder, RequestCombination }
import { RadarrService } from "../../../../../services";
@Component({
standalone: false,
templateUrl: "./movie-advanced-options.component.html",
selector: "movie-advanced-options",
})

View file

@ -6,6 +6,7 @@ import { IMovieRatings } from "../../../../interfaces/IRatings";
import { APP_BASE_HREF } from "@angular/common";
import { IStreamingData } from "../../../../interfaces/IStreams";
@Component({
standalone: false,
templateUrl: "./movie-information-panel.component.html",
styleUrls: ["../../../media-details.component.scss"],
selector: "movie-information-panel",

View file

@ -1,6 +1,7 @@
import { Component, Input } from "@angular/core";
@Component({
standalone: false,
selector: "cast-carousel",
templateUrl: "./cast-carousel.component.html",
styleUrls: ["./cast-carousel.component.scss"]

View file

@ -1,6 +1,7 @@
import { Component, Input } from "@angular/core";
@Component({
standalone: false,
selector: "crew-carousel",
templateUrl: "./crew-carousel.component.html",
styleUrls: ["./crew-carousel.component.scss"]

View file

@ -7,6 +7,7 @@ import { RequestType, IRequestEngineResult } from "../../../../interfaces";
import { firstValueFrom } from "rxjs";
@Component({
standalone: false,
selector: "deny-dialog",
templateUrl: "./deny-dialog.component.html",
})

View file

@ -4,6 +4,7 @@ import { RequestType, IIssues, IssueStatus, IIssueSettings } from "../../../../i
import { TranslateService } from "@ngx-translate/core";
@Component({
standalone: false,
selector: "issues-panel",
templateUrl: "./issues-panel.component.html",
styleUrls: ["./issues-panel.component.scss"],

View file

@ -1,6 +1,7 @@
import { Component, Inject, Input, Output, EventEmitter } from "@angular/core";
@Component({
standalone: false,
selector: "media-poster",
templateUrl: "./media-poster.component.html",
})

View file

@ -7,6 +7,7 @@ import { TranslateService } from "@ngx-translate/core";
import { firstValueFrom } from "rxjs";
@Component({
standalone: false,
selector: "new-issue",
templateUrl: "./new-issue.component.html",
})

View file

@ -7,6 +7,7 @@ import { Observable } from "rxjs";
import { map, startWith } from "rxjs/operators";
@Component({
standalone: false,
selector: "request-behalf",
templateUrl: "./request-behalf.component.html",
})

View file

@ -2,6 +2,7 @@ import { APP_BASE_HREF } from "@angular/common";
import { Component, Input, Output, EventEmitter, Inject } from "@angular/core";
import { RequestType } from "../../../../interfaces";
@Component({
standalone: false,
selector: "social-icons",
templateUrl: "./social-icons.component.html",
styleUrls: ["./social-icons.component.scss"]

View file

@ -2,6 +2,7 @@ import { Component, Input } from "@angular/core";
import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
@Component({
standalone: false,
selector: "top-banner",
templateUrl: "./top-banner.component.html",
styleUrls: ["top-banner.component.scss"]

View file

@ -2,6 +2,7 @@ import { Component, Inject } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
@Component({
standalone: false,
selector: "youtube-trailer",
templateUrl: "./youtube-trailer.component.html",
})

View file

@ -10,6 +10,7 @@ import {
import { SettingsService, SonarrService } from "../../../../../services";
@Component({
standalone: false,
templateUrl: "./tv-advanced-options.component.html",
selector: "tv-advanced-options",
})

View file

@ -7,6 +7,7 @@ import { IStreamingData } from "../../../../../interfaces/IStreams";
import { SearchV2Service } from "../../../../../services";
@Component({
standalone: false,
templateUrl: "./tv-information-panel.component.html",
styleUrls: ["../../../../media-details.component.scss"],
selector: "tv-information-panel",

View file

@ -11,6 +11,7 @@ import { RequestServiceV2 } from "../../../../../services/requestV2.service";
import { AdminRequestDialogComponent } from "../../../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({
standalone: false,
templateUrl: "./tv-request-grid.component.html",
styleUrls: ["./tv-request-grid.component.scss"],
selector: "tv-request-grid"

View file

@ -9,6 +9,7 @@ import { RequestServiceV2 } from "../../../../../services/requestV2.service";
import { TranslateService } from "@ngx-translate/core";
@Component({
standalone: false,
templateUrl: "./tv-requests-panel.component.html",
styleUrls: ["./tv-requests-panel.component.scss"],
selector: "tv-requests-panel"

View file

@ -15,6 +15,7 @@ import { forkJoin } from "rxjs";
import { SonarrFacade } from "app/state/sonarr";
@Component({
standalone: false,
templateUrl: "./tv-details.component.html",
styleUrls: ["../../media-details.component.scss"],
encapsulation: ViewEncapsulation.None

View file

@ -25,6 +25,7 @@ export enum SearchFilterType {
}
@Component({
standalone: false,
selector: 'app-my-nav',
templateUrl: './my-nav.component.html',
styleUrls: ['./my-nav.component.scss'],

View file

@ -11,6 +11,7 @@ import { Router } from "@angular/router";
import { UntypedFormGroup, UntypedFormBuilder } from "@angular/forms";
@Component({
standalone: false,
selector: "app-nav-search",
templateUrl: "./nav-search.component.html",
styleUrls: ["./nav-search.component.scss"],

View file

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
standalone: false,
name: "humanize",
})
export class HumanizePipe implements PipeTransform {

View file

@ -1,7 +1,9 @@
import { Pipe, PipeTransform } from "@angular/core";
import { FormatPipe } from 'ngx-date-fns';
import { parseISO, format } from 'date-fns';
@Pipe({
standalone: false,
name: "ombiDate",
})
export class OmbiDatePipe implements PipeTransform {
@ -10,8 +12,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

@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
import { orderBy as _orderBy } from 'lodash';
@Pipe({
standalone: false,
name: 'orderBy',
})
export class OrderPipe<T> implements PipeTransform {

View file

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'quality' })
@Pipe({
standalone: false, name: 'quality' })
export class QualityPipe implements PipeTransform {
transform(value: string): string {
if (value.toUpperCase() === "4K" || value.toUpperCase() === "8K") {

View file

@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from "@angular/platform-browser";
@Pipe({ name: 'safe' })
@Pipe({
standalone: false, name: 'safe' })
export class SafePipe implements PipeTransform {

View file

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
standalone: false,
name: "thousandShort",
})
export class ThousandShortPipe implements PipeTransform {

View file

@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
standalone: false,
name: 'translateStatus'
})
export class TranslateStatusPipe implements PipeTransform {

View file

@ -11,6 +11,7 @@ import { RequestServiceV2 } from "../../../services/requestV2.service";
import { StorageService } from "../../../shared/storage/storage-service";
@Component({
standalone: false,
templateUrl: "./albums-grid.component.html",
selector: "albums-grid",
styleUrls: ["./albums-grid.component.scss"]

View file

@ -2,6 +2,7 @@ import { Component, Input } from "@angular/core";
@Component({
standalone: false,
templateUrl: "./grid-spinner.component.html",
selector: "grid-spinner",
styleUrls: ["./grid-spinner.component.scss"]

View file

@ -16,6 +16,7 @@ import { StorageService } from "../../../shared/storage/storage-service";
import { TranslateService } from "@ngx-translate/core";
@Component({
standalone: false,
templateUrl: "./movies-grid.component.html",
selector: "movies-grid",
styleUrls: ["./movies-grid.component.scss"]

View file

@ -9,6 +9,7 @@ import { DenyDialogComponent } from '../../../media-details/components/shared/de
import { MatDialog } from '@angular/material/dialog';
@Component({
standalone: false,
selector: 'request-options',
templateUrl: './request-options.component.html',
})

View file

@ -7,6 +7,7 @@ import { LidarrService } from "app/services";
import { take } from "rxjs";
@Component({
standalone: false,
templateUrl: "./requests-list.component.html",
styleUrls: ["./requests-list.component.scss"]
})

View file

@ -12,6 +12,7 @@ import { RequestServiceV2 } from "../../../services/requestV2.service";
import { StorageService } from "../../../shared/storage/storage-service";
@Component({
standalone: false,
templateUrl: "./tv-grid.component.html",
selector: "tv-grid",
styleUrls: ["../requests-list.component.scss", "tv-grid.component.scss"]

View file

@ -9,6 +9,7 @@
// import { NotificationService, RadarrService, RequestService } from "../services";
// @Component({
standalone: false,
// selector: "movie-requests",
// templateUrl: "./movierequests.component.html",
// })

View file

@ -9,6 +9,7 @@
// import { NotificationService, RequestService } from "../../services";
// @Component({
standalone: false,
// selector: "music-requests",
// templateUrl: "./musicrequests.component.html",
// })

View file

@ -5,6 +5,7 @@
// import { Observable } from "rxjs";
// @Component({
standalone: false,
// selector: "remaining-requests",
// templateUrl: "./remainingrequests.component.html",
// })

View file

@ -5,6 +5,7 @@
// import { IssuesService, SettingsService } from "../services";
// @Component({
standalone: false,
// templateUrl: "./request.component.html",
// })
// export class RequestComponent implements OnInit {

View file

@ -4,6 +4,7 @@
// import { NotificationService, RequestService } from "../services";
// @Component({
standalone: false,
// selector: "tvrequests-children",
// templateUrl: "./tvrequest-children.component.html",
// })

View file

@ -10,6 +10,7 @@
// import { ImageService } from "../services/image.service";
// @Component({
standalone: false,
// selector: "tv-requests",
// templateUrl: "./tvrequests.component.html",
// styleUrls: ["./tvrequests.component.scss"],

View file

@ -9,6 +9,7 @@ import { UpdateService } from "../../services/update.service";
import { APP_BASE_HREF } from "@angular/common";
@Component({
standalone: false,
templateUrl: "./about.component.html",
styleUrls: ["./about.component.scss"]
})

View file

@ -4,6 +4,7 @@ import { IUpdateModel } from "../../interfaces";
@Component({
standalone: false,
templateUrl: "update-dialog.component.html",
styleUrls: [ "update-dialog.component.scss" ]
})

View file

@ -5,6 +5,7 @@ import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./authentication.component.html",
styleUrls: ["./authentication.component.scss"],
})

View file

@ -6,6 +6,7 @@ import { CouchPotatoService, NotificationService, SettingsService, TesterService
import { ICouchPotatoProfiles } from "../../interfaces";
@Component({
standalone: false,
templateUrl: "./couchpotato.component.html",
styleUrls: ["./couchpotato.component.scss"]
})

View file

@ -6,6 +6,7 @@ import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./customization.component.html",
styleUrls: ["./customization.component.scss"],
})

View file

@ -4,6 +4,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms
import { NotificationService, SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./dognzb.component.html",
styleUrls: ["./dognzb.component.scss"]
})

View file

@ -6,6 +6,7 @@ import {UntypedFormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({
standalone: false,
templateUrl: "./emby.component.html",
styleUrls: ["./emby.component.scss"]
})

View file

@ -3,6 +3,7 @@ import { IFailedRequestsViewModel, RequestType } from "../../interfaces";
import { RequestRetryService } from "../../services";
@Component({
standalone: false,
templateUrl: "./failedrequests.component.html",
styleUrls: ["./failedrequests.component.scss"],
})

View file

@ -6,6 +6,7 @@ import { MatSlideToggleChange } from "@angular/material/slide-toggle";
import { firstValueFrom } from "rxjs";
@Component({
standalone: false,
templateUrl: "./features.component.html",
styleUrls: ["./features.component.scss"]
})

View file

@ -5,6 +5,7 @@ import { IIssueCategory } from "../../interfaces";
import { IssuesService, NotificationService, SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./issues.component.html",
styleUrls: ["./issues.component.scss"]
})

View file

@ -6,6 +6,7 @@ import {UntypedFormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({
standalone: false,
templateUrl: "./jellyfin.component.html",
styleUrls: ["./jellyfin.component.scss"]
})

View file

@ -3,6 +3,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms
import { JobService, NotificationService, SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./jobs.component.html",
styleUrls: ["./jobs.component.scss"]
})

View file

@ -5,6 +5,7 @@ import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./landingpage.component.html",
styleUrls: ["./landingpage.component.scss"],
})

View file

@ -7,6 +7,7 @@ import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./lidarr.component.html",
styleUrls: ["./lidarr.component.scss"]
})

View file

@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
import { SystemService } from "../../services";
@Component({
standalone: false,
templateUrl: "./logs.component.html",
styleUrls:["./logs.component.scss"]
})

View file

@ -4,6 +4,7 @@ import { IMassEmailModel, IMassEmailUserModel } from "../../interfaces";
import { IdentityService, NotificationMessageService, NotificationService, SettingsService } from "../../services";
@Component({
standalone: false,
templateUrl: "./massemail.component.html",
styleUrls: ["./massemail.component.scss"]
})

Some files were not shown because too many files have changed in this diff Show more