Merge branch 'develop' of https://github.com/Ombi-app/Ombi into develop

pull/4848/head
tidusjar 1 year ago
commit 12e8559e86

@ -1,3 +1,48 @@
## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06)
### Bug Fixes
* **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4))
# [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04)
### Features
* Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942))
## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04)
### Bug Fixes
* **plex-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a))
# [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04)
### Features
* Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d))
## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22)
### Bug Fixes
* **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502))
# [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01) # [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01)
@ -329,53 +374,3 @@
## [4.21.1](https://github.com/Ombi-app/Ombi/compare/v4.21.0...v4.21.1) (2022-07-11)
### Bug Fixes
* **images:** Retry images with a backoff when we get a Too Many requests from TheMovieDb [#4685](https://github.com/Ombi-app/Ombi/issues/4685) ([3f1f35d](https://github.com/Ombi-app/Ombi/commit/3f1f35df3164db6739691cdda8f925c296239791))
# [4.21.0](https://github.com/Ombi-app/Ombi/compare/v4.20.4...v4.21.0) (2022-06-22)
### Features
* Upgrade to Angular14 ([#4668](https://github.com/Ombi-app/Ombi/issues/4668)) ([b9d55a4](https://github.com/Ombi-app/Ombi/commit/b9d55a469b412558cbf67c1e25db7fdda5964cd8))
### Performance Improvements
* stop populating obsolete subscribe fields ([#4625](https://github.com/Ombi-app/Ombi/issues/4625)) ([9a73463](https://github.com/Ombi-app/Ombi/commit/9a734637665f671b17c2bb440d93b35a891c142b))
## [4.20.4](https://github.com/Ombi-app/Ombi/compare/v4.20.3...v4.20.4) (2022-06-15)
### Bug Fixes
* fixed build ([f877921](https://github.com/Ombi-app/Ombi/commit/f8779219146051ea74f8b6408658ff7975afb88b))
## [4.20.3](https://github.com/Ombi-app/Ombi/compare/v4.20.2...v4.20.3) (2022-06-05)
### Bug Fixes
* **plex:** 🐛 Fixed an issue with the Plex Sync ([ab1a11a](https://github.com/Ombi-app/Ombi/commit/ab1a11af78efbe9d37bd55aa80a640796c138a98))
## [4.20.2](https://github.com/Ombi-app/Ombi/compare/v4.20.1...v4.20.2) (2022-06-03)
### Bug Fixes
* :bug: Fixed the Request on Behalf of having blanks ([#4667](https://github.com/Ombi-app/Ombi/issues/4667)) ([7dd9b1c](https://github.com/Ombi-app/Ombi/commit/7dd9b1cac07f571dd35b362544e4fe0226c4b817))

@ -608,6 +608,13 @@ Here are some of the features Ombi has:
<sub><b>Kyle Lucy</b></sub> <sub><b>Kyle Lucy</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/janderedev">
<img src="https://avatars.githubusercontent.com/u/26145882?v=4" width="50;" alt="janderedev"/>
<br />
<sub><b>Lea</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/Lixumos"> <a href="https://github.com/Lixumos">
<img src="https://avatars.githubusercontent.com/u/29160577?v=4" width="50;" alt="Lixumos"/> <img src="https://avatars.githubusercontent.com/u/29160577?v=4" width="50;" alt="Lixumos"/>
@ -621,15 +628,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Lucane</b></sub> <sub><b>Lucane</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/devbymadde"> <a href="https://github.com/devbymadde">
<img src="https://avatars.githubusercontent.com/u/6094593?v=4" width="50;" alt="devbymadde"/> <img src="https://avatars.githubusercontent.com/u/6094593?v=4" width="50;" alt="devbymadde"/>
<br /> <br />
<sub><b>Madeleine Schönemann</b></sub> <sub><b>Madeleine Schönemann</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/marleypowell"> <a href="https://github.com/marleypowell">
<img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/> <img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/>
@ -664,15 +671,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Miguel A Vico Moya</b></sub> <sub><b>Miguel A Vico Moya</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/beast3334"> <a href="https://github.com/beast3334">
<img src="https://avatars.githubusercontent.com/u/20631046?v=4" width="50;" alt="beast3334"/> <img src="https://avatars.githubusercontent.com/u/20631046?v=4" width="50;" alt="beast3334"/>
<br /> <br />
<sub><b>Nathan Miller</b></sub> <sub><b>Nathan Miller</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/cqxmzz"> <a href="https://github.com/cqxmzz">
<img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/> <img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/>
@ -707,15 +714,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/shoghicp"> <a href="https://github.com/shoghicp">
<img src="https://avatars.githubusercontent.com/u/516482?v=4" width="50;" alt="shoghicp"/> <img src="https://avatars.githubusercontent.com/u/516482?v=4" width="50;" alt="shoghicp"/>
<br /> <br />
<sub><b>Shoghi</b></sub> <sub><b>Shoghi</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Teifun2"> <a href="https://github.com/Teifun2">
<img src="https://avatars.githubusercontent.com/u/7461832?v=4" width="50;" alt="Teifun2"/> <img src="https://avatars.githubusercontent.com/u/7461832?v=4" width="50;" alt="Teifun2"/>
@ -750,15 +757,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Torkil</b></sub> <sub><b>Torkil</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/bybeet"> <a href="https://github.com/bybeet">
<img src="https://avatars.githubusercontent.com/u/1662279?v=4" width="50;" alt="bybeet"/> <img src="https://avatars.githubusercontent.com/u/1662279?v=4" width="50;" alt="bybeet"/>
<br /> <br />
<sub><b>Travis Bybee</b></sub> <sub><b>Travis Bybee</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Xirg"> <a href="https://github.com/Xirg">
<img src="https://avatars.githubusercontent.com/u/6020502?v=4" width="50;" alt="Xirg"/> <img src="https://avatars.githubusercontent.com/u/6020502?v=4" width="50;" alt="Xirg"/>
@ -793,15 +800,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Michael DiStaula</b></sub> <sub><b>Michael DiStaula</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/baikunz"> <a href="https://github.com/baikunz">
<img src="https://avatars.githubusercontent.com/u/984911?v=4" width="50;" alt="baikunz"/> <img src="https://avatars.githubusercontent.com/u/984911?v=4" width="50;" alt="baikunz"/>
<br /> <br />
<sub><b>Dorian ALKOUM</b></sub> <sub><b>Dorian ALKOUM</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<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"/>
@ -836,15 +843,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Mkgeeky</b></sub> <sub><b>Mkgeeky</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/sir-marv"> <a href="https://github.com/sir-marv">
<img src="https://avatars.githubusercontent.com/u/3598205?v=4" width="50;" alt="sir-marv"/> <img src="https://avatars.githubusercontent.com/u/3598205?v=4" width="50;" alt="sir-marv"/>
<br /> <br />
<sub><b>Sirmarv</b></sub> <sub><b>Sirmarv</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/tdorsey"> <a href="https://github.com/tdorsey">
<img src="https://avatars.githubusercontent.com/u/1218404?v=4" width="50;" alt="tdorsey"/> <img src="https://avatars.githubusercontent.com/u/1218404?v=4" width="50;" alt="tdorsey"/>

File diff suppressed because one or more lines are too long

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Radarr.Models; using Ombi.Api.Radarr.Models;
using Ombi.Api.Radarr.Models.V3; using Ombi.Api.Radarr.Models.V3;
@ -14,7 +15,8 @@ namespace Ombi.Api.Radarr
Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl); Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl);
Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl); Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl);
Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl); Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl);
Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability); Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List<int> tags);
Task<List<Tag>> GetTags(string apiKey, string baseUrl); Task<List<Tag>> GetTags(string apiKey, string baseUrl);
Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName);
} }
} }

@ -29,5 +29,6 @@ namespace Ombi.Api.Radarr.Models
public int year { get; set; } public int year { get; set; }
public string minimumAvailability { get; set; } public string minimumAvailability { get; set; }
public long sizeOnDisk { get; set; } public long sizeOnDisk { get; set; }
public int[] tags { get; set; }
} }
} }

