mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-23 14:35:24 -07:00
Compare commits
32 commits
Author | SHA1 | Date | |
---|---|---|---|
|
48c358eff0 | ||
|
65b96c3ea9 |
||
|
2a96b40756 | ||
|
f3964ef94a | ||
|
736ff31566 |
||
|
9027401604 | ||
|
3d08c4af96 | ||
|
532ee7e0af | ||
|
b72f47470c | ||
|
72d4115378 |
||
|
57d3880115 | ||
|
11fd7a5fc8 |
||
|
69c556929b | ||
|
d2be48a921 | ||
|
a92c76021a | ||
|
97d5167db6 | ||
|
2519cca9f6 | ||
|
cfeee39978 | ||
|
cee40146ee | ||
|
1eff48e58e | ||
|
3b2a0d84be | ||
|
ed5bc3f873 | ||
|
067c029f42 | ||
|
cfe2b6ac0f | ||
|
c9ab4f4f9f | ||
|
acb679f99d | ||
|
f88c5ad818 | ||
|
b3e8ca6950 |
||
|
15a97794f6 | ||
|
ba6e708e18 | ||
|
dbbfdd926f | ||
|
53a6a092b1 |
161 changed files with 4625 additions and 2675 deletions
2
.github/workflows/automation-tests.yml
vendored
2
.github/workflows/automation-tests.yml
vendored
|
@ -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:
|
||||||
|
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
|
@ -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
|
||||||
|
|
1039
CHANGELOG.md
1039
CHANGELOG.md
File diff suppressed because it is too large
Load diff
99
README.md
99
README.md
|
@ -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 -->
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
52
src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
Normal file
52
src/Ombi.Core/Authentication/PlexTokenKeepAliveService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 { }
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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']
|
||||||
})
|
})
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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',
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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",
|
||||||
// })
|
// })
|
||||||
|
|
|
@ -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",
|
||||||
// })
|
// })
|
||||||
|
|
|
@ -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",
|
||||||
// })
|
// })
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
// })
|
// })
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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" ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue