Merge pull request #2525 from tidusjar/develop

Develop
pull/2541/head v3.0.3776
Jamie 7 years ago committed by GitHub
commit c0e9227e38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -243,3 +243,6 @@ _Pvt_Extensions
# CAKE - C# Make
/Tools/*
*.db-journal
# Ignore local vscode config
*.vscode

@ -1,5 +1,152 @@
# Changelog
## (unreleased)
### **New Features**
- Added the request limits in the ui for music. [Jamie]
- Added the root folders and qualities per user! [Jamie]
- Updated all the MS packages. [TidusJar]
- Update the .net core packages to fix "CVE-2018-8409: ASP.NET Core Denial Of Service Vulnerability" [TidusJar]
- Change way remainingrequests component is notified. [Kenton Royal]
- Added the music request limits. [TidusJar]
- Added the Notification Preferences to the user. [TidusJar]
- Added the API to add user notification preferences. [TidusJar]
- Added more logging into the updater. [Jamie]
- Update CHANGELOG.md. [Jamie]
### **Fixes**
- Fixed #2518. [TidusJar]
- Fixed #2522. [TidusJar]
- Fixed #2485. [TidusJar]
- Fixed #2516. [TidusJar]
- Fix bug in which requested TV wasn't logging for some users. [Kenton Royal]
- Add to translations. [Kenton Royal]
- Add html for displaying remaining requests on users page. [Kenton Royal]
- Add quota fields to user view model. [Kenton Royal]
- Users can now see the music search tab #2493. [TidusJar]
- Add href to a tags so that a pointer cursor shows on requests page. [Stephen Panzer]
- Allow Lidarr to specify if we should search for the album. [TidusJar]
- Fixed the issue if in Radarr we only want to add and monitor, if the movie already exists we search for it. [TidusJar]
- Fix bug causing wrong time to be displayed for next request. [Kenton Royal]
- Bodge fix test to prevent compile error. [Kenton Royal]
- Fix displaying year in issue dialog. [Stephen Panzer]
- Add clearfix class. Closes #2486. [Stephen Panzer]
- Correct path of lidarr component import for unix systems. [Kenton Royal]
- Refactor code. [Kenton Royal]
- Fix formatting error. [Kenton Royal]
- Revert "Revert request.service.ts to version on upstream/develop" [Kenton Royal]
- Revert request.service.ts to version on upstream/develop. [Kenton Royal]
- Fix lint errors. [Kenton Royal]
- Move logic for notifying when reuqest is complete. [Kenton Royal]
- Remove import. [Kenton Royal]
- Remove unused module. [Kenton Royal]
- Refactor code. [Kenton Royal]
- Add text to translation file. [Kenton Royal]
- Fix query for fetching requested tv shows. [Kenton Royal]
- Add vscode to gitignore. [Kenton Royal]
- Fix lint errors. [Kenton Royal]
- Remove unused methods from SearchController. [Kenton Royal]
- Remove local vscode files. [Kenton Royal]
- Fix bug when submitting requests for multiple episodes accross multiple seasons. [Kenton Royal]
- Fix bug with TV requests in which requesting a seasion would treat request as single episode. [Kenton Royal]
- Fix issues with remaining count updating. [Kenton Royal]
- Trigger update of request limit on new request. [Kenton Royal]
- Add logic for movie request count. [Kenton Royal]
- Add logic for retriving request information. [Kenton Royal]
- Move to seperate component and display for both TV and movies. [Kenton Royal]
- Add dummy for request counter. [Kenton Royal]
- Fix scss import for unix systems. [Kenton Royal]
- Add methods to interface and add model class. [Kenton Royal]
- !fixed lint. [TidusJar]
- Fixed #2481. [TidusJar]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Portuguese, Brazilian) [Jamie]
- New translations en.json (Polish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Danish) [Jamie]
- Fixed #2475. [Jamie]
- Stript out certain characters when sending a pushover message #2385. [TidusJar]
- Add default values for Priority and Sound. [David Pooley]
- Allow for the ability to set Pushover notification sound and priority from within Ombi. [David Pooley]
- It works now when we request an album when we do not have the artist in Lidarr. Waiting on https://github.com/lidarr/Lidarr/issues/459 to do when we have the artist. [Jamie]
- Fix non-Windows builds. Fixes #2453. [Joe Groocock]
## v3.0.3587 (2018-08-19)
### **New Features**
@ -30,6 +177,12 @@
### **Fixes**
- Now include the release year in the issue title #2381. [TidusJar]
- Made the OAuth a Popout to work with Org. [Jamie]
- Fixed #2418. [TidusJar]
- #2408 Added the feature to delete comments on issues. [Jamie]
- New translations en.json (Swedish) [Jamie]

Binary file not shown.

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Lidarr.Models;
namespace Ombi.Api.Lidarr
{
public interface ILidarrApi
{
Task<List<AlbumLookup>> AlbumLookup(string searchTerm, string apiKey, string baseUrl);
Task<List<ArtistLookup>> ArtistLookup(string searchTerm, string apiKey, string baseUrl);
Task<List<LidarrProfile>> GetProfiles(string apiKey, string baseUrl);
Task<List<LidarrRootFolder>> GetRootFolders(string apiKey, string baseUrl);
Task<ArtistResult> GetArtist(int artistId, string apiKey, string baseUrl);
Task<ArtistResult> GetArtistByForeignId(string foreignArtistId, string apiKey, string baseUrl);
Task<AlbumByArtistResponse> GetAlbumsByArtist(string foreignArtistId);
Task<AlbumLookup> GetAlbumByForeignId(string foreignArtistId, string apiKey, string baseUrl);
Task<List<ArtistResult>> GetArtists(string apiKey, string baseUrl);
Task<List<AlbumResponse>> GetAllAlbums(string apiKey, string baseUrl);
Task<ArtistResult> AddArtist(ArtistAdd artist, string apiKey, string baseUrl);
Task<AlbumResponse> MontiorAlbum(int albumId, string apiKey, string baseUrl);
Task<List<AlbumResponse>> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl);
Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl);
Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl);
Task<LidarrStatus> Status(string apiKey, string baseUrl);
Task<CommandResult> AlbumSearch(int[] albumIds, string apiKey, string baseUrl);
}
}

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr.Models;
namespace Ombi.Api.Lidarr
{
public class LidarrApi : ILidarrApi
{
public LidarrApi(ILogger<LidarrApi> logger, IApi api)
{
Api = api;
Logger = logger;
}
private IApi Api { get; }
private ILogger Logger { get; }
private const string ApiVersion = "/api/v1";
public Task<List<LidarrProfile>> GetProfiles(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/qualityprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<LidarrProfile>>(request);
}
public Task<List<LidarrRootFolder>> GetRootFolders(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/rootfolder", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<LidarrRootFolder>>(request);
}
public async Task<List<ArtistLookup>> ArtistLookup(string searchTerm, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/Artist/lookup", baseUrl, HttpMethod.Get);
request.AddQueryString("term", searchTerm);
AddHeaders(request, apiKey);
return await Api.Request<List<ArtistLookup>>(request);
}
public Task<List<AlbumLookup>> AlbumLookup(string searchTerm, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/Album/lookup", baseUrl, HttpMethod.Get);
request.AddQueryString("term", searchTerm);
AddHeaders(request, apiKey);
return Api.Request<List<AlbumLookup>>(request);
}
public Task<ArtistResult> GetArtist(int artistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist/{artistId}", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<ArtistResult>(request);
}
public async Task<ArtistResult> GetArtistByForeignId(string foreignArtistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist/lookup", baseUrl, HttpMethod.Get);
request.AddQueryString("term", $"lidarr:{foreignArtistId}");
AddHeaders(request, apiKey);
return (await Api.Request<List<ArtistResult>>(request)).FirstOrDefault();
}
public async Task<AlbumLookup> GetAlbumByForeignId(string foreignArtistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album/lookup", baseUrl, HttpMethod.Get);
request.AddQueryString("term", $"lidarr:{foreignArtistId}");
AddHeaders(request, apiKey);
var albums = await Api.Request<List<AlbumLookup>>(request);
return albums.FirstOrDefault();
}
public Task<AlbumByArtistResponse> GetAlbumsByArtist(string foreignArtistId)
{
var request = new Request(string.Empty, $"https://api.lidarr.audio/api/v0.3/artist/{foreignArtistId}",
HttpMethod.Get) {IgnoreBaseUrlAppend = true};
return Api.Request<AlbumByArtistResponse>(request);
}
public Task<List<ArtistResult>> GetArtists(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<ArtistResult>>(request);
}
public Task<List<AlbumResponse>> GetAllAlbums(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<AlbumResponse>>(request);
}
public Task<ArtistResult> AddArtist(ArtistAdd artist, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/artist", baseUrl, HttpMethod.Post);
request.AddJsonBody(artist);
AddHeaders(request, apiKey);
return Api.Request<ArtistResult>(request);
}
public async Task<AlbumResponse> MontiorAlbum(int albumId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album/monitor", baseUrl, HttpMethod.Put);
request.AddJsonBody(new
{
albumIds = new[] { albumId },
monitored = true
});
AddHeaders(request, apiKey);
return (await Api.Request<List<AlbumResponse>>(request)).FirstOrDefault();
}
public Task<List<AlbumResponse>> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/album", baseUrl, HttpMethod.Get);
request.AddQueryString("artistId", artistId.ToString());
AddHeaders(request, apiKey);
return Api.Request<List<AlbumResponse>>(request);
}
public Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/languageprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<LanguageProfiles>>(request);
}
public Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/metadataprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<MetadataProfile>>(request);
}
public Task<LidarrStatus> Status(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/system/status", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<LidarrStatus>(request);
}
public Task<CommandResult> AlbumSearch(int[] albumIds, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/command/AlbumSearch", baseUrl, HttpMethod.Post);
request.AddJsonBody(albumIds);
AddHeaders(request, apiKey);
return Api.Request<CommandResult>(request);
}
private void AddHeaders(Request request, string key)
{
request.AddHeader("X-Api-Key", key);
}
}
}

@ -0,0 +1,34 @@
namespace Ombi.Api.Lidarr.Models
{
public class AlbumByArtistResponse
{
public Album[] Albums { get; set; }
public string ArtistName { get; set; }
public string Disambiguation { get; set; }
public string Id { get; set; }
public Image[] Images { get; set; }
public Link[] Links { get; set; }
public string Overview { get; set; }
public Rating Rating { get; set; }
public string SortName { get; set; }
public string Status { get; set; }
public string Type { get; set; }
}
public class Rating
{
public int Count { get; set; }
public decimal Value { get; set; }
}
public class Album
{
public string Disambiguation { get; set; }
public string Id { get; set; }
public string ReleaseDate { get; set; }
public string[] ReleaseStatuses { get; set; }
public string[] SecondaryTypes { get; set; }
public string Title { get; set; }
public string Type { get; set; }
}
}

@ -0,0 +1,25 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class AlbumLookup
{
public string title { get; set; }
public int artistId { get; set; }
public string foreignAlbumId { get; set; }
public bool monitored { get; set; }
public int profileId { get; set; }
public int duration { get; set; }
public string albumType { get; set; }
public string[] secondaryTypes { get; set; }
public int mediumCount { get; set; }
public Ratings ratings { get; set; }
public DateTime releaseDate { get; set; }
//public object[] releases { get; set; }
public object[] genres { get; set; }
//public object[] media { get; set; }
public Artist artist { get; set; }
public Image[] images { get; set; }
public string remoteCover { get; set; }
}
}

@ -0,0 +1,27 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class AlbumResponse
{
public string title { get; set; }
public string disambiguation { get; set; }
public int artistId { get; set; }
public string foreignAlbumId { get; set; }
public bool monitored { get; set; }
public int profileId { get; set; }
public int duration { get; set; }
public string albumType { get; set; }
public object[] secondaryTypes { get; set; }
public int mediumCount { get; set; }
public Ratings ratings { get; set; }
public DateTime releaseDate { get; set; }
public Currentrelease currentRelease { get; set; }
public Release[] releases { get; set; }
public object[] genres { get; set; }
public Medium[] media { get; set; }
public Image[] images { get; set; }
public Statistics statistics { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,25 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class Artist
{
public string status { get; set; }
public bool ended { get; set; }
public string artistName { get; set; }
public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public object[] links { get; set; }
public object[] images { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public object[] genres { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Statistics statistics { get; set; }
}
}

@ -0,0 +1,49 @@
using System;
using System.Net.Mime;
namespace Ombi.Api.Lidarr.Models
{
public class ArtistAdd
{
public string status { get; set; }
public bool ended { get; set; }
public string artistName { get; set; }
public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public string overview { get; set; }
public string disambiguation { get; set; }
public Link[] links { get; set; }
public Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
public Addoptions addOptions { get; set; }
public string rootFolderPath { get; set; }
}
public class Addoptions
{
/// <summary>
/// Future = 1
/// Missing = 2
/// Existing = 3
/// First = 5
/// Latest = 4
/// None = 6
/// </summary>
public int selectedOption { get; set; }
public bool monitored { get; set; }
public bool searchForMissingAlbums { get; set; }
public string[] AlbumsToMonitor { get; set; } // Uses the MusicBrainzAlbumId!
}
}

@ -0,0 +1,32 @@
using System;
using System.Net.Mime;
namespace Ombi.Api.Lidarr.Models
{
public class ArtistLookup
{
public string status { get; set; }
public bool ended { get; set; }
public string artistName { get; set; }
public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public string overview { get; set; }
public string artistType { get; set; }
public string disambiguation { get; set; }
public Link[] links { get; set; }
public Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
}
}

@ -0,0 +1,93 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class ArtistResult
{
public string status { get; set; }
public bool ended { get; set; }
public DateTime lastInfoSync { get; set; }
public string artistName { get; set; }
public string foreignArtistId { get; set; }
public int tadbId { get; set; }
public int discogsId { get; set; }
public string overview { get; set; }
public string artistType { get; set; }
public string disambiguation { get; set; }
public Link[] links { get; set; }
public Nextalbum nextAlbum { get; set; }
public Image[] images { get; set; }
public string path { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }
public object[] genres { get; set; }
public string cleanName { get; set; }
public string sortName { get; set; }
public object[] tags { get; set; }
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public Statistics statistics { get; set; }
public int id { get; set; }
}
public class Nextalbum
{
public string foreignAlbumId { get; set; }
public int artistId { get; set; }
public string title { get; set; }
public string disambiguation { get; set; }
public string cleanTitle { get; set; }
public DateTime releaseDate { get; set; }
public int profileId { get; set; }
public int duration { get; set; }
public bool monitored { get; set; }
public object[] images { get; set; }
public object[] genres { get; set; }
public Medium[] media { get; set; }
public DateTime lastInfoSync { get; set; }
public DateTime added { get; set; }
public string albumType { get; set; }
public object[] secondaryTypes { get; set; }
public Ratings ratings { get; set; }
public Release[] releases { get; set; }
public Currentrelease currentRelease { get; set; }
public int id { get; set; }
}
public class Currentrelease
{
public string id { get; set; }
public string title { get; set; }
public DateTime releaseDate { get; set; }
public int trackCount { get; set; }
public int mediaCount { get; set; }
public string disambiguation { get; set; }
public string[] country { get; set; }
public string format { get; set; }
public string[] label { get; set; }
}
public class Medium
{
public int number { get; set; }
public string name { get; set; }
public string format { get; set; }
}
public class Release
{
public string id { get; set; }
public string title { get; set; }
public DateTime releaseDate { get; set; }
public int trackCount { get; set; }
public int mediaCount { get; set; }
public string disambiguation { get; set; }
public string[] country { get; set; }
public string format { get; set; }
public string[] label { get; set; }
}
}

@ -0,0 +1,15 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class CommandResult
{
public string name { get; set; }
public DateTime startedOn { get; set; }
public DateTime stateChangeTime { get; set; }
public bool sendUpdatesToClient { get; set; }
public string state { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class Image
{
public string coverType { get; set; }
public string url { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class LanguageProfiles
{
public string name { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Ombi.Api.Lidarr.Models
{
public class Quality
{
public int id { get; set; }
public string name { get; set; }
}
public class Item
{
public Quality quality { get; set; }
public bool allowed { get; set; }
}
public class LidarrProfile
{
public string name { get; set; }
public List<Item> items { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,11 @@
namespace Ombi.Api.Lidarr.Models
{
public class LidarrRootFolder
{
public string path { get; set; }
public long freeSpace { get; set; }
public object[] unmappedFolders { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,31 @@
using System;
namespace Ombi.Api.Lidarr.Models
{
public class LidarrStatus
{
public string version { get; set; }
public DateTime buildTime { get; set; }
public bool isDebug { get; set; }
public bool isProduction { get; set; }
public bool isAdmin { get; set; }
public bool isUserInteractive { get; set; }
public string startupPath { get; set; }
public string appData { get; set; }
public string osName { get; set; }
public string osVersion { get; set; }
public bool isMonoRuntime { get; set; }
public bool isMono { get; set; }
public bool isLinux { get; set; }
public bool isOsx { get; set; }
public bool isWindows { get; set; }
public string mode { get; set; }
public string branch { get; set; }
public string authentication { get; set; }
public string sqliteVersion { get; set; }
public int migrationVersion { get; set; }
public string urlBase { get; set; }
public string runtimeVersion { get; set; }
public string runtimeName { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class Link
{
public string url { get; set; }
public string name { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class MetadataProfile
{
public string name { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Lidarr.Models
{
public class Ratings
{
public int votes { get; set; }
public decimal value { get; set; }
}
}

@ -0,0 +1,12 @@
namespace Ombi.Api.Lidarr.Models
{
public class Statistics
{
public int albumCount { get; set; }
public int trackFileCount { get; set; }
public int trackCount { get; set; }
public int totalTrackCount { get; set; }
public int sizeOnDisk { get; set; }
public decimal percentOfEpisodes { get; set; }
}
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

@ -5,6 +5,6 @@ namespace Ombi.Api.Pushover
{
public interface IPushoverApi
{
Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken);
Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken, sbyte priority, string sound);
}
}

@ -16,13 +16,13 @@ namespace Ombi.Api.Pushover
private readonly IApi _api;
private const string PushoverEndpoint = "https://api.pushover.net/1";
public async Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken)
public async Task<PushoverResponse> PushAsync(string accessToken, string message, string userToken, sbyte priority, string sound)
{
if (message.Contains("'"))
{
message = message.Replace("'", "&#39;");
}
var request = new Request($"messages.json?token={accessToken}&user={userToken}&message={WebUtility.HtmlEncode(message)}", PushoverEndpoint, HttpMethod.Post);
var request = new Request($"messages.json?token={accessToken}&user={userToken}&priority={priority}&sound={sound}&message={WebUtility.HtmlEncode(message)}", PushoverEndpoint, HttpMethod.Post);
var result = await _api.Request<PushoverResponse>(request);
return result;

@ -28,6 +28,7 @@ namespace Ombi.Api
public bool IgnoreErrors { get; set; }
public bool Retry { get; set; }
public List<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
public bool IgnoreBaseUrlAppend { get; set; }
public Action<string> OnBeforeDeserialization { get; set; }
@ -38,7 +39,7 @@ namespace Ombi.Api
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(BaseUrl))
{
sb.Append(!BaseUrl.EndsWith("/") ? string.Format("{0}/", BaseUrl) : BaseUrl);
sb.Append(!BaseUrl.EndsWith("/") && !IgnoreBaseUrlAppend ? string.Format("{0}/", BaseUrl) : BaseUrl);
}
sb.Append(Endpoint.StartsWith("/") ? Endpoint.Remove(0, 1) : Endpoint);
return sb.ToString();

@ -5,10 +5,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.8.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
<PackageReference Include="Moq" Version="4.9.0" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
</ItemGroup>

@ -19,12 +19,14 @@ namespace Ombi.Core.Tests.Rule.Search
MovieMock = new Mock<IMovieRequestRepository>();
TvMock = new Mock<ITvRequestRepository>();
Rule = new ExistingRule(MovieMock.Object, TvMock.Object);
MusicMock = new Mock<IMusicRequestRepository>();
Rule = new ExistingRule(MovieMock.Object, TvMock.Object, MusicMock.Object);
}
private ExistingRule Rule { get; set; }
private Mock<IMovieRequestRepository> MovieMock { get; set; }
private Mock<ITvRequestRepository> TvMock { get; set; }
private Mock<IMusicRequestRepository> MusicMock { get; set; }
[Test]

@ -0,0 +1,26 @@
using System.Collections.Generic;
using NUnit.Framework;
using Ombi.Helpers;
namespace Ombi.Core.Tests
{
[TestFixture]
public class StringHelperTests
{
[TestCaseSource(nameof(StripCharsData))]
public string StripCharacters(string str, char[] chars)
{
return str.StripCharacters(chars);
}
private static IEnumerable<TestCaseData> StripCharsData
{
get
{
yield return new TestCaseData("this!is^a*string",new []{'!','^','*'}).Returns("thisisastring").SetName("Basic Strip Multipe Chars");
yield return new TestCaseData("What is this madness'",new []{'\'','^','*'}).Returns("What is this madness").SetName("Basic Strip Multipe Chars");
}
}
}
}

@ -36,6 +36,7 @@ namespace Ombi.Core.Engine
protected IRequestServiceMain RequestService { get; }
protected IMovieRequestRepository MovieRepository => RequestService.MovieRequestService;
protected ITvRequestRepository TvRepository => RequestService.TvRequestService;
protected IMusicRequestRepository MusicRepository => RequestService.MusicRequestRepository;
protected readonly ICacheService Cache;
protected readonly ISettingsService<OmbiSettings> OmbiSettings;
protected readonly IRepository<RequestSubscription> _subscriptionRepository;

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Engine
{
public interface IMusicRequestEngine
{
Task<RequestEngineResult>ApproveAlbum(AlbumRequest request);
Task<RequestEngineResult> ApproveAlbumById(int requestId);
Task<RequestEngineResult> DenyAlbumById(int modelId);
Task<IEnumerable<AlbumRequest>> GetRequests();
Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, OrderFilterModel orderFilter);
Task<int> GetTotal();
Task<RequestEngineResult> MarkAvailable(int modelId);
Task<RequestEngineResult> MarkUnavailable(int modelId);
Task RemoveAlbumRequest(int requestId);
Task<RequestEngineResult> RequestAlbum(MusicAlbumRequestViewModel model);
Task<IEnumerable<AlbumRequest>> SearchAlbumRequest(string search);
Task<bool> UserHasRequest(string userId);
Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null);
}
}

@ -17,7 +17,5 @@ namespace Ombi.Core.Engine.Interfaces
Task<RequestEngineResult> ApproveMovie(MovieRequests request);
Task<RequestEngineResult> ApproveMovieById(int requestId);
Task<RequestEngineResult> DenyMovieById(int modelId);
}
}

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Lidarr.Models;
using Ombi.Core.Models.Search;
namespace Ombi.Core.Engine
{
public interface IMusicSearchEngine
{
Task<ArtistResult> GetAlbumArtist(string foreignArtistId);
Task<ArtistResult> GetArtist(int artistId);
Task<IEnumerable<SearchAlbumViewModel>> GetArtistAlbums(string foreignArtistId);
Task<IEnumerable<SearchAlbumViewModel>> SearchAlbum(string search);
Task<IEnumerable<SearchArtistViewModel>> SearchArtist(string search);
}
}

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
@ -22,5 +23,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<int> GetTotal();
Task UnSubscribeRequest(int requestId, RequestType type);
Task SubscribeToRequest(int requestId, RequestType type);
Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null);
}
}

@ -19,6 +19,7 @@ using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
namespace Ombi.Core.Engine
{
@ -336,6 +337,7 @@ namespace Ombi.Core.Engine
};
}
request.MarkedAsApproved = DateTime.Now;
request.Approved = true;
request.Denied = false;
await MovieRepository.Update(request);
@ -483,5 +485,49 @@ namespace Ombi.Core.Engine
return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!"};
}
public async Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user)
{
if (user == null)
{
user = await GetUser();
// If user is still null after attempting to get the logged in user, return null.
if (user == null)
{
return null;
}
}
int limit = user.MovieRequestLimit ?? 0;
if (limit <= 0)
{
return new RequestQuotaCountModel()
{
HasLimit = false,
Limit = 0,
Remaining = 0,
NextRequest = DateTime.Now,
};
}
IQueryable<RequestLog> log = _requestLog.GetAll().Where(x => x.UserId == user.Id && x.RequestType == RequestType.Movie);
int count = limit - await log.CountAsync(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7));
DateTime oldestRequestedAt = await log.Where(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7))
.OrderBy(x => x.RequestDate)
.Select(x => x.RequestDate)
.FirstOrDefaultAsync();
return new RequestQuotaCountModel()
{
HasLimit = true,
Limit = limit,
Remaining = count,
NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc),
};
}
}
}

@ -0,0 +1,503 @@
using Ombi.Api.TheMovieDb;
using Ombi.Core.Models.Requests;
using Ombi.Helpers;
using Ombi.Store.Entities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Senders;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
namespace Ombi.Core.Engine
{
public class MusicRequestEngine : BaseMediaEngine, IMusicRequestEngine
{
public MusicRequestEngine(IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator r, ILogger<MusicRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache,
ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub, ILidarrApi lidarr,
ISettingsService<LidarrSettings> lidarrSettings, IMusicSender sender)
: base(user, requestService, r, manager, cache, ombiSettings, sub)
{
NotificationHelper = helper;
_musicSender = sender;
Logger = log;
_requestLog = rl;
_lidarrApi = lidarr;
_lidarrSettings = lidarrSettings;
}
private INotificationHelper NotificationHelper { get; }
//private IMovieSender Sender { get; }
private ILogger Logger { get; }
private readonly IRepository<RequestLog> _requestLog;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly IMusicSender _musicSender;
/// <summary>
/// Requests the Album.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
public async Task<RequestEngineResult> RequestAlbum(MusicAlbumRequestViewModel model)
{
var s = await _lidarrSettings.GetSettingsAsync();
var album = await _lidarrApi.GetAlbumByForeignId(model.ForeignAlbumId, s.ApiKey, s.FullUri);
if (album == null)
{
return new RequestEngineResult
{
Result = false,
Message = "There was an issue adding this album!",
ErrorMessage = "Please try again later"
};
}
var userDetails = await GetUser();
var requestModel = new AlbumRequest
{
ForeignAlbumId = model.ForeignAlbumId,
ArtistName = album.artist?.artistName,
ReleaseDate = album.releaseDate,
RequestedDate = DateTime.Now,
RequestType = RequestType.Album,
Rating = album.ratings?.value ?? 0m,
RequestedUserId = userDetails.Id,
Title = album.title,
Disk = album.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url,
Cover = album.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url,
ForeignArtistId = album?.artist?.foreignArtistId ?? string.Empty
};
if (requestModel.Cover.IsNullOrEmpty())
{
requestModel.Cover = album.remoteCover;
}
var ruleResults = (await RunRequestRules(requestModel)).ToList();
if (ruleResults.Any(x => !x.Success))
{
return new RequestEngineResult
{
ErrorMessage = ruleResults.FirstOrDefault(x => x.Message.HasValue()).Message
};
}
if (requestModel.Approved) // The rules have auto approved this
{
var requestEngineResult = await AddAlbumRequest(requestModel);
if (requestEngineResult.Result)
{
var result = await ApproveAlbum(requestModel);
if (result.IsError)
{
Logger.LogWarning("Tried auto sending Album but failed. Message: {0}", result.Message);
return new RequestEngineResult
{
Message = result.Message,
ErrorMessage = result.Message,
Result = false
};
}
return requestEngineResult;
}
// If there are no providers then it's successful but album has not been sent
}
return await AddAlbumRequest(requestModel);
}
/// <summary>
/// Gets the requests.
/// </summary>
/// <param name="count">The count.</param>
/// <param name="position">The position.</param>
/// <param name="orderFilter">The order/filter type.</param>
/// <returns></returns>
public async Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position,
OrderFilterModel orderFilter)
{
var shouldHide = await HideFromOtherUsers();
IQueryable<AlbumRequest> allRequests;
if (shouldHide.Hide)
{
allRequests =
MusicRepository.GetWithUser(shouldHide
.UserId); //.Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync();
}
else
{
allRequests =
MusicRepository
.GetWithUser(); //.Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync();
}
switch (orderFilter.AvailabilityFilter)
{
case FilterType.None:
break;
case FilterType.Available:
allRequests = allRequests.Where(x => x.Available);
break;
case FilterType.NotAvailable:
allRequests = allRequests.Where(x => !x.Available);
break;
default:
throw new ArgumentOutOfRangeException();
}
switch (orderFilter.StatusFilter)
{
case FilterType.None:
break;
case FilterType.Approved:
allRequests = allRequests.Where(x => x.Approved);
break;
case FilterType.Processing:
allRequests = allRequests.Where(x => x.Approved && !x.Available);
break;
case FilterType.PendingApproval:
allRequests = allRequests.Where(x => !x.Approved && !x.Available && !(x.Denied ?? false));
break;
default:
throw new ArgumentOutOfRangeException();
}
var total = allRequests.Count();
var requests = await (OrderAlbums(allRequests, orderFilter.OrderType)).Skip(position).Take(count)
.ToListAsync();
requests.ForEach(async x =>
{
await CheckForSubscription(shouldHide, x);
});
return new RequestsViewModel<AlbumRequest>
{
Collection = requests,
Total = total
};
}
private IQueryable<AlbumRequest> OrderAlbums(IQueryable<AlbumRequest> allRequests, OrderType type)
{
switch (type)
{
case OrderType.RequestedDateAsc:
return allRequests.OrderBy(x => x.RequestedDate);
case OrderType.RequestedDateDesc:
return allRequests.OrderByDescending(x => x.RequestedDate);
case OrderType.TitleAsc:
return allRequests.OrderBy(x => x.Title);
case OrderType.TitleDesc:
return allRequests.OrderByDescending(x => x.Title);
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public async Task<int> GetTotal()
{
var shouldHide = await HideFromOtherUsers();
if (shouldHide.Hide)
{
return await MusicRepository.GetWithUser(shouldHide.UserId).CountAsync();
}
else
{
return await MusicRepository.GetWithUser().CountAsync();
}
}
/// <summary>
/// Gets the requests.
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<AlbumRequest>> GetRequests()
{
var shouldHide = await HideFromOtherUsers();
List<AlbumRequest> allRequests;
if (shouldHide.Hide)
{
allRequests = await MusicRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MusicRepository.GetWithUser().ToListAsync();
}
allRequests.ForEach(async x =>
{
await CheckForSubscription(shouldHide, x);
});
return allRequests;
}
private async Task CheckForSubscription(HideResult shouldHide, AlbumRequest x)
{
if (shouldHide.UserId == x.RequestedUserId)
{
x.ShowSubscribe = false;
}
else
{
x.ShowSubscribe = true;
var sub = await _subscriptionRepository.GetAll().FirstOrDefaultAsync(s =>
s.UserId == shouldHide.UserId && s.RequestId == x.Id && s.RequestType == RequestType.Album);
x.Subscribed = sub != null;
}
}
/// <summary>
/// Searches the album request.
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<AlbumRequest>> SearchAlbumRequest(string search)
{
var shouldHide = await HideFromOtherUsers();
List<AlbumRequest> allRequests;
if (shouldHide.Hide)
{
allRequests = await MusicRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MusicRepository.GetWithUser().ToListAsync();
}
var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList();
results.ForEach(async x =>
{
await CheckForSubscription(shouldHide, x);
});
return results;
}
public async Task<RequestEngineResult> ApproveAlbumById(int requestId)
{
var request = await MusicRepository.Find(requestId);
return await ApproveAlbum(request);
}
public async Task<RequestEngineResult> DenyAlbumById(int modelId)
{
var request = await MusicRepository.Find(modelId);
if (request == null)
{
return new RequestEngineResult
{
ErrorMessage = "Request does not exist"
};
}
request.Denied = true;
// We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MusicRepository.Update(request);
return new RequestEngineResult
{
Message = "Request successfully deleted",
};
}
public async Task<RequestEngineResult> ApproveAlbum(AlbumRequest request)
{
if (request == null)
{
return new RequestEngineResult
{
ErrorMessage = "Request does not exist"
};
}
request.MarkedAsApproved = DateTime.Now;
request.Approved = true;
request.Denied = false;
await MusicRepository.Update(request);
var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification);
if (canNotify.Success)
{
NotificationHelper.Notify(request, NotificationType.RequestApproved);
}
if (request.Approved)
{
var result = await _musicSender.Send(request);
if (result.Success && result.Sent)
{
return new RequestEngineResult
{
Result = true
};
}
if (!result.Success)
{
Logger.LogWarning("Tried auto sending album but failed. Message: {0}", result.Message);
return new RequestEngineResult
{
Message = result.Message,
ErrorMessage = result.Message,
Result = false
};
}
// If there are no providers then it's successful but movie has not been sent
}
return new RequestEngineResult
{
Result = true
};
}
/// <summary>
/// Removes the Album request.
/// </summary>
/// <param name="requestId">The request identifier.</param>
/// <returns></returns>
public async Task RemoveAlbumRequest(int requestId)
{
var request = await MusicRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId);
await MusicRepository.Delete(request);
}
public async Task<bool> UserHasRequest(string userId)
{
return await MusicRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId);
}
public async Task<RequestEngineResult> MarkUnavailable(int modelId)
{
var request = await MusicRepository.Find(modelId);
if (request == null)
{
return new RequestEngineResult
{
ErrorMessage = "Request does not exist"
};
}
request.Available = false;
await MusicRepository.Update(request);
return new RequestEngineResult
{
Message = "Request is now unavailable",
Result = true
};
}
public async Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user)
{
if (user == null)
{
user = await GetUser();
// If user is still null after attempting to get the logged in user, return null.
if (user == null)
{
return null;
}
}
int limit = user.MusicRequestLimit ?? 0;
if (limit <= 0)
{
return new RequestQuotaCountModel()
{
HasLimit = false,
Limit = 0,
Remaining = 0,
NextRequest = DateTime.Now,
};
}
IQueryable<RequestLog> log = _requestLog.GetAll().Where(x => x.UserId == user.Id && x.RequestType == RequestType.Album);
int count = limit - await log.CountAsync(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7));
DateTime oldestRequestedAt = await log.Where(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7))
.OrderBy(x => x.RequestDate)
.Select(x => x.RequestDate)
.FirstOrDefaultAsync();
return new RequestQuotaCountModel()
{
HasLimit = true,
Limit = limit,
Remaining = count,
NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc),
};
}
public async Task<RequestEngineResult> MarkAvailable(int modelId)
{
var request = await MusicRepository.Find(modelId);
if (request == null)
{
return new RequestEngineResult
{
ErrorMessage = "Request does not exist"
};
}
request.Available = true;
request.MarkedAsAvailable = DateTime.Now;
NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await MusicRepository.Update(request);
return new RequestEngineResult
{
Message = "Request is now available",
Result = true
};
}
private async Task<RequestEngineResult> AddAlbumRequest(AlbumRequest model)
{
await MusicRepository.Add(model);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification);
if (result.Success)
{
NotificationHelper.NewRequest(model);
}
await _requestLog.Add(new RequestLog
{
UserId = (await GetUser()).Id,
RequestDate = DateTime.UtcNow,
RequestId = model.Id,
RequestType = RequestType.Album,
});
return new RequestEngineResult { Result = true, Message = $"{model.Title} has been successfully added!" };
}
}
}

@ -0,0 +1,219 @@
using System;
using AutoMapper;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Engine
{
public class MusicSearchEngine : BaseMediaEngine, IMusicSearchEngine
{
public MusicSearchEngine(IPrincipal identity, IRequestServiceMain service, ILidarrApi lidarrApi, IMapper mapper,
ILogger<MusicSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub,
ISettingsService<LidarrSettings> lidarrSettings)
: base(identity, service, r, um, mem, s, sub)
{
_lidarrApi = lidarrApi;
_lidarrSettings = lidarrSettings;
Mapper = mapper;
Logger = logger;
}
private readonly ILidarrApi _lidarrApi;
private IMapper Mapper { get; }
private ILogger Logger { get; }
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
/// <summary>
/// Searches the specified album.
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<SearchAlbumViewModel>> SearchAlbum(string search)
{
var settings = await GetSettings();
var result = await _lidarrApi.AlbumLookup(search, settings.ApiKey, settings.FullUri);
var vm = new List<SearchAlbumViewModel>();
foreach (var r in result)
{
vm.Add(await MapIntoAlbumVm(r, settings));
}
return vm;
}
/// <summary>
/// Searches the specified artist
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<SearchArtistViewModel>> SearchArtist(string search)
{
var settings = await GetSettings();
var result = await _lidarrApi.ArtistLookup(search, settings.ApiKey, settings.FullUri);
var vm = new List<SearchArtistViewModel>();
foreach (var r in result)
{
vm.Add(await MapIntoArtistVm(r));
}
return vm;
}
/// <summary>
/// Returns all albums by the specified artist
/// </summary>
/// <param name="foreignArtistId"></param>
/// <returns></returns>
public async Task<IEnumerable<SearchAlbumViewModel>> GetArtistAlbums(string foreignArtistId)
{
var settings = await GetSettings();
var result = await _lidarrApi.GetAlbumsByArtist(foreignArtistId);
// We do not want any Singles (This will include EP's)
var albumsOnly =
result.Albums.Where(x => !x.Type.Equals("Single", StringComparison.InvariantCultureIgnoreCase));
var vm = new List<SearchAlbumViewModel>();
foreach (var album in albumsOnly)
{
vm.Add(await MapIntoAlbumVm(album, result.Id, result.ArtistName, settings));
}
return vm;
}
/// <summary>
/// Returns the artist that produced the album
/// </summary>
/// <param name="foreignArtistId"></param>
/// <returns></returns>
public async Task<ArtistResult> GetAlbumArtist(string foreignArtistId)
{
var settings = await GetSettings();
return await _lidarrApi.GetArtistByForeignId(foreignArtistId, settings.ApiKey, settings.FullUri);
}
public async Task<ArtistResult> GetArtist(int artistId)
{
var settings = await GetSettings();
return await _lidarrApi.GetArtist(artistId, settings.ApiKey, settings.FullUri);
}
private async Task<SearchArtistViewModel> MapIntoArtistVm(ArtistLookup a)
{
var vm = new SearchArtistViewModel
{
ArtistName = a.artistName,
ArtistType = a.artistType,
Banner = a.images?.FirstOrDefault(x => x.coverType.Equals("banner"))?.url,
Logo = a.images?.FirstOrDefault(x => x.coverType.Equals("logo"))?.url,
CleanName = a.cleanName,
Disambiguation = a.disambiguation,
ForignArtistId = a.foreignArtistId,
Links = a.links,
Overview = a.overview,
};
var poster = a.images?.FirstOrDefault(x => x.coverType.Equals("poaster"));
if (poster == null)
{
vm.Poster = a.remotePoster;
}
await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist);
return vm;
}
private async Task<SearchAlbumViewModel> MapIntoAlbumVm(AlbumLookup a, LidarrSettings settings)
{
var vm = new SearchAlbumViewModel
{
ForeignAlbumId = a.foreignAlbumId,
Monitored = a.monitored,
Rating = a.ratings?.value ?? 0m,
ReleaseDate = a.releaseDate,
Title = a.title,
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url
};
if (a.artistId > 0)
{
//TODO THEY HAVE FIXED THIS IN DEV
// The JSON is different for some stupid reason
// Need to lookup the artist now and all the images -.-"
var artist = await _lidarrApi.GetArtist(a.artistId, settings.ApiKey, settings.FullUri);
vm.ArtistName = artist.artistName;
vm.ForeignArtistId = artist.foreignArtistId;
}
else
{
vm.ForeignArtistId = a.artist?.foreignArtistId;
vm.ArtistName = a.artist?.artistName;
}
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url;
if (vm.Cover.IsNullOrEmpty())
{
vm.Cover = a.remoteCover;
}
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
await RunSearchRules(vm);
return vm;
}
private async Task<SearchAlbumViewModel> MapIntoAlbumVm(Album a, string artistId, string artistName, LidarrSettings settings)
{
var fullAlbum = await _lidarrApi.GetAlbumByForeignId(a.Id, settings.ApiKey, settings.FullUri);
var vm = new SearchAlbumViewModel
{
ForeignAlbumId = a.Id,
Monitored = fullAlbum.monitored,
Rating = fullAlbum.ratings?.value ?? 0m,
ReleaseDate = fullAlbum.releaseDate,
Title = a.Title,
Disk = fullAlbum.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url,
ForeignArtistId = artistId,
ArtistName = artistName,
Cover = fullAlbum.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url
};
if (vm.Cover.IsNullOrEmpty())
{
vm.Cover = fullAlbum.remoteCover;
}
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
await RunSearchRules(vm);
return vm;
}
private LidarrSettings _settings;
private async Task<LidarrSettings> GetSettings()
{
return _settings ?? (_settings = await _lidarrSettings.GetSettingsAsync());
}
}
}

@ -23,6 +23,7 @@ using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
namespace Ombi.Core.Engine
{
@ -587,6 +588,15 @@ namespace Ombi.Core.Engine
NotificationHelper.NewRequest(model);
}
await _requestLog.Add(new RequestLog
{
UserId = (await GetUser()).Id,
RequestDate = DateTime.UtcNow,
RequestId = model.Id,
RequestType = RequestType.TvShow,
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
});
if (model.Approved)
{
// Autosend
@ -602,15 +612,58 @@ namespace Ombi.Core.Engine
};
}
await _requestLog.Add(new RequestLog
return new RequestEngineResult { Result = true };
}
public async Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user)
{
if (user == null)
{
UserId = (await GetUser()).Id,
RequestDate = DateTime.UtcNow,
RequestId = model.Id,
RequestType = RequestType.TvShow,
});
user = await GetUser();
return new RequestEngineResult { Result = true };
// If user is still null after attempting to get the logged in user, return null.
if (user == null)
{
return null;
}
}
int limit = user.EpisodeRequestLimit ?? 0;
if (limit <= 0)
{
return new RequestQuotaCountModel()
{
HasLimit = false,
Limit = 0,
Remaining = 0,
NextRequest = DateTime.Now,
};
}
IQueryable<RequestLog> log = _requestLog.GetAll()
.Where(x => x.UserId == user.Id
&& x.RequestType == RequestType.TvShow
&& x.RequestDate >= DateTime.UtcNow.AddDays(-7));
// Needed, due to a bug which would cause all episode counts to be 0
int zeroEpisodeCount = await log.Where(x => x.EpisodeCount == 0).Select(x => x.EpisodeCount).CountAsync();
int episodeCount = await log.Where(x => x.EpisodeCount != 0).Select(x => x.EpisodeCount).SumAsync();
int count = limit - (zeroEpisodeCount + episodeCount);
DateTime oldestRequestedAt = await log.OrderBy(x => x.RequestDate)
.Select(x => x.RequestDate)
.FirstOrDefaultAsync();
return new RequestQuotaCountModel()
{
HasLimit = true,
Limit = limit,
Remaining = count,
NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc),
};
}
}
}

@ -54,7 +54,16 @@ namespace Ombi.Core.Engine
if (searchResult != null)
{
return await ProcessResults(searchResult);
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in searchResult)
{
if (tvMazeSearch.show.externals == null || !(tvMazeSearch.show.externals?.thetvdb.HasValue ?? false))
{
continue;
}
retVal.Add(await ProcessResult(tvMazeSearch));
}
return retVal;
}
return null;
}
@ -145,12 +154,16 @@ namespace Ombi.Core.Engine
var retVal = new List<SearchTvShowViewModel>();
foreach (var tvMazeSearch in items)
{
var viewT = Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
retVal.Add(await ProcessResult(viewT));
retVal.Add(await ProcessResult(tvMazeSearch));
}
return retVal;
}
private async Task<SearchTvShowViewModel> ProcessResult<T>(T tvMazeSearch)
{
return Mapper.Map<SearchTvShowViewModel>(tvMazeSearch);
}
private async Task<SearchTvShowViewModel> ProcessResult(SearchTvShowViewModel item)
{
item.TheTvDbId = item.Id.ToString();

@ -40,6 +40,18 @@ namespace Ombi.Core
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
public void NewRequest(AlbumRequest model)
{
var notificationModel = new NotificationOptions
{
RequestId = model.Id,
DateTime = DateTime.Now,
NotificationType = NotificationType.NewRequest,
RequestType = model.RequestType
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
public void Notify(MovieRequests model, NotificationType type)
{
@ -66,5 +78,19 @@ namespace Ombi.Core
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
public void Notify(AlbumRequest model, NotificationType type)
{
var notificationModel = new NotificationOptions
{
RequestId = model.Id,
DateTime = DateTime.Now,
NotificationType = type,
RequestType = model.RequestType,
Recipient = model.RequestedUser?.Email ?? string.Empty
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
}
}

@ -0,0 +1,15 @@
using System;
namespace Ombi.Core.Models
{
public class RequestQuotaCountModel
{
public bool HasLimit { get; set; }
public int Limit { get; set; }
public int Remaining { get; set; }
public DateTime NextRequest { get; set; }
}
}

@ -7,5 +7,6 @@ namespace Ombi.Core.Models.Requests
{
IMovieRequestRepository MovieRequestService { get; }
ITvRequestRepository TvRequestService { get; }
IMusicRequestRepository MusicRequestRepository { get; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Core.Models.Requests
{
public class MusicAlbumRequestViewModel
{
public string ForeignAlbumId { get; set; }
}
}

@ -5,13 +5,15 @@ namespace Ombi.Core.Models.Requests
{
public class RequestService : IRequestServiceMain
{
public RequestService(ITvRequestRepository tv, IMovieRequestRepository movie)
public RequestService(ITvRequestRepository tv, IMovieRequestRepository movie, IMusicRequestRepository music)
{
TvRequestService = tv;
MovieRequestService = movie;
MusicRequestRepository = music;
}
public ITvRequestRepository TvRequestService { get; }
public IMusicRequestRepository MusicRequestRepository { get; }
public IMovieRequestRepository MovieRequestService { get; }
}
}

@ -0,0 +1,23 @@
using System;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.Search
{
public class SearchAlbumViewModel : SearchViewModel
{
public string Title { get; set; }
public string ForeignAlbumId { get; set; }
public bool Monitored { get; set; }
public string AlbumType { get; set; }
public decimal Rating { get; set; }
public DateTime ReleaseDate { get; set; }
public string ArtistName { get; set; }
public string ForeignArtistId { get; set; }
public string Cover { get; set; }
public string Disk { get; set; }
public decimal PercentOfTracks { get; set; }
public override RequestType Type => RequestType.Album;
public bool PartiallyAvailable => PercentOfTracks != 100 && PercentOfTracks > 0;
public bool FullyAvailable => PercentOfTracks == 100;
}
}

@ -0,0 +1,19 @@
using Ombi.Api.Lidarr.Models;
namespace Ombi.Core.Models.Search
{
public class SearchArtistViewModel
{
public string ArtistName { get; set; }
public string ForignArtistId { get; set; }
public string Overview { get; set; }
public string Disambiguation { get; set; }
public string Banner { get; set; }
public string Poster { get; set; }
public string Logo { get; set; }
public bool Monitored { get; set; }
public string ArtistType { get; set; }
public string CleanName { get; set; }
public Link[] Links { get; set; } // Couldn't be bothered to map it
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
@ -16,6 +17,11 @@ namespace Ombi.Core.Models.UI
public UserType UserType { get; set; }
public int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; }
public RequestQuotaCountModel EpisodeRequestQuota { get; set; }
public RequestQuotaCountModel MovieRequestQuota { get; set; }
public RequestQuotaCountModel MusicRequestQuota { get; set; }
public int MusicRequestLimit { get; set; }
public UserQualityProfiles UserQualityProfiles { get; set; }
}
public class ClaimCheckboxes

@ -13,8 +13,7 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="2.0.0-preview1-final" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.3" />
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
@ -22,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.DogNzb\Ombi.Api.DogNzb.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />

@ -3,5 +3,7 @@
public enum SpecificRules
{
CanSendNotification,
LidarrArtist,
LidarrAlbum,
}
}

@ -29,6 +29,8 @@ namespace Ombi.Core.Rule.Rules.Request
obj.Approved = true;
if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiRoles.AutoApproveTv))
obj.Approved = true;
if (obj.RequestType == RequestType.Album && User.IsInRole(OmbiRoles.AutoApproveMusic))
obj.Approved = true;
return Task.FromResult(Success()); // We don't really care, we just don't set the obj to approve
}
}

@ -23,13 +23,23 @@ namespace Ombi.Core.Rule.Rules
if (obj.RequestType == RequestType.Movie)
{
if (User.IsInRole(OmbiRoles.RequestMovie))
if (User.IsInRole(OmbiRoles.RequestMovie) || User.IsInRole(OmbiRoles.AutoApproveMovie))
return Task.FromResult(Success());
return Task.FromResult(Fail("You do not have permissions to Request a Movie"));
}
if (User.IsInRole(OmbiRoles.RequestTv))
return Task.FromResult(Success());
if (obj.RequestType == RequestType.TvShow)
{
if (User.IsInRole(OmbiRoles.RequestTv) || User.IsInRole(OmbiRoles.AutoApproveTv))
return Task.FromResult(Success());
}
if (obj.RequestType == RequestType.Album)
{
if (User.IsInRole(OmbiRoles.RequestMusic) || User.IsInRole(OmbiRoles.AutoApproveMusic))
return Task.FromResult(Success());
}
return Task.FromResult(Fail("You do not have permissions to Request a TV Show"));
}
}

@ -54,6 +54,7 @@ namespace Ombi.Core.Rule.Rules.Request
var movieLimit = user.MovieRequestLimit;
var episodeLimit = user.EpisodeRequestLimit;
var musicLimit = user.MusicRequestLimit;
var requestLog = _requestLog.GetAll().Where(x => x.UserId == obj.RequestedUserId);
if (obj.RequestType == RequestType.Movie)
@ -71,7 +72,7 @@ namespace Ombi.Core.Rule.Rules.Request
return Fail("You have exceeded your Movie request quota!");
}
}
else
else if (obj.RequestType == RequestType.TvShow)
{
if (episodeLimit <= 0)
return Success();
@ -81,21 +82,40 @@ namespace Ombi.Core.Rule.Rules.Request
// Get the count of requests to be made
foreach (var s in child.SeasonRequests)
{
requestCount = s.Episodes.Count;
requestCount += s.Episodes.Count;
}
var tvLogs = requestLog.Where(x => x.RequestType == RequestType.TvShow);
// Count how many requests in the past 7 days
var tv = tvLogs.Where(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7));
var count = await tv.Select(x => x.EpisodeCount).CountAsync();
count += requestCount; // Add the amount of requests in
// Needed, due to a bug which would cause all episode counts to be 0
var zeroEpisodeCount = await tv.Where(x => x.EpisodeCount == 0).Select(x => x.EpisodeCount).CountAsync();
var episodeCount = await tv.Where(x => x.EpisodeCount != 0).Select(x => x.EpisodeCount).SumAsync();
var count = requestCount + episodeCount + zeroEpisodeCount; // Add the amount of requests in
if (count > episodeLimit)
{
return Fail("You have exceeded your Episode request quota!");
}
} else if (obj.RequestType == RequestType.Album)
{
if (musicLimit <= 0)
return Success();
var albumLogs = requestLog.Where(x => x.RequestType == RequestType.Album);
// Count how many requests in the past 7 days
var count = await albumLogs.CountAsync(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7));
count += 1; // Since we are including this request
if (count > musicLimit)
{
return Fail("You have exceeded your Album request quota!");
}
}
return Success();
return Success();
}
}
}

@ -11,20 +11,22 @@ namespace Ombi.Core.Rule.Rules.Search
{
public class ExistingRule : BaseSearchRule, IRules<SearchViewModel>
{
public ExistingRule(IMovieRequestRepository movie, ITvRequestRepository tv)
public ExistingRule(IMovieRequestRepository movie, ITvRequestRepository tv, IMusicRequestRepository music)
{
Movie = movie;
Tv = tv;
Music = music;
}
private IMovieRequestRepository Movie { get; }
private IMusicRequestRepository Music { get; }
private ITvRequestRepository Tv { get; }
public Task<RuleResult> Execute(SearchViewModel obj)
public async Task<RuleResult> Execute(SearchViewModel obj)
{
if (obj.Type == RequestType.Movie)
{
var movieRequests = Movie.GetRequest(obj.Id);
var movieRequests = await Movie.GetRequestAsync(obj.Id);
if (movieRequests != null) // Do we already have a request for this?
{
@ -33,11 +35,11 @@ namespace Ombi.Core.Rule.Rules.Search
obj.Approved = movieRequests.Approved;
obj.Available = movieRequests.Available;
return Task.FromResult(Success());
return Success();
}
return Task.FromResult(Success());
return Success();
}
else
if (obj.Type == RequestType.TvShow)
{
//var tvRequests = Tv.GetRequest(obj.Id);
//if (tvRequests != null) // Do we already have a request for this?
@ -50,7 +52,7 @@ namespace Ombi.Core.Rule.Rules.Search
// return Task.FromResult(Success());
//}
var request = (SearchTvShowViewModel) obj;
var request = (SearchTvShowViewModel)obj;
var tvRequests = Tv.GetRequest(obj.Id);
if (tvRequests != null) // Do we already have a request for this?
{
@ -94,8 +96,24 @@ namespace Ombi.Core.Rule.Rules.Search
request.PartlyAvailable = true;
}
return Task.FromResult(Success());
return Success();
}
if (obj.Type == RequestType.Album)
{
var album = (SearchAlbumViewModel) obj;
var albumRequest = await Music.GetRequestAsync(album.ForeignAlbumId);
if (albumRequest != null) // Do we already have a request for this?
{
obj.Requested = true;
obj.RequestId = albumRequest.Id;
obj.Approved = albumRequest.Approved;
obj.Available = albumRequest.Available;
return Success();
}
return Success();
}
return Success();
}
}
}

@ -0,0 +1,36 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Search
{
public class LidarrAlbumCacheRule : SpecificRule, ISpecificRule<object>
{
public LidarrAlbumCacheRule(IRepository<LidarrAlbumCache> db)
{
_db = db;
}
private readonly IRepository<LidarrAlbumCache> _db;
public Task<RuleResult> Execute(object objec)
{
var obj = (SearchAlbumViewModel) objec;
// Check if it's in Lidarr
var result = _db.GetAll().FirstOrDefault(x => x.ForeignAlbumId.Equals(obj.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
if (result != null)
{
obj.PercentOfTracks = result.PercentOfTracks;
obj.Monitored = true; // It's in Lidarr so it's monitored
}
return Task.FromResult(Success());
}
public override SpecificRules Rule => SpecificRules.LidarrAlbum;
}
}

@ -0,0 +1,35 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Rule.Rules.Search
{
public class LidarrArtistCacheRule : SpecificRule, ISpecificRule<object>
{
public LidarrArtistCacheRule(IRepository<LidarrArtistCache> db)
{
_db = db;
}
private readonly IRepository<LidarrArtistCache> _db;
public Task<RuleResult> Execute(object objec)
{
var obj = (SearchArtistViewModel) objec;
// Check if it's in Lidarr
var result = _db.GetAll().FirstOrDefault(x => x.ForeignArtistId.Equals(obj.ForignArtistId, StringComparison.InvariantCultureIgnoreCase));
if (result != null)
{
obj.Monitored = true; // It's in Lidarr so it's monitored
}
return Task.FromResult(Success());
}
public override SpecificRules Rule => SpecificRules.LidarrArtist;
}
}

@ -42,6 +42,13 @@ namespace Ombi.Core.Rule.Rules.Specific
sendNotification = !await UserManager.IsInRoleAsync(requestedUser, OmbiRoles.AutoApproveTv);
}
}
else if (req.RequestType == RequestType.Album)
{
if (settings.DoNotSendNotificationsForAutoApprove)
{
sendNotification = !await UserManager.IsInRoleAsync(requestedUser, OmbiRoles.AutoApproveMusic);
}
}
if (await UserManager.IsInRoleAsync(requestedUser, OmbiRoles.Admin))
{

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders
{
public interface IMusicSender
{
Task<SenderResult> Send(AlbumRequest model);
}
}

@ -8,7 +8,9 @@ namespace Ombi.Core
{
void NewRequest(FullBaseRequest model);
void NewRequest(ChildRequests model);
void NewRequest(AlbumRequest model);
void Notify(MovieRequests model, NotificationType type);
void Notify(ChildRequests model, NotificationType type);
void Notify(AlbumRequest model, NotificationType type);
}
}

@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.CouchPotato;
using Ombi.Api.DogNzb.Models;
@ -9,6 +10,8 @@ using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
using Ombi.Api.DogNzb;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.Senders
{
@ -16,7 +19,7 @@ namespace Ombi.Core.Senders
{
public MovieSender(ISettingsService<RadarrSettings> radarrSettings, IRadarrApi api, ILogger<MovieSender> log,
ISettingsService<DogNzbSettings> dogSettings, IDogNzbApi dogApi, ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi cpApi)
ICouchPotatoApi cpApi, IRepository<UserQualityProfiles> userProfiles)
{
RadarrSettings = radarrSettings;
RadarrApi = api;
@ -25,6 +28,7 @@ namespace Ombi.Core.Senders
DogNzbApi = dogApi;
CouchPotatoSettings = cpSettings;
CouchPotatoApi = cpApi;
_userProfiles = userProfiles;
}
private ISettingsService<RadarrSettings> RadarrSettings { get; }
@ -34,6 +38,7 @@ namespace Ombi.Core.Senders
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<CouchPotatoSettings> CouchPotatoSettings { get; }
private ICouchPotatoApi CouchPotatoApi { get; }
private readonly IRepository<UserQualityProfiles> _userProfiles;
public async Task<SenderResult> Send(MovieRequests model)
{
@ -88,13 +93,33 @@ namespace Ombi.Core.Senders
private async Task<SenderResult> SendToRadarr(MovieRequests model, RadarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
var rootFolderPath = settings.DefaultRootPath;
var profiles = await _userProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
if (profiles != null)
{
if (profiles.SonarrRootPathAnime > 0)
{
rootFolderPath = await RadarrRootPath(profiles.SonarrRootPathAnime, settings);
}
if (profiles.SonarrQualityProfileAnime > 0)
{
qualityToUse = profiles.SonarrQualityProfileAnime;
}
}
// Overrides on the request take priority
if (model.QualityOverride > 0)
{
qualityToUse = model.QualityOverride;
}
var rootFolderPath = model.RootPathOverride <= 0 ? settings.DefaultRootPath : await RadarrRootPath(model.RootPathOverride, settings);
if (model.RootPathOverride > 0)
{
rootFolderPath = await RadarrRootPath(model.RootPathOverride, settings);
}
// Check if the movie already exists? Since it could be unmonitored
var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri);
@ -123,7 +148,10 @@ namespace Ombi.Core.Senders
existingMovie.monitored = true;
await RadarrApi.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
if (!settings.AddOnly)
{
await RadarrApi.MovieSearch(new[] {existingMovie.id}, settings.ApiKey, settings.FullUri);
}
return new SenderResult { Success = true, Sent = true };
}

@ -0,0 +1,130 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
using Serilog;
namespace Ombi.Core.Senders
{
public class MusicSender : IMusicSender
{
public MusicSender(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
public async Task<SenderResult> Send(AlbumRequest model)
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
//if (model.QualityOverride > 0)
//{
// qualityToUse = model.QualityOverride;
//}
var rootFolderPath = /*model.RootPathOverride <= 0 ?*/ settings.DefaultRootPath /*: await RadarrRootPath(model.RootPathOverride, settings)*/;
// Need to get the artist
var artist = await _lidarrApi.GetArtistByForeignId(model.ForeignArtistId, settings.ApiKey, settings.FullUri);
if (artist == null || artist.id <= 0)
{
// Create artist
var newArtist = new ArtistAdd
{
foreignArtistId = model.ForeignArtistId,
addOptions = new Addoptions
{
monitored = true,
searchForMissingAlbums = false,
selectedOption = 6, // None
AlbumsToMonitor = new[] {model.ForeignAlbumId}
},
added = DateTime.Now,
monitored = true,
albumFolder = settings.AlbumFolder,
artistName = model.ArtistName,
cleanName = model.ArtistName.ToLowerInvariant().RemoveSpaces(),
images = new Image[] { },
languageProfileId = settings.LanguageProfileId,
links = new Link[] {},
metadataProfileId = settings.MetadataProfileId,
qualityProfileId = qualityToUse,
rootFolderPath = rootFolderPath,
};
var result = await _lidarrApi.AddArtist(newArtist, settings.ApiKey, settings.FullUri);
if (result != null && result.id > 0)
{
// Setup the albums
return new SenderResult { Message = "Album has been requested!", Sent = true, Success = true };
}
}
else
{
await SetupAlbum(model, artist, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Album is already monitored" };
}
private async Task<SenderResult> SetupAlbum(AlbumRequest model, ArtistResult artist, LidarrSettings settings)
{
// Get the album id
var albums = await _lidarrApi.GetAllAlbumsByArtistId(artist.id, settings.ApiKey, settings.FullUri);
var album = albums.FirstOrDefault(x =>
x.foreignAlbumId.Equals(model.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
var maxRetryCount = 10; // 5 seconds
var currentRetry = 0;
while (!albums.Any() || album == null)
{
if (currentRetry >= maxRetryCount)
{
break;
}
currentRetry++;
await Task.Delay(500);
albums = await _lidarrApi.GetAllAlbumsByArtistId(artist.id, settings.ApiKey, settings.FullUri);
album = albums.FirstOrDefault(x =>
x.foreignAlbumId.Equals(model.ForeignAlbumId, StringComparison.InvariantCultureIgnoreCase));
}
// Get the album we want.
if (album == null)
{
return new SenderResult { Message = "Could not find album in Lidarr", Sent = false, Success = false };
}
var result = await _lidarrApi.MontiorAlbum(album.id, settings.ApiKey, settings.FullUri);
if (!settings.AddOnly)
{
await _lidarrApi.AlbumSearch(new[] {result.id}, settings.ApiKey, settings.FullUri);
}
if (result.monitored)
{
return new SenderResult {Message = "Album has been requested!", Sent = true, Success = true};
}
return new SenderResult { Message = "Could not set album to monitored", Sent = false, Success = false };
}
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models;
@ -12,7 +13,9 @@ using Ombi.Api.Sonarr.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
namespace Ombi.Core.Senders
{
@ -20,7 +23,7 @@ namespace Ombi.Core.Senders
{
public TvSender(ISonarrApi sonarrApi, ILogger<TvSender> log, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<DogNzbSettings> dog, IDogNzbApi dogApi, ISettingsService<SickRageSettings> srSettings,
ISickRageApi srApi)
ISickRageApi srApi, IRepository<UserQualityProfiles> userProfiles)
{
SonarrApi = sonarrApi;
Logger = log;
@ -29,6 +32,7 @@ namespace Ombi.Core.Senders
DogNzbApi = dogApi;
SickRageSettings = srSettings;
SickRageApi = srApi;
UserQualityProfiles = userProfiles;
}
private ISonarrApi SonarrApi { get; }
@ -38,6 +42,7 @@ namespace Ombi.Core.Senders
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<DogNzbSettings> DogNzbSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private IRepository<UserQualityProfiles> UserQualityProfiles { get; }
public async Task<SenderResult> Send(ChildRequests model)
{
@ -122,13 +127,25 @@ namespace Ombi.Core.Senders
string rootFolderPath;
string seriesType;
var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
if (model.SeriesType == SeriesType.Anime)
{
// Get the root path from the rootfolder selected.
// For some reason, if we haven't got one use the first root folder in Sonarr
// TODO make this overrideable via the UI
rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder ?? int.Parse(s.RootPathAnime), s);
int.TryParse(s.QualityProfileAnime, out qualityToUse);
if (profiles != null)
{
if (profiles.SonarrRootPathAnime > 0)
{
rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPathAnime, s);
}
if (profiles.SonarrQualityProfileAnime > 0)
{
qualityToUse = profiles.SonarrQualityProfileAnime;
}
}
seriesType = "anime";
}
@ -137,11 +154,22 @@ namespace Ombi.Core.Senders
int.TryParse(s.QualityProfile, out qualityToUse);
// Get the root path from the rootfolder selected.
// For some reason, if we haven't got one use the first root folder in Sonarr
// TODO make this overrideable via the UI
rootFolderPath = await GetSonarrRootPath(model.ParentRequest.RootFolder ?? int.Parse(s.RootPath), s);
if (profiles != null)
{
if (profiles.SonarrRootPath > 0)
{
rootFolderPath = await GetSonarrRootPath(profiles.SonarrRootPath, s);
}
if (profiles.SonarrQualityProfile > 0)
{
qualityToUse = profiles.SonarrQualityProfile;
}
}
seriesType = "standard";
}
// Overrides on the request take priority
if (model.ParentRequest.QualityOverride.HasValue)
{
qualityToUse = model.ParentRequest.QualityOverride.Value;

@ -32,6 +32,7 @@ using Ombi.Api.CouchPotato;
using Ombi.Api.DogNzb;
using Ombi.Api.FanartTv;
using Ombi.Api.Github;
using Ombi.Api.Lidarr;
using Ombi.Api.Mattermost;
using Ombi.Api.Notifications;
using Ombi.Api.Pushbullet;
@ -53,9 +54,11 @@ using PlexContentCacher = Ombi.Schedule.Jobs.Plex;
using Ombi.Api.Telegram;
using Ombi.Core.Authentication;
using Ombi.Core.Processor;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.SickRage;
using Ombi.Schedule.Processor;
using Ombi.Store.Entities;
namespace Ombi.DependencyInjection
{
@ -82,7 +85,10 @@ namespace Ombi.DependencyInjection
services.AddTransient<IUserStatsEngine, UserStatsEngine>();
services.AddTransient<IMovieSender, MovieSender>();
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<IMusicSearchEngine, MusicSearchEngine>();
services.AddTransient<IMusicRequestEngine, MusicRequestEngine>();
services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
}
@ -117,6 +123,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ISickRageApi, SickRageApi>();
services.AddTransient<IAppVeyorApi, AppVeyorApi>();
services.AddTransient<IOneSignalApi, OneSignalApi>();
services.AddTransient<ILidarrApi, LidarrApi>();
}
public static void RegisterStore(this IServiceCollection services) {
@ -131,6 +138,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ITvRequestRepository, TvRequestRepository>();
services.AddTransient<IMovieRequestRepository, MovieRequestRepository>();
services.AddTransient<IMusicRequestRepository, MusicRequestRepository>();
services.AddTransient<IAuditRepository, AuditRepository>();
services.AddTransient<IApplicationConfigRepository, ApplicationConfigRepository>();
services.AddTransient<ITokenRepository, TokenRepository>();
@ -180,6 +188,9 @@ namespace Ombi.DependencyInjection
services.AddTransient<IRefreshMetadata, RefreshMetadata>();
services.AddTransient<INewsletterJob, NewsletterJob>();
services.AddTransient<IPlexRecentlyAddedSync, PlexRecentlyAddedSync>();
services.AddTransient<ILidarrAlbumSync, LidarrAlbumSync>();
services.AddTransient<ILidarrArtistSync, LidarrArtistSync>();
services.AddTransient<ILidarrAvailabilityChecker, LidarrAvailabilityChecker>();
}
}
}

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
</ItemGroup>
@ -21,6 +21,7 @@
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />
<ProjectReference Include="..\Ombi.Api.Github\Ombi.Api.Github.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />

@ -18,6 +18,8 @@ namespace Ombi.Helpers
public const string NowPlayingMovies = nameof(NowPlayingMovies);
public const string RadarrRootProfiles = nameof(RadarrRootProfiles);
public const string RadarrQualityProfiles = nameof(RadarrQualityProfiles);
public const string LidarrRootFolders = nameof(LidarrRootFolders);
public const string LidarrQualityProfiles = nameof(LidarrQualityProfiles);
public const string FanartTv = nameof(FanartTv);
}
}

@ -20,6 +20,7 @@ namespace Ombi.Helpers
public static EventId CouchPotatoCacher => new EventId(2007);
public static EventId PlexContentCacher => new EventId(2008);
public static EventId SickRageCacher => new EventId(2009);
public static EventId LidarrArtistCache => new EventId(2010);
public static EventId MovieSender => new EventId(3000);

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" />

@ -7,9 +7,11 @@
public const string Admin = nameof(Admin);
public const string AutoApproveMovie = nameof(AutoApproveMovie);
public const string AutoApproveTv = nameof(AutoApproveTv);
public const string AutoApproveMusic = nameof(AutoApproveMusic);
public const string PowerUser = nameof(PowerUser);
public const string RequestTv = nameof(RequestTv);
public const string RequestMovie = nameof(RequestMovie);
public const string RequestMusic = nameof(RequestMusic);
public const string Disabled = nameof(Disabled);
public const string ReceivesNewsletter = nameof(ReceivesNewsletter);
}

@ -75,5 +75,14 @@ namespace Ombi.Helpers
return -1;
}
public static string RemoveSpaces(this string str)
{
return str.Replace(" ", "");
}
public static string StripCharacters(this string str, params char[] chars)
{
return string.Concat(str.Where(c => !chars.Contains(c)));
}
}
}

@ -20,8 +20,9 @@ namespace Ombi.Notifications.Agents
{
public DiscordNotification(IDiscordApi api, ISettingsService<DiscordNotificationSettings> sn,
ILogger<DiscordNotification> log, INotificationTemplatesRepository r,
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub)
: base(sn, r, m, t,s,log, sub)
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref)
: base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;
@ -130,12 +131,18 @@ namespace Ombi.Notifications.Agents
title = MovieRequest.Title;
image = MovieRequest.PosterPath;
}
else
else if (model.RequestType == RequestType.TvShow)
{
user = TvRequest.RequestedUser.UserAlias;
title = TvRequest.ParentRequest.Title;
image = TvRequest.ParentRequest.PosterPath;
}
else if (model.RequestType == RequestType.Album)
{
user = AlbumRequest.RequestedUser.UserAlias;
title = AlbumRequest.Title;
image = AlbumRequest.Cover;
}
var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying";
var notification = new NotificationMessage
{

@ -22,7 +22,8 @@ namespace Ombi.Notifications.Agents
public class EmailNotification : BaseNotification<EmailNotificationSettings>, IEmailNotification
{
public EmailNotification(ISettingsService<EmailNotificationSettings> settings, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, IEmailProvider prov, ISettingsService<CustomizationSettings> c,
ILogger<EmailNotification> log, UserManager<OmbiUser> um, IRepository<RequestSubscription> sub) : base(settings, r, m, t, c, log, sub)
ILogger<EmailNotification> log, UserManager<OmbiUser> um, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(settings, r, m, t, c, log, sub, music, userPref)
{
EmailProvider = prov;
Logger = log;

@ -21,7 +21,8 @@ namespace Ombi.Notifications.Agents
public class MattermostNotification : BaseNotification<MattermostNotificationSettings>, IMattermostNotification
{
public MattermostNotification(IMattermostApi api, ISettingsService<MattermostNotificationSettings> sn, ILogger<MattermostNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub) : base(sn, r, m, t,s,log, sub)
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;

@ -22,7 +22,8 @@ namespace Ombi.Notifications.Agents
{
public MobileNotification(IOneSignalApi api, ISettingsService<MobileNotificationSettings> sn, ILogger<MobileNotification> log, INotificationTemplatesRepository r,
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s, IRepository<NotificationUserId> notification,
UserManager<OmbiUser> um, IRepository<RequestSubscription> sub) : base(sn, r, m, t, s,log, sub)
UserManager<OmbiUser> um, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
_api = api;
_logger = log;

@ -17,7 +17,8 @@ namespace Ombi.Notifications.Agents
public class PushbulletNotification : BaseNotification<PushbulletSettings>, IPushbulletNotification
{
public PushbulletNotification(IPushbulletApi api, ISettingsService<PushbulletSettings> sn, ILogger<PushbulletNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub) : base(sn, r, m, t,s,log, sub)
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;

@ -18,7 +18,8 @@ namespace Ombi.Notifications.Agents
public class PushoverNotification : BaseNotification<PushoverSettings>, IPushoverNotification
{
public PushoverNotification(IPushoverApi api, ISettingsService<PushoverSettings> sn, ILogger<PushoverNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub) : base(sn, r, m, t, s, log, sub)
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;
@ -177,7 +178,8 @@ namespace Ombi.Notifications.Agents
{
try
{
await Api.PushAsync(settings.AccessToken, model.Message, settings.UserToken);
//&+' < >
await Api.PushAsync(settings.AccessToken, model.Message.StripCharacters('&','+','<','>'), settings.UserToken, settings.Priority, settings.Sound);
}
catch (Exception e)
{

@ -18,7 +18,8 @@ namespace Ombi.Notifications.Agents
public class SlackNotification : BaseNotification<SlackNotificationSettings>, ISlackNotification
{
public SlackNotification(ISlackApi api, ISettingsService<SlackNotificationSettings> sn, ILogger<SlackNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub) : base(sn, r, m, t, s, log, sub)
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
Api = api;
Logger = log;

@ -19,7 +19,8 @@ namespace Ombi.Notifications.Agents
public TelegramNotification(ITelegramApi api, ISettingsService<TelegramSettings> sn, ILogger<TelegramNotification> log,
INotificationTemplatesRepository r, IMovieRequestRepository m,
ITvRequestRepository t, ISettingsService<CustomizationSettings> s
, IRepository<RequestSubscription> sub) : base(sn, r, m, t,s,log, sub)
, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t,s,log, sub, music, userPref)
{
Api = api;
Logger = log;

@ -19,7 +19,8 @@ namespace Ombi.Notifications.Interfaces
public abstract class BaseNotification<T> : INotification where T : Settings.Settings.Models.Settings, new()
{
protected BaseNotification(ISettingsService<T> settings, INotificationTemplatesRepository templateRepo, IMovieRequestRepository movie, ITvRequestRepository tv,
ISettingsService<CustomizationSettings> customization, ILogger<BaseNotification<T>> log, IRepository<RequestSubscription> sub)
ISettingsService<CustomizationSettings> customization, ILogger<BaseNotification<T>> log, IRepository<RequestSubscription> sub, IMusicRequestRepository album,
IRepository<UserNotificationPreferences> notificationUserPreferences)
{
Settings = settings;
TemplateRepository = templateRepo;
@ -30,19 +31,24 @@ namespace Ombi.Notifications.Interfaces
CustomizationSettings.ClearCache();
RequestSubscription = sub;
_log = log;
AlbumRepository = album;
UserNotificationPreferences = notificationUserPreferences;
}
protected ISettingsService<T> Settings { get; }
protected INotificationTemplatesRepository TemplateRepository { get; }
protected IMovieRequestRepository MovieRepository { get; }
protected ITvRequestRepository TvRepository { get; }
protected IMusicRequestRepository AlbumRepository { get; }
protected CustomizationSettings Customization { get; set; }
protected IRepository<RequestSubscription> RequestSubscription { get; set; }
protected IRepository<UserNotificationPreferences> UserNotificationPreferences { get; set; }
private ISettingsService<CustomizationSettings> CustomizationSettings { get; }
private readonly ILogger<BaseNotification<T>> _log;
protected ChildRequests TvRequest { get; set; }
protected AlbumRequest AlbumRequest { get; set; }
protected MovieRequests MovieRequest { get; set; }
protected IQueryable<OmbiUser> SubsribedUsers { get; private set; }
@ -130,10 +136,14 @@ namespace Ombi.Notifications.Interfaces
{
MovieRequest = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId);
}
else
else if (type == RequestType.TvShow)
{
TvRequest = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId);
}
else if (type == RequestType.Album)
{
AlbumRequest = await AlbumRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId);
}
}
private async Task<T> GetConfiguration()
@ -160,7 +170,7 @@ namespace Ombi.Notifications.Interfaces
{
return new NotificationMessageContent { Disabled = true };
}
var parsed = Parse(model, template);
var parsed = Parse(model, template, agent);
return parsed;
}
@ -171,20 +181,32 @@ namespace Ombi.Notifications.Interfaces
return subs.Select(x => x.User);
}
private NotificationMessageContent Parse(NotificationOptions model, NotificationTemplates template)
protected UserNotificationPreferences GetUserPreference(string userId, NotificationAgent agent)
{
return UserNotificationPreferences.GetAll()
.FirstOrDefault(x => x.Enabled && x.Agent == agent && x.UserId == userId);
}
private NotificationMessageContent Parse(NotificationOptions model, NotificationTemplates template, NotificationAgent agent)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
var preference = GetUserPreference(model.UserId, agent);
if (model.RequestType == RequestType.Movie)
{
_log.LogDebug("Notification options: {@model}, Req: {@MovieRequest}, Settings: {@Customization}", model, MovieRequest, Customization);
curlys.Setup(model, MovieRequest, Customization);
curlys.Setup(model, MovieRequest, Customization, preference);
}
else
else if (model.RequestType == RequestType.TvShow)
{
_log.LogDebug("Notification options: {@model}, Req: {@TvRequest}, Settings: {@Customization}", model, TvRequest, Customization);
curlys.Setup(model, TvRequest, Customization);
curlys.Setup(model, TvRequest, Customization, preference);
}
else if (model.RequestType == RequestType.Album)
{
_log.LogDebug("Notification options: {@model}, Req: {@AlbumRequest}, Settings: {@Customization}", model, AlbumRequest, Customization);
curlys.Setup(model, AlbumRequest, Customization, preference);
}
var parsed = resolver.ParseMessage(template, curlys);

@ -14,9 +14,10 @@ namespace Ombi.Notifications
{
public class NotificationMessageCurlys
{
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s)
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
UserPreference = pref.Enabled ? pref.Value : string.Empty;
string title;
if (req == null)
{
@ -47,14 +48,49 @@ namespace Ombi.Notifications
if (req?.RequestType == RequestType.Movie)
{
PosterImage = string.Format((req?.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
PosterImage = string.Format((req?.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
? "https://image.tmdb.org/t/p/w300{0}" : "https://image.tmdb.org/t/p/w300/{0}", req?.PosterPath);
}
else
{
PosterImage = req?.PosterPath;
}
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
}
public void Setup(NotificationOptions opts, AlbumRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
UserPreference = pref.Enabled ? pref.Value : string.Empty;
string title;
if (req == null)
{
opts.Substitutes.TryGetValue("Title", out title);
}
else
{
title = req?.Title;
}
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = req?.RequestedUser?.UserName;
if (UserName.IsNullOrEmpty())
{
// Can be set if it's an issue
UserName = req?.RequestedUser?.UserName;
}
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
{
Type = req?.RequestType.Humanize();
}
Year = req?.ReleaseDate.Year.ToString();
PosterImage = (req?.Cover.HasValue() ?? false) ? req.Cover : req?.Disk ?? string.Empty;
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
}
@ -67,9 +103,10 @@ namespace Ombi.Notifications
Alias = username.Alias.HasValue() ? username.Alias : username.UserName;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
UserPreference = pref.Enabled ? pref.Value : string.Empty;
string title;
if (req == null)
{
@ -187,6 +224,7 @@ namespace Ombi.Notifications
public string IssueStatus { get; set; }
public string IssueSubject { get; set; }
public string NewIssueComment { get; set; }
public string UserPreference { get; set; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");

@ -18,7 +18,7 @@ namespace Ombi.Schedule.Tests
var emailSettings = new Mock<ISettingsService<EmailNotificationSettings>>();
var customziation = new Mock<ISettingsService<CustomizationSettings>>();
var newsletterSettings = new Mock<ISettingsService<NewsletterSettings>>();
var newsletter = new NewsletterJob(null, null, null, null, null, null, customziation.Object, emailSettings.Object, null, null, newsletterSettings.Object, null);
var newsletter = new NewsletterJob(null, null, null, null, null, null, customziation.Object, emailSettings.Object, null, null, newsletterSettings.Object, null, null, null, null);
var ep = new List<int>();
foreach (var i in episodes)

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />

@ -4,6 +4,7 @@ using Ombi.Core.Settings;
using Ombi.Schedule.Jobs;
using Ombi.Schedule.Jobs.Couchpotato;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Radarr;
@ -19,7 +20,7 @@ namespace Ombi.Schedule
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex)
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex, ILidarrArtistSync artist)
{
_plexContentSync = plexContentSync;
_radarrSync = radarrSync;
@ -34,6 +35,7 @@ namespace Ombi.Schedule
_refreshMetadata = refresh;
_newsletter = newsletter;
_plexRecentlyAddedSync = recentlyAddedPlex;
_lidarrArtistSync = artist;
}
private readonly IPlexContentSync _plexContentSync;
@ -49,6 +51,7 @@ namespace Ombi.Schedule
private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter;
private readonly ILidarrArtistSync _lidarrArtistSync;
public void Setup()
{
@ -62,6 +65,7 @@ namespace Ombi.Schedule
RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s));
RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s));
RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s));
RecurringJob.AddOrUpdate(() => _lidarrArtistSync.CacheContent(), JobSettingsHelper.LidarrArtistSync(s));
RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s));

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Store.Entities;
namespace Ombi.Schedule.Jobs.Lidarr
{
public interface ILidarrAlbumSync
{
Task CacheContent();
void Dispose();
Task<IEnumerable<LidarrAlbumCache>> GetCachedContent();
}
}

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Store.Entities;
namespace Ombi.Schedule.Jobs.Lidarr
{
public interface ILidarrArtistSync
{
Task CacheContent();
void Dispose();
Task<IEnumerable<LidarrArtistCache>> GetCachedContent();
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Lidarr
{
public interface ILidarrAvailabilityChecker
{
Task Start();
}
}

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Schedule.Jobs.Lidarr
{
public class LidarrAlbumSync : ILidarrAlbumSync
{
public LidarrAlbumSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrAlbumSync> log, IOmbiContext ctx,
IBackgroundJobClient job, ILidarrAvailabilityChecker availability)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
_logger = log;
_ctx = ctx;
_job = job;
_availability = availability;
_lidarrSettings.ClearCache();
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly ILogger _logger;
private readonly IOmbiContext _ctx;
private readonly IBackgroundJobClient _job;
private readonly ILidarrAvailabilityChecker _availability;
public async Task CacheContent()
{
try
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
try
{
var albums = await _lidarrApi.GetAllAlbums(settings.ApiKey, settings.FullUri);
if (albums != null && albums.Any())
{
// Let's remove the old cached data
await _ctx.Database.ExecuteSqlCommandAsync("DELETE FROM LidarrAlbumCache");
var albumCache = new List<LidarrAlbumCache>();
foreach (var a in albums)
{
if (a.id > 0)
{
albumCache.Add(new LidarrAlbumCache
{
ArtistId = a.artistId,
ForeignAlbumId = a.foreignAlbumId,
ReleaseDate = a.releaseDate,
TrackCount = a.currentRelease.trackCount,
Monitored = a.monitored,
Title = a.title,
PercentOfTracks = a.statistics?.percentOfEpisodes ?? 0m,
AddedAt = DateTime.Now,
});
}
}
await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache);
await _ctx.SaveChangesAsync();
}
}
catch (System.Exception ex)
{
_logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr Album");
}
_job.Enqueue(() => _availability.Start());
}
}
catch (Exception)
{
_logger.LogInformation(LoggingEvents.LidarrArtistCache, "Lidarr is not setup, cannot cache Album");
}
}
public async Task<IEnumerable<LidarrAlbumCache>> GetCachedContent()
{
return await _ctx.LidarrAlbumCache.ToListAsync();
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_ctx?.Dispose();
_lidarrSettings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Radarr;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Schedule.Jobs.Lidarr
{
public class LidarrArtistSync : ILidarrArtistSync
{
public LidarrArtistSync(ISettingsService<LidarrSettings> lidarr, ILidarrApi lidarrApi, ILogger<LidarrArtistSync> log, IOmbiContext ctx,
IBackgroundJobClient background, ILidarrAlbumSync album)
{
_lidarrSettings = lidarr;
_lidarrApi = lidarrApi;
_logger = log;
_ctx = ctx;
_job = background;
_albumSync = album;
_lidarrSettings.ClearCache();
}
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
private readonly ILogger _logger;
private readonly IOmbiContext _ctx;
private readonly IBackgroundJobClient _job;
private readonly ILidarrAlbumSync _albumSync;
public async Task CacheContent()
{
try
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
try
{
var artists = await _lidarrApi.GetArtists(settings.ApiKey, settings.FullUri);
if (artists != null && artists.Any())
{
// Let's remove the old cached data
await _ctx.Database.ExecuteSqlCommandAsync("DELETE FROM LidarrArtistCache");
var artistCache = new List<LidarrArtistCache>();
foreach (var a in artists)
{
if (a.id > 0)
{
artistCache.Add(new LidarrArtistCache
{
ArtistId = a.id,
ArtistName = a.artistName,
ForeignArtistId = a.foreignArtistId,
Monitored = a.monitored
});
}
}
await _ctx.LidarrArtistCache.AddRangeAsync(artistCache);
await _ctx.SaveChangesAsync();
}
}
catch (Exception ex)
{
_logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Lidarr");
}
_job.Enqueue(() => _albumSync.CacheContent());
}
}
catch (Exception)
{
_logger.LogInformation(LoggingEvents.LidarrArtistCache, "Lidarr is not setup, cannot cache Artist");
}
}
public async Task<IEnumerable<LidarrArtistCache>> GetCachedContent()
{
return await _ctx.LidarrArtistCache.ToListAsync();
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_ctx?.Dispose();
_lidarrSettings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Lidarr
{
public class LidarrAvailabilityChecker : ILidarrAvailabilityChecker
{
public LidarrAvailabilityChecker(IMusicRequestRepository requests, IRepository<LidarrAlbumCache> albums, ILogger<LidarrAvailabilityChecker> log,
IBackgroundJobClient job, INotificationService notification)
{
_cachedAlbums = albums;
_requestRepository = requests;
_logger = log;
_job = job;
_notificationService = notification;
}
private readonly IMusicRequestRepository _requestRepository;
private readonly IRepository<LidarrAlbumCache> _cachedAlbums;
private readonly ILogger _logger;
private readonly IBackgroundJobClient _job;
private readonly INotificationService _notificationService;
public async Task Start()
{
var allAlbumRequests = _requestRepository.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available);
var albumsToUpdate = new List<AlbumRequest>();
foreach (var request in allAlbumRequests)
{
// Check if we have it cached
var cachedAlbum = await _cachedAlbums.FirstOrDefaultAsync(x => x.ForeignAlbumId.Equals(request.ForeignAlbumId));
if (cachedAlbum != null)
{
if (cachedAlbum.Monitored && cachedAlbum.FullyAvailable)
{
request.Available = true;
request.MarkedAsAvailable = DateTime.Now;
albumsToUpdate.Add(request);
}
}
}
foreach (var albumRequest in albumsToUpdate)
{
await _requestRepository.Update(albumRequest);
var recipient = albumRequest.RequestedUser.Email.HasValue() ? albumRequest.RequestedUser.Email : string.Empty;
_logger.LogDebug("AlbumId: {0}, RequestUser: {1}", albumRequest.Id, recipient);
_job.Enqueue(() => _notificationService.Publish(new NotificationOptions
{
DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable,
RequestId = albumRequest.Id,
RequestType = RequestType.Album,
Recipient = recipient,
}));
}
}
}
}

@ -1,4 +1,5 @@
using System.Text;
using Ombi.Helpers;
namespace Ombi.Schedule.Jobs.Ombi
{
@ -22,13 +23,20 @@ namespace Ombi.Schedule.Jobs.Ombi
protected virtual void AddMediaServerUrl(StringBuilder sb, string mediaurl, string url)
{
sb.Append("<tr>");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", mediaurl);
sb.AppendFormat("<img class=\"poster-overlay\" src=\"{0}\" width=\"150\" height=\"225\" style=\"border: none;-ms-interpolation-mode: bicubic; max-width: 100%;display: block; visibility: hidden; \">", url);
sb.Append("</a>");
sb.Append("</td>");
sb.Append("</tr>");
if (url.HasValue())
{
sb.Append("<tr>");
sb.Append(
"<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", mediaurl);
sb.AppendFormat(
"<img class=\"poster-overlay\" src=\"{0}\" width=\"150\" height=\"225\" style=\"border: none;-ms-interpolation-mode: bicubic; max-width: 100%;display: block; visibility: hidden; \">",
url);
sb.Append("</a>");
sb.Append("</td>");
sb.Append("</tr>");
}
sb.Append("</table>");
sb.Append("</td>");
}
@ -44,9 +52,9 @@ namespace Ombi.Schedule.Jobs.Ombi
{
sb.Append("<tr>");
sb.Append("<td class=\"title\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.9rem; vertical-align: top; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; line-height: 1.2rem; padding: 5px; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", url);
if(url.HasValue()) sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", url);
sb.AppendFormat("<h1 style=\"white-space: normal; line-height: 1;\" >{0}</h1>", title);
sb.Append("</a>");
if (url.HasValue()) sb.Append("</a>");
sb.Append("</td>");
sb.Append("</tr>");
}

@ -9,6 +9,8 @@ using MailKit;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
@ -18,6 +20,7 @@ using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -29,7 +32,8 @@ namespace Ombi.Schedule.Jobs.Ombi
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log)
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log,
ILidarrApi lidarrApi, IRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings)
{
_plex = plex;
_emby = emby;
@ -46,6 +50,10 @@ namespace Ombi.Schedule.Jobs.Ombi
_customizationSettings.ClearCache();
_newsletterSettings.ClearCache();
_log = log;
_lidarrApi = lidarrApi;
_lidarrAlbumRepository = albumCache;
_lidarrSettings = lidarrSettings;
_lidarrSettings.ClearCache();
}
private readonly IPlexContentRepository _plex;
@ -60,6 +68,9 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _userManager;
private readonly ILogger _log;
private readonly ILidarrApi _lidarrApi;
private readonly IRepository<LidarrAlbumCache> _lidarrAlbumRepository;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
public async Task Start(NewsletterSettings settings, bool test)
{
@ -87,21 +98,26 @@ namespace Ombi.Schedule.Jobs.Ombi
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking();
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId);
var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode);
var addedEmbyEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode);
// Filter out the ones that we haven't sent yet
var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && x.HasTheMovieDb && !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && x.HasTheMovieDb && !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
var lidarrContentAlbumsToSend = lidarrContent.Where(x => !addedAlbumLogIds.Contains(x.ForeignAlbumId)).ToHashSet();
_log.LogInformation("Plex Movies to send: {0}", plexContentMoviesToSend.Count());
_log.LogInformation("Emby Movies to send: {0}", embyContentMoviesToSend.Count());
_log.LogInformation("Albums to send: {0}", lidarrContentAlbumsToSend.Count());
var plexEpisodesToSend =
FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).Where(x => x.Series.HasTvDb).AsNoTracking(), addedPlexEpisodesLogIds);
@ -117,11 +133,12 @@ namespace Ombi.Schedule.Jobs.Ombi
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie ).OrderByDescending(x => x.AddedAt).Take(10);
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet();
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
body = await BuildHtml(plexm, embym, plext, embyt, settings);
var lidarr = lidarrContent.OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings);
}
else
{
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, settings);
body = await BuildHtml(plexContentMoviesToSend, embyContentMoviesToSend, plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings);
if (body.IsNullOrEmpty())
{
return;
@ -298,7 +315,8 @@ namespace Ombi.Schedule.Jobs.Ombi
return resolver.ParseMessage(template, curlys);
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, NewsletterSettings settings)
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend,
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings)
{
var sb = new StringBuilder();
@ -340,6 +358,24 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("</table>");
}
if (albums.Any() && !settings.DisableMusic)
{
sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Albums</h1><br /><br />");
sb.Append(
"<table class=\"movies-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
sb.Append("<tr>");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
sb.Append("<tr>");
await ProcessAlbums(albums, sb);
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("</table>");
}
return sb.ToString();
}
@ -382,6 +418,40 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
}
private async Task ProcessAlbums(HashSet<LidarrAlbumCache> albumsToSend, StringBuilder sb)
{
var settings = await _lidarrSettings.GetSettingsAsync();
int count = 0;
var ordered = albumsToSend.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
var info = await _lidarrApi.GetAlbumByForeignId(content.ForeignAlbumId, settings.ApiKey, settings.FullUri);
if (info == null)
{
continue;
}
try
{
CreateAlbumHtmlContent(sb, info);
count += 1;
}
catch (Exception e)
{
_log.LogError(e, "Error when Processing Lidarr Album {0}", info.title);
}
finally
{
EndLoopHtml(sb);
}
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
}
}
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
{
@ -467,6 +537,41 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private void CreateAlbumHtmlContent(StringBuilder sb, AlbumLookup info)
{
var cover = info.images
.FirstOrDefault(x => x.coverType.Equals("cover", StringComparison.InvariantCultureIgnoreCase))?.url;
if (cover.IsNullOrEmpty())
{
cover = info.remoteCover;
}
AddBackgroundInsideTable(sb, cover);
var disk = info.images
.FirstOrDefault(x => x.coverType.Equals("disc", StringComparison.InvariantCultureIgnoreCase))?.url;
if (disk.IsNullOrEmpty())
{
disk = info.remoteCover;
}
AddPosterInsideTable(sb, disk);
AddMediaServerUrl(sb, string.Empty, string.Empty);
AddInfoTable(sb);
var releaseDate = $"({info.releaseDate.Year})";
AddTitle(sb, string.Empty, $"{info.title} {releaseDate}");
var summary = info.artist?.artistName ?? string.Empty;
if (summary.Length > 280)
{
summary = summary.Remove(280);
summary = summary + "...</p>";
}
AddParagraph(sb, summary);
AddGenres(sb, $"Type: {info.albumType}");
}
private async Task ProcessPlexTv(HashSet<PlexEpisode> plexContent, StringBuilder sb)
{
var series = new List<PlexServerContent>();

@ -27,6 +27,7 @@
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" />

@ -0,0 +1,16 @@
using Ombi.Core.Settings.Models.External;
namespace Ombi.Settings.Settings.Models.External
{
public class LidarrSettings : ExternalSettings
{
public bool Enabled { get; set; }
public string ApiKey { get; set; }
public string DefaultQualityProfile { get; set; }
public string DefaultRootPath { get; set; }
public bool AlbumFolder { get; set; }
public int LanguageProfileId { get; set; }
public int MetadataProfileId { get; set; }
public bool AddOnly { get; set; }
}
}

@ -13,5 +13,6 @@
public string SickRageSync { get; set; }
public string RefreshMetadata { get; set; }
public string Newsletter { get; set; }
public string LidarrArtistSync { get; set; }
}
}

@ -52,6 +52,10 @@ namespace Ombi.Settings.Settings.Models
{
return Get(s.RefreshMetadata, Cron.DayInterval(3));
}
public static string LidarrArtistSync(JobSettings s)
{
return Get(s.LidarrArtistSync, Cron.Hourly(40));
}
private static string Get(string settings, string defaultCron)
{

@ -6,6 +6,7 @@ namespace Ombi.Settings.Settings.Models.Notifications
{
public bool DisableTv { get; set; }
public bool DisableMovies { get; set; }
public bool DisableMusic { get; set; }
public bool Enabled { get; set; }
public List<string> ExternalEmails { get; set; } = new List<string>();
}

@ -8,5 +8,7 @@ namespace Ombi.Settings.Settings.Models.Notifications
public bool Enabled { get; set; }
public string AccessToken { get; set; }
public string UserToken { get; set; }
public sbyte Priority { get; set; } = 0;
public string Sound { get; set; } = "pushover";
}
}

@ -28,6 +28,7 @@ namespace Ombi.Store.Context
void Seed();
DbSet<Audit> Audit { get; set; }
DbSet<MovieRequests> MovieRequests { get; set; }
DbSet<AlbumRequest> AlbumRequests { get; set; }
DbSet<TvRequests> TvRequests { get; set; }
DbSet<ChildRequests> ChildRequests { get; set; }
DbSet<Issues> Issues { get; set; }
@ -39,6 +40,8 @@ namespace Ombi.Store.Context
EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
DbSet<CouchPotatoCache> CouchPotatoCache { get; set; }
DbSet<SickRageCache> SickRageCache { get; set; }
DbSet<LidarrArtistCache> LidarrArtistCache { get; set; }
DbSet<LidarrAlbumCache> LidarrAlbumCache { get; set; }
DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
DbSet<RequestLog> RequestLogs { get; set; }
DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }

@ -31,6 +31,7 @@ namespace Ombi.Store.Context
public DbSet<EmbyEpisode> EmbyEpisode { get; set; }
public DbSet<MovieRequests> MovieRequests { get; set; }
public DbSet<AlbumRequest> AlbumRequests { get; set; }
public DbSet<TvRequests> TvRequests { get; set; }
public DbSet<ChildRequests> ChildRequests { get; set; }
@ -44,11 +45,14 @@ namespace Ombi.Store.Context
public DbSet<Audit> Audit { get; set; }
public DbSet<Tokens> Tokens { get; set; }
public DbSet<SonarrCache> SonarrCache { get; set; }
public DbSet<LidarrArtistCache> LidarrArtistCache { get; set; }
public DbSet<LidarrAlbumCache> LidarrAlbumCache { get; set; }
public DbSet<SonarrEpisodeCache> SonarrEpisodeCache { get; set; }
public DbSet<SickRageCache> SickRageCache { get; set; }
public DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
public DbSet<RequestSubscription> RequestSubscription { get; set; }
public DbSet<UserNotificationPreferences> UserNotificationPreferences { get; set; }
public DbSet<UserQualityProfiles> UserQualityProfileses { get; set; }
public DbSet<ApplicationConfiguration> ApplicationConfigurations { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
@ -117,8 +121,8 @@ namespace Ombi.Store.Context
Database.ExecuteSqlCommand("VACUUM;");
// Make sure we have the roles
var roles = Roles.Where(x => x.Name == OmbiRoles.ReceivesNewsletter);
if (!roles.Any())
var newsletterRole = Roles.Where(x => x.Name == OmbiRoles.ReceivesNewsletter);
if (!newsletterRole.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.ReceivesNewsletter)
{
@ -126,6 +130,19 @@ namespace Ombi.Store.Context
});
SaveChanges();
}
var requestMusicRole = Roles.Where(x => x.Name == OmbiRoles.RequestMusic);
if (!requestMusicRole.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.RequestMusic)
{
NormalizedName = OmbiRoles.RequestMusic.ToUpper()
});
Roles.Add(new IdentityRole(OmbiRoles.AutoApproveMusic)
{
NormalizedName = OmbiRoles.AutoApproveMusic.ToUpper()
});
SaveChanges();
}
// Make sure we have the API User
var apiUserExists = Users.Any(x => x.UserName.Equals("Api", StringComparison.CurrentCultureIgnoreCase));

@ -0,0 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("LidarrAlbumCache")]
public class LidarrAlbumCache : Entity
{
public int ArtistId { get; set; }
public string ForeignAlbumId { get; set; }
public int TrackCount { get; set; }
public DateTime ReleaseDate { get; set; }
public bool Monitored { get; set; }
public string Title { get; set; }
public decimal PercentOfTracks { get; set; }
public DateTime AddedAt { get; set; }
[NotMapped]
public bool PartiallyAvailable => PercentOfTracks != 100 && PercentOfTracks > 0;
[NotMapped]
public bool FullyAvailable => PercentOfTracks == 100;
}
}

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

Loading…
Cancel
Save