Compare commits

..

32 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
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
161 changed files with 4625 additions and 2675 deletions

View file

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

View file

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

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '18' node-version: '20'
- name: NodeModules Cache - name: NodeModules Cache
uses: actions/cache@v4 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> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/MattJeanes"> <a href="https://github.com/AmyJeanes">
<img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="MattJeanes"/> <img src="https://avatars.githubusercontent.com/u/2363642?v=4" width="50;" alt="AmyJeanes"/>
<br /> <br />
<sub><b>Matt Jeanes</b></sub> <sub><b>Amy Jeanes</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
@ -407,14 +407,21 @@ Here are some of the features Ombi has:
<sub><b>Andrew Metzger</b></sub> <sub><b>Andrew Metzger</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/tombomb"> <a href="https://github.com/tombomb">
<img src="https://avatars.githubusercontent.com/u/544509?v=4" width="50;" alt="tombomb"/> <img src="https://avatars.githubusercontent.com/u/544509?v=4" width="50;" alt="tombomb"/>
<br /> <br />
<sub><b>Tom McClellan</b></sub> <sub><b>Tom McClellan</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Tim-Trott"> <a href="https://github.com/Tim-Trott">
<img src="https://avatars.githubusercontent.com/u/8249434?v=4" width="50;" alt="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 /> <br />
<sub><b>Sean Callinan</b></sub> <sub><b>Sean Callinan</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/sambartik"> <a href="https://github.com/sambartik">
<img src="https://avatars.githubusercontent.com/u/63553146?v=4" width="50;" alt="sambartik"/> <img src="https://avatars.githubusercontent.com/u/63553146?v=4" width="50;" alt="sambartik"/>
<br /> <br />
<sub><b>Samuel Bartík</b></sub> <sub><b>Samuel Bartík</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/rob1998"> <a href="https://github.com/rob1998">
<img src="https://avatars.githubusercontent.com/u/1560707?v=4" width="50;" alt="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 /> <br />
<sub><b>Micky</b></sub> <sub><b>Micky</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/LMaxence"> <a href="https://github.com/LMaxence">
<img src="https://avatars.githubusercontent.com/u/29194680?v=4" width="50;" alt="LMaxence"/> <img src="https://avatars.githubusercontent.com/u/29194680?v=4" width="50;" alt="LMaxence"/>
<br /> <br />
<sub><b>Maxence Lecanu</b></sub> <sub><b>Maxence Lecanu</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/mattmattmatt"> <a href="https://github.com/mattmattmatt">
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="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> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/Lucane"> <a href="https://github.com/Drewster727">
<img src="https://avatars.githubusercontent.com/u/7999446?v=4" width="50;" alt="Lucane"/> <img src="https://avatars.githubusercontent.com/u/4528753?v=4" width="50;" alt="Drewster727"/>
<br /> <br />
<sub><b>Lucane</b></sub> <sub><b>Drew</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> </a>
</td> </td>
<td align="center"> <td align="center">
@ -594,6 +594,13 @@ Here are some of the features Ombi has:
<sub><b>M4tta</b></sub> <sub><b>M4tta</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/echel0n"> <a href="https://github.com/echel0n">
<img src="https://avatars.githubusercontent.com/u/1128022?v=4" width="50;" alt="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 /> <br />
<sub><b>Camjac251</b></sub> <sub><b>Camjac251</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/x-limitless-x"> <a href="https://github.com/x-limitless-x">
<img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/> <img src="https://avatars.githubusercontent.com/u/17127926?v=4" width="50;" alt="x-limitless-x"/>
<br /> <br />
<sub><b>Blake Drumm</b></sub> <sub><b>Blake Drumm</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/bazhip"> <a href="https://github.com/bazhip">
<img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="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> <sub><b>Torkil</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/onedr0p"> <a href="https://github.com/onedr0p">
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="50;" alt="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> <sub><b>Abe Kline</b></sub>
</a> </a>
</td> </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"> <td align="center">
<a href="https://github.com/kmlucy"> <a href="https://github.com/kmlucy">
<img src="https://avatars.githubusercontent.com/u/13952475?v=4" width="50;" alt="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 /> <br />
<sub><b>Kris Klosterman</b></sub> <sub><b>Kris Klosterman</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/jonocairns"> <a href="https://github.com/jonocairns">
<img src="https://avatars.githubusercontent.com/u/182836?v=4" width="50;" alt="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 /> <br />
<sub><b>Joe Harvey</b></sub> <sub><b>Joe Harvey</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/frebib"> <a href="https://github.com/frebib">
<img src="https://avatars.githubusercontent.com/u/775104?v=4" width="50;" alt="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 /> <br />
<sub><b>James White</b></sub> <sub><b>James White</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/JPyke3"> <a href="https://github.com/JPyke3">
<img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="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 /> <br />
<sub><b>Haries Ramdhani</b></sub> <sub><b>Haries Ramdhani</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/ketsapiwiq"> <a href="https://github.com/ketsapiwiq">
<img src="https://avatars.githubusercontent.com/u/26697460?v=4" width="50;" alt="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 /> <br />
<sub><b>Grygon</b></sub> <sub><b>Grygon</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Fish2"> <a href="https://github.com/Fish2">
<img src="https://avatars.githubusercontent.com/u/2311734?v=4" width="50;" alt="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 /> <br />
<sub><b>Eli</b></sub> <sub><b>Eli</b></sub>
</a> </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> </td></tr>
</table> </table>
<!-- readme: collaborators,contributors -end --> <!-- 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<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken); Task<PlexWatchlistContainer> GetWatchlist(string plexToken, CancellationToken cancellationToken);
Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken); Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken);
Task<bool> Ping(string authToken, CancellationToken cancellationToken = default);
} }
} }