@ -72,7 +72,7 @@ namespace Ombi.Api.Radarr
return await Api.Request<MovieResponse>(request); return await Api.Request<MovieResponse>(request);
} }
public async Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability) public async Task<RadarrAddMovie> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List<int> tags)
{ {
var request = new Request("/api/v3/movie", baseUrl, HttpMethod.Post); var request = new Request("/api/v3/movie", baseUrl, HttpMethod.Post);
@ -86,7 +86,8 @@ namespace Ombi.Api.Radarr
monitored = true, monitored = true,
year = year, year = year,
minimumAvailability = minimumAvailability, minimumAvailability = minimumAvailability,
sizeOnDisk = 0 sizeOnDisk = 0,
tags = tags.Any() ? tags.ToArray() : Enumerable.Empty<int>().ToArray()
}; };
if (searchNow) if (searchNow)
@ -156,5 +157,14 @@ namespace Ombi.Api.Radarr
{ {
request.AddHeader("X-Api-Key", key); request.AddHeader("X-Api-Key", key);
} }
public Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName)
{
var request = new Request($"/api/v3/tag", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(new { Label = tagName });
return Api.Request<Tag>(request);
}
} }
} }

@ -15,6 +15,8 @@ using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using System.Collections.Generic; using System.Collections.Generic;
using Ombi.Api.Radarr.Models; using Ombi.Api.Radarr.Models;
using Microsoft.Extensions.Options;
using Ombi.Api.Sonarr;
namespace Ombi.Core.Senders namespace Ombi.Core.Senders
{ {
@ -67,7 +69,7 @@ namespace Ombi.Core.Senders
} }
if (radarrSettings.Enabled) if (radarrSettings.Enabled)
{ {
return await SendToRadarr(model, is4K, radarrSettings); return await SendToRadarr(model, radarrSettings);
} }
var dogSettings = await _dogNzbSettings.GetSettingsAsync(); var dogSettings = await _dogNzbSettings.GetSettingsAsync();
@ -131,7 +133,7 @@ namespace Ombi.Core.Senders
return await _dogNzbApi.AddMovie(settings.ApiKey, id); return await _dogNzbApi.AddMovie(settings.ApiKey, id);
} }
private async Task<SenderResult> SendToRadarr(MovieRequests model, bool is4K, RadarrSettings settings) private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{ {
var qualityToUse = int.Parse(settings.DefaultQualityProfile); var qualityToUse = int.Parse(settings.DefaultQualityProfile);
@ -154,6 +156,17 @@ namespace Ombi.Core.Senders
} }
} }
var tags = new List<int>();
if (settings.Tag.HasValue)
{
tags.Add(settings.Tag.Value);
}
if (settings.SendUserTags)
{
var userTag = await GetOrCreateTag(model, settings);
tags.Add(userTag.id);
}
// Overrides on the request take priority // Overrides on the request take priority
if (model.QualityOverride > 0) if (model.QualityOverride > 0)
{ {
@ -174,7 +187,7 @@ namespace Ombi.Core.Senders
{ {
var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability); settings.MinimumAvailability, tags);
if (!string.IsNullOrEmpty(result.Error?.message)) if (!string.IsNullOrEmpty(result.Error?.message))
{ {
@ -212,5 +225,17 @@ namespace Ombi.Core.Senders
var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); var selectedPath = paths.FirstOrDefault(x => x.id == overrideId);
return selectedPath?.path ?? string.Empty; return selectedPath?.path ?? string.Empty;
} }
private async Task<Tag> GetOrCreateTag(MovieRequests model, RadarrSettings s)
{
var tagName = model.RequestedUser.UserName;
// Does tag exist?
var allTags = await _radarrV3Api.GetTags(s.ApiKey, s.FullUri);
var existingTag = allTags.FirstOrDefault(x => x.label.Equals(tagName, StringComparison.InvariantCultureIgnoreCase));
existingTag ??= await _radarrV3Api.CreateTag(s.ApiKey, s.FullUri, tagName);
return existingTag;
}
} }
} }

@ -4,6 +4,8 @@ using Moq.AutoMock;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Engine; using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
@ -55,7 +57,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task TerminatesWhenWatchlistIsNotEnabled() public async Task TerminatesWhenWatchlistIsNotEnabled()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false });
await _subject.Execute(null); await _subject.Execute(null);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never); _mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
@ -145,7 +147,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task NoPlexUsersWithToken() public async Task NoPlexUsersWithToken()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser> var um = MockHelper.MockUserManager(new List<OmbiUser>
{ {
@ -170,7 +172,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MultipleUsers() public async Task MultipleUsers()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
var um = MockHelper.MockUserManager(new List<OmbiUser> var um = MockHelper.MockUserManager(new List<OmbiUser>
{ {
@ -194,7 +196,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_NoGuid() public async Task MovieRequestFromWatchList_NoGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -245,7 +247,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task TvRequestFromWatchList_NoGuid() public async Task TvRequestFromWatchList_NoGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -295,7 +297,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_AlreadyRequested() public async Task MovieRequestFromWatchList_AlreadyRequested()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -394,7 +396,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_NoTmdbGuid() public async Task MovieRequestFromWatchList_NoTmdbGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -433,6 +435,7 @@ namespace Ombi.Schedule.Tests
}); });
_mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>())) _mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 }); .ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object); await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never); _mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once); _mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
@ -441,10 +444,195 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
} }
[Test]
public async Task MovieRequestFromWatchList_NoTmdbGuid_LookupFromTdb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "movie",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "imdb://123"
}
}
}
}
}
});
_mocker.Setup<IMovieRequestEngine, Task<RequestEngineResult>>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult
{
movie_results = new Movie_Results[]
{
new Movie_Results
{
id = 333
}
}
});
await _subject.Execute(_context.Object);
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.Is<MovieRequestViewModel>(x => x.TheMovieDbId == 333)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<IMovieRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.imdb_id), Times.Once);
}
[Test]
public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "imdbid://123"
}
}
}
}
}
});
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult
{
tv_results = new TvResults[]
{
new TvResults
{
id = 333
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.imdb_id), Times.Once);
}
[Test]
public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb_ViaTvDb()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "thetvdb://123"
}
}
}
}
}
});
_mocker.Setup<IMovieDbApi, Task<FindResult>>(x => x.Find("123", ExternalSource.tvdb_id)).ReturnsAsync(new FindResult
{
tv_results = new TvResults[]
{
new TvResults
{
id = 333
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
_mocker.Verify<IMovieDbApi>(x => x.Find("123", ExternalSource.tvdb_id), Times.Once);
}
[Test] [Test]
public async Task TvRequestFromWatchList_NoTmdbGuid() public async Task TvRequestFromWatchList_NoTmdbGuid()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -494,7 +682,7 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task MovieRequestFromWatchList_AlreadyImported() public async Task MovieRequestFromWatchList_AlreadyImported()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {

@ -2,6 +2,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine; using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
@ -14,6 +16,7 @@ using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Quartz; using Quartz;
using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -30,13 +33,15 @@ namespace Ombi.Schedule.Jobs.Plex
private readonly IMovieRequestEngine _movieRequestEngine; private readonly IMovieRequestEngine _movieRequestEngine;
private readonly ITvRequestEngine _tvRequestEngine; private readonly ITvRequestEngine _tvRequestEngine;
private readonly INotificationHubService _notificationHubService; private readonly INotificationHubService _notificationHubService;
private readonly ILogger _logger; private readonly Microsoft.Extensions.Logging.ILogger _logger;
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo; private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
private readonly IRepository<PlexWatchlistUserError> _userError; private readonly IRepository<PlexWatchlistUserError> _userError;
private readonly IMovieDbApi _movieDbApi;
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)
{ {
_plexApi = plexApi; _plexApi = plexApi;
_settings = settings; _settings = settings;
@ -47,6 +52,7 @@ namespace Ombi.Schedule.Jobs.Plex
_logger = logger; _logger = logger;
_watchlistRepo = watchlistRepo; _watchlistRepo = watchlistRepo;
_userError = userError; _userError = userError;
_movieDbApi = movieDbApi;
} }
public async Task Execute(IJobExecutionContext context) public async Task Execute(IJobExecutionContext context)
@ -109,9 +115,16 @@ namespace Ombi.Schedule.Jobs.Plex
var providerIds = await GetProviderIds(user.MediaServerToken, item, context?.CancellationToken ?? CancellationToken.None); var providerIds = await GetProviderIds(user.MediaServerToken, item, context?.CancellationToken ?? CancellationToken.None);
if (!providerIds.TheMovieDb.HasValue()) if (!providerIds.TheMovieDb.HasValue())
{ {
_logger.LogWarning($"No TheMovieDb Id found for {item.title}, could not import via Plex WatchList"); // Try and use another Id to figure out TheMovieDB
// We need a MovieDbId to support this; var movieDbId = await FindTmdbIdFromAlternateSources(providerIds, item.type);
continue; if (string.IsNullOrEmpty(movieDbId))
{
_logger.LogWarning($"No TheMovieDb Id found for {item.title} for user {user.UserName}, could not import via Plex WatchList");
// We need a MovieDbId to support this;
continue;
}
providerIds.TheMovieDb = movieDbId;
} }
// Check to see if we have already imported this item // Check to see if we have already imported this item
@ -143,6 +156,43 @@ namespace Ombi.Schedule.Jobs.Plex
await NotifyClient("Finished Watchlist Import"); await NotifyClient("Finished Watchlist Import");
} }
private async Task<string> FindTmdbIdFromAlternateSources(ProviderId providerId, string type)
{
FindResult result = null;
var hasResult = false;
var movie = type == "movie";
if (!string.IsNullOrEmpty(providerId.TheTvDb))
{
result = await _movieDbApi.Find(providerId.TheTvDb, ExternalSource.tvdb_id);
hasResult = movie ? result?.movie_results?.Length > 0 : result?.tv_results?.Length > 0;
}
if (!string.IsNullOrEmpty(providerId.ImdbId) && !hasResult)
{
result = await _movieDbApi.Find(providerId.ImdbId, ExternalSource.imdb_id);
if (movie)
{
hasResult = result?.movie_results?.Length > 0;
}
else
{
hasResult = result?.tv_results?.Length > 0;
}
}
if (hasResult)
{
if (movie)
{
return result.movie_results?[0]?.id.ToString() ?? string.Empty;
}
else
{
return result.tv_results?[0]?.id.ToString() ?? string.Empty;
}
}
return string.Empty;
}
private async Task ProcessMovie(int theMovieDbId, OmbiUser user) private async Task ProcessMovie(int theMovieDbId, OmbiUser user)
{ {
_movieRequestEngine.SetUser(user); _movieRequestEngine.SetUser(user);

@ -15,5 +15,6 @@ namespace Ombi.Settings.Settings.Models
public bool EnableOAuth { get; set; } // Plex OAuth public bool EnableOAuth { get; set; } // Plex OAuth
public bool EnableHeaderAuth { get; set; } // Header SSO public bool EnableHeaderAuth { get; set; } // Header SSO
public string HeaderAuthVariable { get; set; } // Header SSO public string HeaderAuthVariable { get; set; } // Header SSO
public bool HeaderAuthCreateUser { get; set; } // Header SSO
} }
} }