View file

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

View file

@ -0,0 +1,52 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
namespace Ombi.Core.Authentication
{
public interface IPlexTokenKeepAliveService
{
Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken);
}
public class PlexTokenKeepAliveService : IPlexTokenKeepAliveService
{
private readonly IPlexApi _plexApi;
private readonly ILogger<PlexTokenKeepAliveService> _logger;
public PlexTokenKeepAliveService(IPlexApi plexApi, ILogger<PlexTokenKeepAliveService> logger)
{
_plexApi = plexApi;
_logger = logger;
}
public async Task<bool> KeepTokenAliveAsync(string token, CancellationToken cancellationToken)
{
try
{
if (string.IsNullOrEmpty(token))
{
_logger.LogWarning("Token is null or empty");
return false;
}
// Use the Ping method to validate the token
var isValid = await _plexApi.Ping(token, cancellationToken);
if (!isValid)
{
_logger.LogWarning("Token validation failed - token may be expired or invalid");
}
return isValid;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while keeping token alive");
return false;
}
}
}
}

View file

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

View file

@ -201,7 +201,9 @@ namespace Ombi.Core.Senders
List<MovieResponse> movies; List<MovieResponse> movies;
// Check if the movie already exists? Since it could be unmonitored // 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); var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId);
if (existingMovie == null) if (existingMovie == null)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -105,6 +105,30 @@
padding: 5px; padding: 5px;
} }
.loading-container {
display: flex;
gap: 10px;
padding: 0 20px;
margin-top: 20px;
}
.loading-container .col-2 {
flex: 0 0 auto;
width: calc(10% - 9px);
}
@media (max-width: 768px) {
.loading-container .col-2 {
width: calc(50% - 5px);
}
}
@media (max-width: 480px) {
.loading-container .col-2 {
width: calc(100% - 0px);
}
}
@media (min-width:755px){ @media (min-width:755px){
::ng-deep .p-carousel-item{ ::ng-deep .p-carousel-item{
flex: 1 0 200px !important; flex: 1 0 200px !important;

View file

@ -17,6 +17,7 @@ export enum DiscoverType {
} }
@Component({ @Component({
standalone: false,
selector: "carousel-list", selector: "carousel-list",
templateUrl: "./carousel-list.component.html", templateUrl: "./carousel-list.component.html",
styleUrls: ["./carousel-list.component.scss"], styleUrls: ["./carousel-list.component.scss"],
@ -43,7 +44,7 @@ export class CarouselListComponent implements OnInit {
get mediaTypeStorageKey() { get mediaTypeStorageKey() {
return "DiscoverOptions" + this.discoverType.toString(); return "DiscoverOptions" + this.discoverType.toString();
}; };
private amountToLoad = 17; private amountToLoad = 10;
private currentlyLoaded = 0; private currentlyLoaded = 0;
private baseUrl: string = ""; private baseUrl: string = "";
@ -148,6 +149,7 @@ export class CarouselListComponent implements OnInit {
} }
public async ngOnInit() { public async ngOnInit() {
this.is4kEnabled = this.featureFacade.is4kEnabled(); this.is4kEnabled = this.featureFacade.is4kEnabled();
this.currentlyLoaded = 0; this.currentlyLoaded = 0;
const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey);
@ -155,11 +157,15 @@ export class CarouselListComponent implements OnInit {
this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]];
} }
let currentIteration = 0; // Load initial data - just enough to fill the first carousel page
while (this.discoverResults.length <= 14 && currentIteration <= 3) { // This reduces initial API calls and improves loading performance
currentIteration++; await this.loadData(false);
// If we don't have enough results to fill the carousel, load one more batch
if (this.discoverResults.length < 10) {
await this.loadData(false); await this.loadData(false);
} }
} }
public async toggleChanged(event: MatButtonToggleChange) { public async toggleChanged(event: MatButtonToggleChange) {

View file

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

View file

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

View file

@ -10,3 +10,27 @@ h2{
margin-left:40px; margin-left:40px;
font-size: 24px; font-size: 24px;
} }
.loading-container {
display: flex;
gap: 10px;
padding: 0 20px;
margin-top: 20px;
}
.loading-container .col-2 {
flex: 0 0 auto;
width: calc(10% - 9px);
}
@media (max-width: 768px) {
.loading-container .col-2 {
width: calc(50% - 5px);
}
}
@media (max-width: 480px) {
.loading-container .col-2 {
width: calc(100% - 0px);
}
}

View file

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

View file

@ -13,6 +13,7 @@ interface IGenreSelect {
type: "movie"|"tv"; type: "movie"|"tv";
} }
@Component({ @Component({
standalone: false,
selector: "genre-button-select", selector: "genre-button-select",
templateUrl: "./genre-button-select.component.html", templateUrl: "./genre-button-select.component.html",
styleUrls: ["./genre-button-select.component.scss"], 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"> <div *ngIf="requests().length > 0">
<p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1" <p-carousel #carousel [value]="requests()" [numVisible]="3" [numScroll]="1"
[responsiveOptions]="responsiveOptions" [page]="0"> [responsiveOptions]="responsiveOptions" [page]="0">
@ -13,21 +13,9 @@
</ng-template> </ng-template>
</p-carousel> </p-carousel>
</div> </div>
}@placeholder(minimum 500) { }@placeholder(minimum 300) {
<div class="row loading-container"> <div class="row loading-container">
<div class="col-2"> <div class="col-2" *ngFor="let item of [1,2,3,4,5]">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton>
</div>
<div class="col-2">
<p-skeleton width="100%" height="270px"></p-skeleton> <p-skeleton width="100%" height="270px"></p-skeleton>
</div> </div>
</div> </div>

View file

@ -105,12 +105,32 @@
padding: 5px; padding: 5px;
} }
.loading-container {
display: flex;
gap: 10px;
padding: 0 20px;
margin-top: 20px;
}
.loading-container .col-2 {
flex: 0 0 auto;
width: calc(20% - 8px);
}
@media (max-width: 768px) {
.loading-container .col-2 {
width: calc(50% - 5px);
}
}
@media (max-width: 480px) {
.loading-container .col-2 {
width: calc(100% - 0px);
}
}
@media (min-width:755px){ @media (min-width:755px){
::ng-deep .p-carousel-item{ ::ng-deep .p-carousel-item{
flex: 1 0 200px !important; flex: 1 0 200px !important;
} }
} }
.loading-container {
margin-left: 10rem;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ export interface ChatData {
} }
@Component({ @Component({
standalone: false,
selector: "issue-chat", selector: "issue-chat",
templateUrl: "issue-chat.component.html", templateUrl: "issue-chat.component.html",
styleUrls: ["issue-chat.component.scss"], 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"; import { IIssues, IIssuesChat, IIssueSettings, INewIssueComments, IssueStatus } from "../interfaces";
@Component({ @Component({
standalone: false,
templateUrl: "issueDetails.component.html", templateUrl: "issueDetails.component.html",
styleUrls: ["./issueDetails.component.scss"], styleUrls: ["./issueDetails.component.scss"],
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import { Observable } from "rxjs";
import { map, startWith } from "rxjs/operators"; import { map, startWith } from "rxjs/operators";
@Component({ @Component({
standalone: false,
selector: "request-behalf", selector: "request-behalf",
templateUrl: "./request-behalf.component.html", 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 { Component, Input, Output, EventEmitter, Inject } from "@angular/core";
import { RequestType } from "../../../../interfaces"; import { RequestType } from "../../../../interfaces";
@Component({ @Component({
standalone: false,
selector: "social-icons", selector: "social-icons",
templateUrl: "./social-icons.component.html", templateUrl: "./social-icons.component.html",
styleUrls: ["./social-icons.component.scss"] styleUrls: ["./social-icons.component.scss"]

View file

@ -2,6 +2,7 @@ import { Component, Input } from "@angular/core";
import { DomSanitizer, SafeStyle } from "@angular/platform-browser"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser";
@Component({ @Component({
standalone: false,
selector: "top-banner", selector: "top-banner",
templateUrl: "./top-banner.component.html", templateUrl: "./top-banner.component.html",
styleUrls: ["top-banner.component.scss"] 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"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
@Component({ @Component({
standalone: false,
selector: "youtube-trailer", selector: "youtube-trailer",
templateUrl: "./youtube-trailer.component.html", templateUrl: "./youtube-trailer.component.html",
}) })

View file

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

View file

@ -7,6 +7,7 @@ import { IStreamingData } from "../../../../../interfaces/IStreams";
import { SearchV2Service } from "../../../../../services"; import { SearchV2Service } from "../../../../../services";
@Component({ @Component({
standalone: false,
templateUrl: "./tv-information-panel.component.html", templateUrl: "./tv-information-panel.component.html",
styleUrls: ["../../../../media-details.component.scss"], styleUrls: ["../../../../media-details.component.scss"],
selector: "tv-information-panel", 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"; import { AdminRequestDialogComponent } from "../../../../../shared/admin-request-dialog/admin-request-dialog.component";
@Component({ @Component({
standalone: false,
templateUrl: "./tv-request-grid.component.html", templateUrl: "./tv-request-grid.component.html",
styleUrls: ["./tv-request-grid.component.scss"], styleUrls: ["./tv-request-grid.component.scss"],
selector: "tv-request-grid" selector: "tv-request-grid"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
import { Pipe, PipeTransform } from "@angular/core"; import { Pipe, PipeTransform } from "@angular/core";
import { FormatPipe } from 'ngx-date-fns'; import { FormatPipe } from 'ngx-date-fns';
import { parseISO, format } from 'date-fns';
@Pipe({ @Pipe({
standalone: false,
name: "ombiDate", name: "ombiDate",
}) })
export class OmbiDatePipe implements PipeTransform { export class OmbiDatePipe implements PipeTransform {
@ -10,8 +12,16 @@ export class OmbiDatePipe implements PipeTransform {
private FormatPipe: FormatPipe, private FormatPipe: FormatPipe,
) {} ) {}
public transform(value: string, format: string ) { public transform(value: string, formatStr: string ) {
const date = new Date(value); if (!value) {
return this.FormatPipe.transform(date, format); return '';
}
// Parse the ISO string as UTC
const utcDate = parseISO(value);
// Format the date using date-fns format function
// This will automatically handle the UTC to local conversion
return format(utcDate, formatStr);
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ import { StorageService } from "../../../shared/storage/storage-service";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
@Component({ @Component({
standalone: false,
templateUrl: "./movies-grid.component.html", templateUrl: "./movies-grid.component.html",
selector: "movies-grid", selector: "movies-grid",
styleUrls: ["./movies-grid.component.scss"] 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'; import { MatDialog } from '@angular/material/dialog';
@Component({ @Component({
standalone: false,
selector: 'request-options', selector: 'request-options',
templateUrl: './request-options.component.html', templateUrl: './request-options.component.html',
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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