@ -9,6 +9,8 @@
public bool AddOnly { get; set; } public bool AddOnly { get; set; }
public string MinimumAvailability { get; set; } public string MinimumAvailability { get; set; }
public bool ScanForAvailability { get; set; } public bool ScanForAvailability { get; set; }
public int? Tag { get; set; }
public bool SendUserTags { get; set; }
} }
public class Radarr4KSettings : RadarrSettings public class Radarr4KSettings : RadarrSettings

@ -19,11 +19,11 @@
public string RootPathAnime { get; set; } public string RootPathAnime { get; set; }
public int? AnimeTag { get; set; } public int? AnimeTag { get; set; }
public int? Tag { get; set; } public int? Tag { get; set; }
public bool SendUserTags { get; set; }
public bool AddOnly { get; set; } public bool AddOnly { get; set; }
public int LanguageProfile { get; set; } public int LanguageProfile { get; set; }
public int LanguageProfileAnime { get; set; } public int LanguageProfileAnime { get; set; }
public bool ScanForAvailability { get; set; } public bool ScanForAvailability { get; set; }
public bool SendUserTags { get; set; }
} }
} }

@ -14,6 +14,7 @@ import { BrowserModule } from "@angular/platform-browser";
import { ButtonModule } from "primeng/button"; import { ButtonModule } from "primeng/button";
import { CUSTOMIZATION_INITIALIZER } from "./state/customization/customization-initializer"; import { CUSTOMIZATION_INITIALIZER } from "./state/customization/customization-initializer";
import { SONARR_INITIALIZER } from "./state/sonarr/sonarr-initializer"; import { SONARR_INITIALIZER } from "./state/sonarr/sonarr-initializer";
import { RADARR_INITIALIZER } from "./state/radarr/radarr-initializer";
import { ConfirmDialogModule } from "primeng/confirmdialog"; import { ConfirmDialogModule } from "primeng/confirmdialog";
import { CookieComponent } from "./auth/cookie.component"; import { CookieComponent } from "./auth/cookie.component";
import { CookieService } from "ng2-cookies"; import { CookieService } from "ng2-cookies";
@ -24,6 +25,7 @@ import { DialogModule } from "primeng/dialog";
import { FEATURES_INITIALIZER } from "./state/features/features-initializer"; import { FEATURES_INITIALIZER } from "./state/features/features-initializer";
import { FeatureState } from "./state/features"; import { FeatureState } from "./state/features";
import { SonarrSettingsState } from "./state/sonarr"; import { SonarrSettingsState } from "./state/sonarr";
import { RadarrSettingsState } from "./state/radarr";
import { JwtModule } from "@auth0/angular-jwt"; import { JwtModule } from "@auth0/angular-jwt";
import { LandingPageComponent } from "./landingpage/landingpage.component"; import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LandingPageService } from "./services"; import { LandingPageService } from "./services";
@ -162,7 +164,7 @@ export function JwtTokenGetter() {
}), }),
SidebarModule, SidebarModule,
MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, MatSlideToggleModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, LayoutModule, MatSlideToggleModule,
NgxsModule.forRoot([CustomizationState, FeatureState, SonarrSettingsState], { NgxsModule.forRoot([CustomizationState, FeatureState, SonarrSettingsState, RadarrSettingsState], {
developmentMode: !environment.production, developmentMode: !environment.production,
}), }),
...environment.production ? [] : ...environment.production ? [] :
@ -211,6 +213,7 @@ export function JwtTokenGetter() {
FEATURES_INITIALIZER, FEATURES_INITIALIZER,
SONARR_INITIALIZER, SONARR_INITIALIZER,
CUSTOMIZATION_INITIALIZER, CUSTOMIZATION_INITIALIZER,
RADARR_INITIALIZER,
{ {
provide: APP_BASE_HREF, provide: APP_BASE_HREF,
useValue: window["baseHref"] useValue: window["baseHref"]

@ -37,7 +37,7 @@ export class AuthService extends ServiceHelpers {
} }
public loggedIn() { public loggedIn() {
const token: string = this.jwtHelperService.tokenGetter(); const token = this.jwtHelperService.tokenGetter() as string;
if (!token) { if (!token) {
return false; return false;

@ -157,7 +157,7 @@ export class DiscoverCardComponent implements OnInit {
AdminRequestDialogComponent, AdminRequestDialogComponent,
{ {
width: "700px", width: "700px",
data: { type: RequestType.movie, id: this.result.id }, data: { type: RequestType.movie, id: this.result.id, is4k: is4k },
panelClass: "modal-panel", panelClass: "modal-panel",
} }
); );

@ -160,6 +160,8 @@ export interface IRadarrSettings extends IExternalSettings {
addOnly: boolean; addOnly: boolean;
minimumAvailability: string; minimumAvailability: string;
scanForAvailability: boolean; scanForAvailability: boolean;
tag: number | null;
sendUserTags: boolean;
} }
export interface IRadarrCombined { export interface IRadarrCombined {
@ -245,6 +247,7 @@ export interface IAuthenticationSettings extends ISettings {
enableOAuth: boolean; enableOAuth: boolean;
enableHeaderAuth: boolean; enableHeaderAuth: boolean;
headerAuthVariable: string; headerAuthVariable: string;
headerAuthCreateUser: boolean;
} }
export interface ICustomPage extends ISettings { export interface ICustomPage extends ISettings {

@ -6,14 +6,13 @@ import { TranslateService } from "@ngx-translate/core";
import { APP_BASE_HREF } from "@angular/common"; import { APP_BASE_HREF } from "@angular/common";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { IAuthenticationSettings, ICustomizationSettings } from "../interfaces"; import { IAuthenticationSettings, ICustomizationSettings } from "../interfaces";
import { PlexTvService } from "../services"; import { PlexTvService, StatusService, SettingsService } from "../services";
import { SettingsService } from "../services";
import { StatusService } from "../services";
import { StorageService } from "../shared/storage/storage-service"; import { StorageService } from "../shared/storage/storage-service";
import { MatSnackBar } from "@angular/material/snack-bar"; import { MatSnackBar } from "@angular/material/snack-bar";
import { CustomizationFacade } from "../state/customization"; import { CustomizationFacade } from "../state/customization";
import { SonarrFacade } from "app/state/sonarr"; import { SonarrFacade } from "app/state/sonarr";
import { RadarrFacade } from "app/state/radarr";
@Component({ @Component({
templateUrl: "./login.component.html", templateUrl: "./login.component.html",
@ -62,6 +61,7 @@ export class LoginComponent implements OnDestroy, OnInit {
private plexTv: PlexTvService, private plexTv: PlexTvService,
private store: StorageService, private store: StorageService,
private sonarrFacade: SonarrFacade, private sonarrFacade: SonarrFacade,
private radarrFacade: RadarrFacade,
private readonly notify: MatSnackBar private readonly notify: MatSnackBar
) { ) {
this.href = href; this.href = href;
@ -89,7 +89,7 @@ export class LoginComponent implements OnDestroy, OnInit {
}); });
if (authService.loggedIn()) { if (authService.loggedIn()) {
this.router.navigate(["/"]); this.loadStores();
} }
} }
@ -144,7 +144,7 @@ export class LoginComponent implements OnDestroy, OnInit {
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
this.ngOnDestroy(); this.ngOnDestroy();
this.sonarrFacade.load().subscribe(); this.loadStores();
this.router.navigate(["/"]); this.router.navigate(["/"]);
} else { } else {
this.notify.open(this.errorBody, "OK", { this.notify.open(this.errorBody, "OK", {
@ -221,7 +221,7 @@ export class LoginComponent implements OnDestroy, OnInit {
this.oAuthWindow.close(); this.oAuthWindow.close();
} }
this.oauthLoading = false; this.oauthLoading = false;
this.sonarrFacade.load().subscribe(); this.loadStores();
this.router.navigate(["search"]); this.router.navigate(["search"]);
return; return;
} }
@ -252,7 +252,7 @@ export class LoginComponent implements OnDestroy, OnInit {
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
this.ngOnDestroy(); this.ngOnDestroy();
this.sonarrFacade.load().subscribe(); this.loadStores();
this.router.navigate(["/"]); this.router.navigate(["/"]);
} else { } else {
this.notify.open(this.errorBody, "OK", { this.notify.open(this.errorBody, "OK", {
@ -274,4 +274,9 @@ export class LoginComponent implements OnDestroy, OnInit {
public ngOnDestroy() { public ngOnDestroy() {
clearInterval(this.pinTimer); clearInterval(this.pinTimer);
} }
private loadStores() {
this.sonarrFacade.load().subscribe();
this.radarrFacade.load().subscribe();
}
} }

@ -75,7 +75,7 @@ export class MovieDetailsComponent implements OnInit{
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
if (this.isAdmin) { if (this.isAdmin) {
this.showAdvanced = await this.radarrService.isRadarrEnabled(); this.showAdvanced = await firstValueFrom(this.radarrService.isRadarrEnabled());
} }
if (this.imdbId) { if (this.imdbId) {
@ -111,7 +111,7 @@ export class MovieDetailsComponent implements OnInit{
is4K = false; is4K = false;
} }
if (this.isAdmin) { if (this.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.movie.id }, panelClass: 'modal-panel' }); const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.movie.id, is4K: is4K }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => { dialog.afterClosed().subscribe(async (result) => {
if (result) { if (result) {
const requestResult = await firstValueFrom(this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId, const requestResult = await firstValueFrom(this.requestService.requestMovie({ theMovieDbId: this.theMovidDbId,

@ -62,7 +62,7 @@ export class TvRequestGridComponent {
}); });
if (this.isAdmin) { if (this.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.tv.id }, panelClass: 'modal-panel' }); const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.tv.id, is4k: null }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => { dialog.afterClosed().subscribe(async (result) => {
if (result) { if (result) {
viewModel.requestOnBehalf = result.username?.id; viewModel.requestOnBehalf = result.username?.id;

@ -3,7 +3,7 @@ import { HttpClient } from "@angular/common/http";
import { Injectable, Inject } from "@angular/core"; import { Injectable, Inject } from "@angular/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; import { IRadarrProfile, IRadarrRootFolder, ITag } from "../../interfaces";
import { IRadarrSettings } from "../../interfaces"; import { IRadarrSettings } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
@ -23,10 +23,28 @@ export class RadarrService extends ServiceHelpers {
public getRootFoldersFromSettings(): Observable<IRadarrRootFolder[]> { public getRootFoldersFromSettings(): Observable<IRadarrRootFolder[]> {
return this.http.get<IRadarrRootFolder[]>(`${this.url}/RootFolders/`, { headers: this.headers }); return this.http.get<IRadarrRootFolder[]>(`${this.url}/RootFolders/`, { headers: this.headers });
} }
public getQualityProfilesFromSettings(): Observable<IRadarrProfile[]> { public getQualityProfilesFromSettings(): Observable<IRadarrProfile[]> {
return this.http.get<IRadarrProfile[]>(`${this.url}/Profiles/`, { headers: this.headers }); return this.http.get<IRadarrProfile[]>(`${this.url}/Profiles/`, { headers: this.headers });
} }
public isRadarrEnabled(): Promise<boolean> {
return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise(); public getRootFolders4kFromSettings(): Observable<IRadarrRootFolder[]> {
return this.http.get<IRadarrRootFolder[]>(`${this.url}/RootFolders/4k`, { headers: this.headers });
}
public getQualityProfiles4kFromSettings(): Observable<IRadarrProfile[]> {
return this.http.get<IRadarrProfile[]>(`${this.url}/Profiles/4k`, { headers: this.headers });
}
public isRadarrEnabled(): Observable<boolean> {
return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers });
}
public getTagsWithSettings(settings: IRadarrSettings): Observable<ITag[]> {
return this.http.post<ITag[]>(`${this.url}/tags/`, JSON.stringify(settings), { headers: this.headers });
}
public getTags(): Observable<ITag[]> {
return this.http.get<ITag[]>(`${this.url}/tags/`, { headers: this.headers })
} }
} }

@ -23,6 +23,9 @@
<div class="checkbox"> <div class="checkbox">
<mat-slide-toggle id="enableHeaderAuth" name="enableHeaderAuth" formControlName="enableHeaderAuth">Enable Authentication with Header Variable</mat-slide-toggle> <mat-slide-toggle id="enableHeaderAuth" name="enableHeaderAuth" formControlName="enableHeaderAuth">Enable Authentication with Header Variable</mat-slide-toggle>
</div> </div>
<div class="alert warning-box">
Enabling Header Authentication will allow anyone to bypass authentication unless you are using a properly configured reverse proxy. Use with caution!
</div>
</div> </div>
<div class="form-group" *ngIf="form.controls.enableHeaderAuth.value"> <div class="form-group" *ngIf="form.controls.enableHeaderAuth.value">
@ -32,6 +35,15 @@
</div> </div>
</div> </div>
<div class="form-group" *ngIf="form.controls.enableHeaderAuth.value">
<div class="checkbox">
<mat-slide-toggle id="headerAuthCreateUser" name="headerAuthCreateUser" formControlName="headerAuthCreateUser">SSO creates new users automatically</mat-slide-toggle>
</div>
<div class="alert warning-box" *ngIf="form.controls.headerAuthCreateUser.value">
If the user in the Header Authentication variable does not exist, a new user will be created. You can configure the default permissions for new users in the <a target="_blank" href="/Settings/UserManagement">User Management settings</a>.
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>

@ -12,4 +12,11 @@
::ng-deep .dark .btn:hover { ::ng-deep .dark .btn:hover {
box-shadow: 0 5px 11px 0 rgba(255, 255, 255, 0.18), 0 4px 15px 0 rgba(255, 255, 255, 0.15); box-shadow: 0 5px 11px 0 rgba(255, 255, 255, 0.18), 0 4px 15px 0 rgba(255, 255, 255, 0.15);
color: inherit; color: inherit;
} }
.warning-box {
margin: 16px 0;
color: white;
background-color: $ombi-background-accent;
border-color: $warn;
}

@ -28,6 +28,7 @@ export class AuthenticationComponent implements OnInit {
enableOAuth: [x.enableOAuth], enableOAuth: [x.enableOAuth],
enableHeaderAuth: [x.enableHeaderAuth], enableHeaderAuth: [x.enableHeaderAuth],
headerAuthVariable: [x.headerAuthVariable], headerAuthVariable: [x.headerAuthVariable],
headerAuthCreateUser: [x.headerAuthCreateUser],
}); });
this.form.controls.enableHeaderAuth.valueChanges.subscribe(x => { this.form.controls.enableHeaderAuth.valueChanges.subscribe(x => {
if (x) { if (x) {

@ -8,6 +8,10 @@
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle formControlName="scanForAvailability">Scan for Availability</mat-slide-toggle> <mat-slide-toggle formControlName="scanForAvailability">Scan for Availability</mat-slide-toggle>
</div> </div>
<div class="md-form-field">
<mat-slide-toggle formControlName="sendUserTags" id="sendUserTags">Add the user as a tag</mat-slide-toggle>
<small><br>This will add the username of the requesting user as a tag in Sonarr. If the tag doesn't exist, Ombi will create it.</small>
</div>
<div class="md-form-field" > <div class="md-form-field" >
<mat-slide-toggle formControlName="addOnly"> <mat-slide-toggle formControlName="addOnly">
Do not search for Movies Do not search for Movies
@ -79,6 +83,22 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="md-form-field">
<div class="md-form-field" style="display:inline;">
<button mat-raised-button (click)="getTags(form)" type="button" color="primary">Load Tags <span *ngIf="tagsRunning" class="fas fa-spinner fa-spin"></span></button>
</div>
<div class="md-form-field" style="margin-top:1em;"></div>
<mat-form-field appearance="outline" >
<mat-label>Tag</mat-label>
<mat-select formControlName="tag">
<mat-option *ngFor="let tag of tags" [value]="tag.id">
{{tag.label}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="md-form-field"> <div class="md-form-field">
<mat-form-field appearance="outline" > <mat-form-field appearance="outline" >
<mat-label>Default Minimum Availability</mat-label> <mat-label>Default Minimum Availability</mat-label>

@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core"; import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { ControlContainer, UntypedFormGroup, Validators } from "@angular/forms"; import { ControlContainer, UntypedFormGroup, Validators } from "@angular/forms";
import { finalize, map } from "rxjs";
import { IMinimumAvailability, IRadarrProfile, IRadarrRootFolder, IRadarrSettings } from "../../../interfaces"; import { IMinimumAvailability, IRadarrProfile, IRadarrRootFolder, IRadarrSettings, ITag } from "../../../interfaces";
import { TesterService, NotificationService, RadarrService } from "../../../services"; import { TesterService, NotificationService, RadarrService } from "../../../services";
@ -16,8 +17,10 @@ export class RadarrFormComponent implements OnInit {
public qualities: IRadarrProfile[]; public qualities: IRadarrProfile[];
public rootFolders: IRadarrRootFolder[]; public rootFolders: IRadarrRootFolder[];
public minimumAvailabilityOptions: IMinimumAvailability[]; public minimumAvailabilityOptions: IMinimumAvailability[];
public tags: ITag[];
public profilesRunning: boolean; public profilesRunning: boolean;
public rootFoldersRunning: boolean; public rootFoldersRunning: boolean;
public tagsRunning: boolean;
public form: UntypedFormGroup; public form: UntypedFormGroup;
constructor(private radarrService: RadarrService, constructor(private radarrService: RadarrService,
@ -34,6 +37,10 @@ export class RadarrFormComponent implements OnInit {
this.rootFolders = []; this.rootFolders = [];
this.rootFolders.push({ path: "Please Select", id: -1 }); this.rootFolders.push({ path: "Please Select", id: -1 });
this.tags = [];
this.tags.push({ label: "None", id: -1 });
this.minimumAvailabilityOptions = [ this.minimumAvailabilityOptions = [
{ name: "Announced", value: "Announced" }, { name: "Announced", value: "Announced" },
{ name: "In Cinemas", value: "InCinemas" }, { name: "In Cinemas", value: "InCinemas" },
@ -47,9 +54,16 @@ export class RadarrFormComponent implements OnInit {
if (this.form.controls.defaultRootPath.value) { if (this.form.controls.defaultRootPath.value) {
this.getRootFolders(this.form); this.getRootFolders(this.form);
} }
if (this.form.controls.tag.value) {
this.getTags(this.form);
}
this.toggleValidators();
} }
public toggleValidators() { public toggleValidators() {
debugger;
const enabled = this.form.controls.enabled.value as boolean; const enabled = this.form.controls.enabled.value as boolean;
this.form.controls.apiKey.setValidators(enabled ? [Validators.required] : null); this.form.controls.apiKey.setValidators(enabled ? [Validators.required] : null);
this.form.controls.defaultQualityProfile.setValidators(enabled ? [Validators.required] : null); this.form.controls.defaultQualityProfile.setValidators(enabled ? [Validators.required] : null);
@ -81,6 +95,20 @@ export class RadarrFormComponent implements OnInit {
}); });
} }
public getTags(form: UntypedFormGroup) {
this.tagsRunning = true;
this.radarrService.getTagsWithSettings(form.value).pipe(
finalize(() => {
this.tagsRunning = false;
this.tags.unshift({ label: "None", id: -1 });
this.notificationService.success("Successfully retrieved the Tags");
}),
map(result => {
this.tags = result;
})
).subscribe()
}
public test(form: UntypedFormGroup) { public test(form: UntypedFormGroup) {
if (form.invalid) { if (form.invalid) {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");

@ -1,8 +1,9 @@
import { Component, OnInit, QueryList, ViewChild, ViewChildren } from "@angular/core"; import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms"; import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
import { RadarrFacade } from "app/state/radarr";
import { IMinimumAvailability, IRadarrCombined, IRadarrProfile, IRadarrRootFolder } from "../../interfaces"; import { IMinimumAvailability, IRadarrCombined, IRadarrProfile, IRadarrRootFolder } from "../../interfaces";
import { NotificationService, SettingsService } from "../../services"; import { NotificationService } from "../../services";
import { FeaturesFacade } from "../../state/features/features.facade"; import { FeaturesFacade } from "../../state/features/features.facade";
import { RadarrFormComponent } from "./components/radarr-form.component"; import { RadarrFormComponent } from "./components/radarr-form.component";
@ -23,7 +24,7 @@ export class RadarrComponent implements OnInit {
@ViewChildren('4kForm') public form4k: QueryList<RadarrFormComponent>; @ViewChildren('4kForm') public form4k: QueryList<RadarrFormComponent>;
@ViewChildren('normalForm') public normalForm: QueryList<RadarrFormComponent>; @ViewChildren('normalForm') public normalForm: QueryList<RadarrFormComponent>;
constructor(private settingsService: SettingsService, constructor(private radarrFacade: RadarrFacade,
private notificationService: NotificationService, private notificationService: NotificationService,
private featureFacade: FeaturesFacade, private featureFacade: FeaturesFacade,
private fb: UntypedFormBuilder) { } private fb: UntypedFormBuilder) { }
@ -31,34 +32,38 @@ export class RadarrComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.is4kEnabled = this.featureFacade.is4kEnabled(); this.is4kEnabled = this.featureFacade.is4kEnabled();
this.settingsService.getRadarr() this.radarrFacade.state$()
.subscribe(x => { .subscribe(x => {
this.form = this.fb.group({ this.form = this.fb.group({
radarr: this.fb.group({ radarr: this.fb.group({
enabled: [x.radarr.enabled], enabled: [x.settings.radarr.enabled],
apiKey: [x.radarr.apiKey], apiKey: [x.settings.radarr.apiKey],
defaultQualityProfile: [+x.radarr.defaultQualityProfile], defaultQualityProfile: [+x.settings.radarr.defaultQualityProfile],
defaultRootPath: [x.radarr.defaultRootPath], defaultRootPath: [x.settings.radarr.defaultRootPath],
ssl: [x.radarr.ssl], tag: [x.settings.radarr.tag],
subDir: [x.radarr.subDir], sendUserTags: [x.settings.radarr.sendUserTags],
ip: [x.radarr.ip], ssl: [x.settings.radarr.ssl],
port: [x.radarr.port], subDir: [x.settings.radarr.subDir],
addOnly: [x.radarr.addOnly], ip: [x.settings.radarr.ip],
minimumAvailability: [x.radarr.minimumAvailability], port: [x.settings.radarr.port],
scanForAvailability: [x.radarr.scanForAvailability] addOnly: [x.settings.radarr.addOnly],
minimumAvailability: [x.settings.radarr.minimumAvailability],
scanForAvailability: [x.settings.radarr.scanForAvailability]
}), }),
radarr4K: this.fb.group({ radarr4K: this.fb.group({
enabled: [x.radarr4K.enabled], enabled: [x.settings.radarr4K.enabled],
apiKey: [x.radarr4K.apiKey], apiKey: [x.settings.radarr4K.apiKey],
defaultQualityProfile: [+x.radarr4K.defaultQualityProfile], defaultQualityProfile: [+x.settings.radarr4K.defaultQualityProfile],
defaultRootPath: [x.radarr4K.defaultRootPath], defaultRootPath: [x.settings.radarr4K.defaultRootPath],
ssl: [x.radarr4K.ssl], tag: [x.settings.radarr4K.tag],
subDir: [x.radarr4K.subDir], sendUserTags: [x.settings.radarr4K.sendUserTags],
ip: [x.radarr4K.ip], ssl: [x.settings.radarr4K.ssl],
port: [x.radarr4K.port], subDir: [x.settings.radarr4K.subDir],
addOnly: [x.radarr4K.addOnly], ip: [x.settings.radarr4K.ip],
minimumAvailability: [x.radarr4K.minimumAvailability], port: [x.settings.radarr4K.port],
scanForAvailability: [x.radarr4K.scanForAvailability] addOnly: [x.settings.radarr4K.addOnly],
minimumAvailability: [x.settings.radarr4K.minimumAvailability],
scanForAvailability: [x.settings.radarr4K.scanForAvailability]
}), }),
}); });
this.normalForm.changes.forEach((comp => { this.normalForm.changes.forEach((comp => {
@ -70,7 +75,6 @@ export class RadarrComponent implements OnInit {
})) }))
} }
}); });
} }
@ -82,17 +86,26 @@ export class RadarrComponent implements OnInit {
const radarrForm = form.controls.radarr as UntypedFormGroup; const radarrForm = form.controls.radarr as UntypedFormGroup;
const radarr4KForm = form.controls.radarr4K as UntypedFormGroup; const radarr4KForm = form.controls.radarr4K as UntypedFormGroup;
if (radarrForm.controls.enabled.value && (radarrForm.controls.defaultQualityProfile.value === -1 || radarrForm.controls.defaultRootPath.value === "Please Select")) { if (radarrForm.controls.enabled.value && (radarrForm.controls.defaultQualityProfile.value === -1
|| radarrForm.controls.defaultRootPath.value === "Please Select")) {
this.notificationService.error("Please check your entered values for Radarr"); this.notificationService.error("Please check your entered values for Radarr");
return; return;
} }
if (radarr4KForm.controls.enabled.value && (radarr4KForm.controls.defaultQualityProfile.value === -1 || radarr4KForm.controls.defaultRootPath.value === "Please Select")) { if (radarr4KForm.controls.enabled.value && (radarr4KForm.controls.defaultQualityProfile.value === -1
|| radarr4KForm.controls.defaultRootPath.value === "Please Select")) {
this.notificationService.error("Please check your entered values for Radarr 4K"); this.notificationService.error("Please check your entered values for Radarr 4K");
return; return;
} }
if (radarr4KForm.controls.tag.value === -1) {
radarr4KForm.controls.tag.setValue(null);
}
if (radarrForm.controls.tag.value === -1) {
radarr4KForm.controls.tag.setValue(null);
}
const settings = <IRadarrCombined> form.value; const settings = <IRadarrCombined> form.value;
this.settingsService.saveRadarr(settings).subscribe(x => { this.radarrFacade.updateSettings(settings).subscribe(x => {
if (x) { if (x) {
this.notificationService.success("Successfully saved Radarr settings"); this.notificationService.success("Successfully saved Radarr settings");
} else { } else {

@ -138,7 +138,7 @@
<div id="tag" class="col-md-6"> <div id="tag" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Default Tag</mat-label> <mat-label>Tag</mat-label>
<mat-select formControlName="tag"> <mat-select formControlName="tag">
<mat-option *ngFor="let tag of tags" [value]="tag.id">{{tag.label}} </mat-option> <mat-option *ngFor="let tag of tags" [value]="tag.id">{{tag.label}} </mat-option>
</mat-select> </mat-select>

@ -65,25 +65,25 @@
<!-- Radarr --> <!-- Radarr -->
<div *ngIf="data.type === RequestType.movie && radarrEnabled"><hr /> <div *ngIf="data.type === RequestType.movie && radarrEnabled"><hr />
<div> <div>
<h3>Radarr Overrides</h3> <h3>Radarr Overrides</h3>
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.QualityProfilesSelect' | translate }}</mat-label>
<mat-select id="radarrQualitySelect" formControlName="radarrPathId"> <mat-select id="radarrQualitySelect" formControlName="radarrPathId">
<mat-option id="radarrQualitySelect{{profile.id}}" *ngFor="let profile of radarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option> <mat-option id="radarrQualitySelect{{profile.id}}" *ngFor="let profile of radarrProfiles" value="{{profile.id}}">{{profile.name}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div mat-dialog-content> <div mat-dialog-content>
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label> <mat-label>{{'MediaDetails.RootFolderSelect' | translate }}</mat-label>
<mat-select id="radarrFolderSelect" formControlName="radarrFolderId"> <mat-select id="radarrFolderSelect" formControlName="radarrFolderId">
<mat-option id="radarrFolderSelect{{profile.id}}" *ngFor="let profile of radarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option> <mat-option id="radarrFolderSelect{{profile.id}}" *ngFor="let profile of radarrRootFolders" value="{{profile.id}}">{{profile.path}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div>
</div> </div>
</div> <!-- End Radarr-->
<!-- End Radarr-->
<div mat-dialog-actions class="right-buttons"> <div mat-dialog-actions class="right-buttons">

@ -1,124 +1,143 @@
import { Component, Inject, OnInit } from "@angular/core"; import { Component, Inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms"; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { SonarrFacade } from "app/state/sonarr"; import { RadarrFacade } from 'app/state/radarr';
import { firstValueFrom, Observable } from "rxjs"; import { SonarrFacade } from 'app/state/sonarr';
import { startWith, map } from "rxjs/operators"; import { firstValueFrom, Observable } from 'rxjs';
import { ILanguageProfiles, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUserDropdown, RequestType } from "../../interfaces"; import { startWith, map } from 'rxjs/operators';
import { IdentityService, RadarrService, SonarrService } from "../../services"; import {
ILanguageProfiles,
IRadarrProfile,
IRadarrRootFolder,
ISonarrProfile,
ISonarrRootFolder,
IUserDropdown,
RequestType,
} from '../../interfaces';
import { IdentityService, RadarrService, SonarrService } from '../../services';
export interface IAdminRequestDialogData { export interface IAdminRequestDialogData {
type: RequestType, type: RequestType;
id: number id: number;
is4k: boolean | null;
} }
@Component({ @Component({
selector: "admin-request-dialog", selector: 'admin-request-dialog',
templateUrl: "admin-request-dialog.component.html", templateUrl: 'admin-request-dialog.component.html',
styleUrls: [ "admin-request-dialog.component.scss" ] styleUrls: ['admin-request-dialog.component.scss'],
}) })
export class AdminRequestDialogComponent implements OnInit { export class AdminRequestDialogComponent implements OnInit {
constructor( constructor(
public dialogRef: MatDialogRef<AdminRequestDialogComponent>, public dialogRef: MatDialogRef<AdminRequestDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: IAdminRequestDialogData, @Inject(MAT_DIALOG_DATA) public data: IAdminRequestDialogData,
private identityService: IdentityService, private identityService: IdentityService,
private sonarrService: SonarrService, private sonarrService: SonarrService,
private radarrService: RadarrService, private radarrService: RadarrService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private sonarrFacade: SonarrFacade private sonarrFacade: SonarrFacade,
) {} private radarrFacade: RadarrFacade,
) {}
public form: UntypedFormGroup; public form: UntypedFormGroup;
public RequestType = RequestType; public RequestType = RequestType;
public options: IUserDropdown[]; public options: IUserDropdown[];
public filteredOptions: Observable<IUserDropdown[]>; public filteredOptions: Observable<IUserDropdown[]>;
public userId: string; public userId: string;
public radarrEnabled: boolean; public radarrEnabled: boolean;
public sonarrEnabled: boolean; public radarr4kEnabled: boolean;
public sonarrEnabled: boolean;
public sonarrProfiles: ISonarrProfile[]; public sonarrProfiles: ISonarrProfile[];
public sonarrRootFolders: ISonarrRootFolder[]; public sonarrRootFolders: ISonarrRootFolder[];
public sonarrLanguageProfiles: ILanguageProfiles[]; public sonarrLanguageProfiles: ILanguageProfiles[];
public radarrProfiles: IRadarrProfile[]; public radarrProfiles: IRadarrProfile[];
public radarrRootFolders: IRadarrRootFolder[]; public radarrRootFolders: IRadarrRootFolder[];
public async ngOnInit() { public async ngOnInit() {
this.form = this.fb.group({
username: [null],
sonarrPathId: [null],
sonarrFolderId: [null],
sonarrLanguageId: [null],
radarrPathId: [null],
radarrFolderId: [null],
});
this.form = this.fb.group({ this.options = await firstValueFrom(this.identityService.getUsersDropdown());
username: [null],
sonarrPathId: [null],
sonarrFolderId: [null],
sonarrLanguageId: [null],
radarrPathId: [null],
radarrFolderId: [null]
})
this.options = await firstValueFrom(this.identityService.getUsersDropdown()); this.filteredOptions = this.form.controls['username'].valueChanges.pipe(
startWith(''),
map((value) => this._filter(value)),
);
this.filteredOptions = this.form.controls['username'].valueChanges.pipe( if (this.data.type === RequestType.tvShow) {
startWith(""), this.sonarrEnabled = this.sonarrFacade.isEnabled();
map((value) => this._filter(value)) if (this.sonarrEnabled) {
); console.log(this.sonarrFacade.version());
if (this.sonarrFacade.version()[0] === '3') {
this.sonarrService.getV3LanguageProfilesWithoutSettings().subscribe((profiles: ILanguageProfiles[]) => {
this.sonarrLanguageProfiles = profiles;
});
}
this.sonarrService.getQualityProfilesWithoutSettings().subscribe((c) => {
this.sonarrProfiles = c;
});
this.sonarrService.getRootFoldersWithoutSettings().subscribe((c) => {
this.sonarrRootFolders = c;
});
}
}
if (this.data.type === RequestType.movie) {
this.radarrEnabled = this.radarrFacade.isEnabled();
this.radarr4kEnabled = this.radarrFacade.is4KEnabled();
if (this.data.type === RequestType.tvShow) { if (this.data.is4k ?? false) {
this.sonarrEnabled = this.sonarrFacade.isEnabled(); if (this.radarr4kEnabled) {
if (this.sonarrEnabled) { this.radarrService.getQualityProfiles4kFromSettings().subscribe((c) => {
console.log(this.sonarrFacade.version()); this.radarrProfiles = c;
if (this.sonarrFacade.version()[0] === "3") { });
this.sonarrService.getV3LanguageProfilesWithoutSettings().subscribe((profiles: ILanguageProfiles[]) => { this.radarrService.getRootFolders4kFromSettings().subscribe((c) => {
this.sonarrLanguageProfiles = profiles; this.radarrRootFolders = c;
}) });
} }
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => { } else {
this.sonarrProfiles = c; if (this.radarrEnabled) {
}); this.radarrService.getQualityProfilesFromSettings().subscribe((c) => {
this.sonarrService.getRootFoldersWithoutSettings().subscribe(c => { this.radarrProfiles = c;
this.sonarrRootFolders = c; });
}); this.radarrService.getRootFoldersFromSettings().subscribe((c) => {
} this.radarrRootFolders = c;
} });
if (this.data.type === RequestType.movie) { }
this.radarrEnabled = await this.radarrService.isRadarrEnabled(); }
if (this.radarrEnabled) { }
this.radarrService.getQualityProfilesFromSettings().subscribe(c => { }
this.radarrProfiles = c;
});
this.radarrService.getRootFoldersFromSettings().subscribe(c => {
this.radarrRootFolders = c;
});
}
}
}
public displayFn(user: IUserDropdown): string { public displayFn(user: IUserDropdown): string {
const username = user?.username ? user.username : ""; const username = user?.username ? user.username : '';
const email = user?.email ? `(${user.email})` : ""; const email = user?.email ? `(${user.email})` : '';
if (username || email) { if (username || email) {
return `${username} ${email}`; return `${username} ${email}`;
} }
return ''; return '';
} }
private _filter(value: string | IUserDropdown): IUserDropdown[] { private _filter(value: string | IUserDropdown): IUserDropdown[] {
const filterValue = const filterValue = typeof value === 'string' ? value.toLowerCase() : value.username.toLowerCase();
typeof value === "string"
? value.toLowerCase()
: value.username.toLowerCase();
return this.options.filter((option) => return this.options.filter((option) => option.username.toLowerCase().includes(filterValue));
option.username.toLowerCase().includes(filterValue) }
);
}
public async submitRequest() { public async submitRequest() {
const model = this.form.value; const model = this.form.value;
model.radarrQualityOverrideTitle = this.radarrProfiles?.filter(x => x.id == model.radarrPathId)[0]?.name; model.radarrQualityOverrideTitle = this.radarrProfiles?.filter((x) => x.id == model.radarrPathId)[0]?.name;
model.radarrRootFolderTitle = this.radarrRootFolders?.filter(x => x.id == model.radarrFolderId)[0]?.path; model.radarrRootFolderTitle = this.radarrRootFolders?.filter((x) => x.id == model.radarrFolderId)[0]?.path;
model.sonarrRootFolderTitle = this.sonarrRootFolders?.filter(x => x.id == model.sonarrFolderId)[0]?.path; model.sonarrRootFolderTitle = this.sonarrRootFolders?.filter((x) => x.id == model.sonarrFolderId)[0]?.path;
model.sonarrQualityOverrideTitle = this.sonarrProfiles?.filter(x => x.id == model.sonarrPathId)[0]?.name; model.sonarrQualityOverrideTitle = this.sonarrProfiles?.filter((x) => x.id == model.sonarrPathId)[0]?.name;
model.sonarrLanguageProfileTitle = this.sonarrLanguageProfiles?.filter(x => x.id == model.sonarrLanguageId)[0]?.name; model.sonarrLanguageProfileTitle = this.sonarrLanguageProfiles?.filter((x) => x.id == model.sonarrLanguageId)[0]?.name;
this.dialogRef.close(model); this.dialogRef.close(model);
} }
} }

@ -62,7 +62,7 @@ export class EpisodeRequestComponent {
}); });
if (this.data.isAdmin) { if (this.data.isAdmin) {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.data.series.id }, panelClass: 'modal-panel' }); const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.tvShow, id: this.data.series.id, is4k: null }, panelClass: 'modal-panel' });
dialog.afterClosed().subscribe(async (result) => { dialog.afterClosed().subscribe(async (result) => {
if (result) { if (result) {
viewModel.requestOnBehalf = result.username?.id; viewModel.requestOnBehalf = result.username?.id;

@ -0,0 +1,4 @@
export * from './radarr.state';
export * from './radarr.actions';
export * from './radarr.facade';
export * from './radarr.selectors';

@ -0,0 +1,12 @@
import { APP_INITIALIZER } from "@angular/core";
import { Observable } from "rxjs";
import { RadarrFacade } from "./radarr.facade";
export const RADARR_INITIALIZER = {
provide: APP_INITIALIZER,
useFactory: (radarrFacade: RadarrFacade) => (): Observable<unknown> => {
return radarrFacade.load();
},
multi: true,
deps: [RadarrFacade],
};

@ -0,0 +1,10 @@
import { IRadarrCombined } from "../../interfaces";
export class LoadSettings {
public static readonly type = '[Radarr] LoadSettings';
}
export class UpdateSettings {
public static readonly type = '[Radarr] UpdateSettings';
constructor(public settings: IRadarrCombined) { }
}

@ -0,0 +1,27 @@
import { IRadarrCombined } from "../../interfaces";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { Store } from "@ngxs/store";
import { RadarrState } from "./types";
import { RadarrSelectors } from "./radarr.selectors";
import { LoadSettings, UpdateSettings } from "./radarr.actions";
@Injectable({
providedIn: 'root',
})
export class RadarrFacade {
public constructor(private store: Store) {}
public state$ = (): Observable<RadarrState> => this.store.select(RadarrSelectors.state);
public updateSettings = (settings: IRadarrCombined): Observable<unknown> => this.store.dispatch(new UpdateSettings(settings));
public load = (): Observable<unknown> => this.store.dispatch(new LoadSettings());
public settings = (): IRadarrCombined => this.store.selectSnapshot(RadarrSelectors.settings);
public isEnabled = (): boolean => this.store.selectSnapshot(RadarrSelectors.isEnabled);
public is4KEnabled = (): boolean => this.store.selectSnapshot(RadarrSelectors.is4KEnabled);
}

@ -0,0 +1,26 @@
import { RadarrState, RADARR_STATE_TOKEN } from "./types";
import { Selector } from "@ngxs/store";
import { IRadarrCombined } from "../../interfaces";
export class RadarrSelectors {
@Selector([RADARR_STATE_TOKEN])
public static state(state: RadarrState): RadarrState {
return state;
}
@Selector([RadarrSelectors.state])
public static settings(state: RadarrState): IRadarrCombined {
return state.settings;
}
@Selector([RadarrSelectors.settings])
public static isEnabled(settings: IRadarrCombined): boolean {
return settings?.radarr?.enabled ?? false;
}
@Selector([RadarrSelectors.settings])
public static is4KEnabled(settings: IRadarrCombined): boolean {
return settings?.radarr4K?.enabled ?? false;
}
}

@ -0,0 +1,41 @@
import { Action, State, StateContext } from "@ngxs/store";
import { RadarrState, RADARR_STATE_TOKEN } from "./types";
import { SettingsService } from "../../services";
import { AuthService } from "../../auth/auth.service";
import { Injectable } from "@angular/core";
import { combineLatest, Observable, of } from "rxjs";
import { map, tap } from "rxjs/operators";
import { IRadarrCombined } from "../../interfaces";
import { LoadSettings, UpdateSettings } from "./radarr.actions";
@State({
name: RADARR_STATE_TOKEN
})
@Injectable()
export class RadarrSettingsState {
constructor(private settingsService: SettingsService, private authService: AuthService) { }
@Action(LoadSettings)
public load({ setState }: StateContext<RadarrState>): Observable<RadarrState> {
const isAdmin = this.authService.isAdmin();
const calls = isAdmin ? [this.settingsService.getRadarr()] : [of({})];
return combineLatest(calls).pipe(
tap(([settings]) =>
{
setState({settings: settings as IRadarrCombined});
}),
map((result) => <RadarrState>{settings: result[0]})
);
}
@Action(UpdateSettings)
public enable(ctx: StateContext<RadarrState>, { settings }: UpdateSettings): Observable<RadarrState> {
const state = ctx.getState();
return this.settingsService.saveRadarr(settings).pipe(
tap((_) => ctx.setState({...state, settings})),
map(_ => <RadarrState>{...state, settings})
);
}
}

@ -0,0 +1,8 @@
import { IRadarrCombined } from "../../interfaces";
import { StateToken } from "@ngxs/store";
export const RADARR_STATE_TOKEN = new StateToken<RadarrState>('RadarrState');
export interface RadarrState {
settings: IRadarrCombined;
}

@ -18,18 +18,18 @@ namespace Ombi.Controllers.V1.External
public class RadarrController : ControllerBase public class RadarrController : ControllerBase
{ {
public RadarrController(IRadarrApi radarr, ISettingsService<RadarrSettings> settings, public RadarrController(
ICacheService mem, IRadarrV3Api radarrV3Api) ISettingsService<RadarrSettings> settings,
ISettingsService<Radarr4KSettings> radarr4kSettings,
IRadarrV3Api radarrV3Api)
{ {
_radarrApi = radarr;
_radarrSettings = settings; _radarrSettings = settings;
_cache = mem; _radarr4KSettings = radarr4kSettings;
_radarrV3Api = radarrV3Api; _radarrV3Api = radarrV3Api;
} }
private readonly IRadarrApi _radarrApi;
private readonly ISettingsService<RadarrSettings> _radarrSettings; private readonly ISettingsService<RadarrSettings> _radarrSettings;
private readonly ICacheService _cache; private readonly ISettingsService<Radarr4KSettings> _radarr4KSettings;
private readonly IRadarrV3Api _radarrV3Api; private readonly IRadarrV3Api _radarrV3Api;
/// <summary> /// <summary>
/// Gets the Radarr profiles. /// Gets the Radarr profiles.
@ -80,6 +80,23 @@ namespace Ombi.Controllers.V1.External
return null; return null;
} }
/// <summary>
/// Gets the Radarr 4K profiles using the saved settings
/// <remarks>The data is cached for an hour</remarks>
/// </summary>
/// <returns></returns>
[HttpGet("Profiles/4k")]
[PowerUser]
public async Task<IActionResult> GetProfiles4K()
{
var settings = await _radarr4KSettings.GetSettingsAsync();
if (settings.Enabled)
{
return Ok(await _radarrV3Api.GetProfiles(settings.ApiKey, settings.FullUri));
}
return null;
}
/// <summary> /// <summary>
/// Gets the Radarr root folders using the saved settings. /// Gets the Radarr root folders using the saved settings.
/// <remarks>The data is cached for an hour</remarks> /// <remarks>The data is cached for an hour</remarks>
@ -97,6 +114,23 @@ namespace Ombi.Controllers.V1.External
return null; return null;
} }
/// <summary>
/// Gets the Radarr 4K root folders using the saved settings.
/// <remarks>The data is cached for an hour</remarks>
/// </summary>
/// <returns></returns>
[HttpGet("RootFolders/4k")]
[PowerUser]
public async Task<IEnumerable<RadarrRootFolder>> GetRootFolders4K()
{
var settings = await _radarr4KSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await _radarrV3Api.GetRootFolders(settings.ApiKey, settings.FullUri);
}
return null;
}
/// <summary> /// <summary>
/// Gets the Radarr tags /// Gets the Radarr tags
/// </summary> /// </summary>

@ -36,13 +36,15 @@ namespace Ombi.Controllers.V1
public class TokenController : ControllerBase public class TokenController : ControllerBase
{ {
public TokenController(OmbiUserManager um, ITokenRepository token, public TokenController(OmbiUserManager um, ITokenRepository token,
IPlexOAuthManager oAuthManager, ILogger<TokenController> logger, ISettingsService<AuthenticationSettings> auth) IPlexOAuthManager oAuthManager, ILogger<TokenController> logger, ISettingsService<AuthenticationSettings> auth,
ISettingsService<UserManagementSettings> userManagement)
{ {
_userManager = um; _userManager = um;
_token = token; _token = token;
_plexOAuthManager = oAuthManager; _plexOAuthManager = oAuthManager;
_log = logger; _log = logger;
_authSettings = auth; _authSettings = auth;
_userManagementSettings = userManagement;
} }
private readonly ITokenRepository _token; private readonly ITokenRepository _token;
@ -50,6 +52,7 @@ namespace Ombi.Controllers.V1
private readonly IPlexOAuthManager _plexOAuthManager; private readonly IPlexOAuthManager _plexOAuthManager;
private readonly ILogger<TokenController> _log; private readonly ILogger<TokenController> _log;
private readonly ISettingsService<AuthenticationSettings> _authSettings; private readonly ISettingsService<AuthenticationSettings> _authSettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
/// <summary> /// <summary>
/// Gets the token. /// Gets the token.
@ -305,7 +308,28 @@ namespace Ombi.Controllers.V1
var user = await _userManager.FindByNameAsync(username); var user = await _userManager.FindByNameAsync(username);
if (user == null) if (user == null)
{ {
return new UnauthorizedResult(); if (authSettings.HeaderAuthCreateUser)
{
var defaultSettings = await _userManagementSettings.GetSettingsAsync();
user = new OmbiUser {
UserName = username,
UserType = UserType.LocalUser,
StreamingCountry = defaultSettings.DefaultStreamingCountry ?? "US",
MovieRequestLimit = defaultSettings.MovieRequestLimit,
MovieRequestLimitType = defaultSettings.MovieRequestLimitType,
EpisodeRequestLimit = defaultSettings.EpisodeRequestLimit,
EpisodeRequestLimitType = defaultSettings.EpisodeRequestLimitType,
MusicRequestLimit = defaultSettings.MusicRequestLimit,
MusicRequestLimitType = defaultSettings.MusicRequestLimitType,
};
await _userManager.CreateAsync(user);
await _userManager.AddToRolesAsync(user, defaultSettings.DefaultRoles);
}
else
{
return new UnauthorizedResult();
}
} }
return await CreateToken(true, user); return await CreateToken(true, user);

@ -1,3 +1,3 @@
{ {
"version": "4.33.0" "version": "4.35.1"
} }
Loading…
Cancel
Save