Merge branch 'feature/music-rework' into merge-music

pull/4447/head
Jamie 2 years ago committed by GitHub
commit 296e739b0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ namespace Ombi.Api.Lidarr
{
Task<List<AlbumLookup>> AlbumLookup(string searchTerm, string apiKey, string baseUrl);
Task<List<ArtistLookup>> ArtistLookup(string searchTerm, string apiKey, string baseUrl);
Task<List<Search>> Search(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);

@ -36,6 +36,15 @@ namespace Ombi.Api.Lidarr
return _api.Request<List<LidarrRootFolder>>(request);
}
public async Task<List<Search>> Search(string searchTerm, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/search", baseUrl, HttpMethod.Get);
request.AddQueryString("term", searchTerm);
AddHeaders(request, apiKey);
return await _api.Request<List<Search>>(request);
}
public async Task<List<ArtistLookup>> ArtistLookup(string searchTerm, string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/Artist/lookup", baseUrl, HttpMethod.Get);
@ -83,7 +92,7 @@ namespace Ombi.Api.Lidarr
public Task<AlbumByArtistResponse> GetAlbumsByArtist(string foreignArtistId)
{
var request = new Request(string.Empty, $"https://api.lidarr.audio/api/v0.4/artist/{foreignArtistId}",
var request = new Request(string.Empty, $"https://api.lidarr.audio/api/v1/artist/{foreignArtistId}",
HttpMethod.Get) {IgnoreBaseUrlAppend = true};
return _api.Request<AlbumByArtistResponse>(request);
}

@ -8,6 +8,7 @@ namespace Ombi.Api.Lidarr.Models
public string title { get; set; }
public string status { get; set; }
public string artistType { get; set; }
public string overview { get; set; }
public string disambiguation { get; set; }
public List<LidarrLinks> links { get; set; }
public int artistId { get; set; }

@ -32,21 +32,9 @@ namespace Ombi.Api.Lidarr.Models
public class Addoptions
{
public MonitorTypes monitor { get; set; }
public string monitor { get; set; }
public bool monitored { get; set; }
public bool searchForMissingAlbums { get; set; } // Only for Artists add
public string[] AlbumsToMonitor { get; set; } // Uses the MusicBrainzAlbumId!
}
public enum MonitorTypes
{
All,
Future,
Missing,
Existing,
Latest,
First,
None,
Unknown
}
}

@ -0,0 +1,13 @@
using System;
using System.Net.Mime;
namespace Ombi.Api.Lidarr.Models
{
public class Search
{
public ArtistLookup artist { get; set; }
public AlbumLookup album { get; set; }
public string foreignId { get; set; }
public string id { get; set; }
}
}

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

@ -5,12 +5,10 @@ using System.Threading.Tasks;
using System.Linq;
using System.Threading;
using AutoFixture;
using Hqub.MusicBrainz.API.Entities;
using Moq;
using NUnit.Framework;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.MusicBrainz;
using Ombi.Core.Engine.V2;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search.V2.Music;
@ -22,7 +20,6 @@ using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Test.Common;
using Artist = Hqub.MusicBrainz.API.Entities.Artist;
namespace Ombi.Core.Tests.Engine.V2
{
@ -32,7 +29,6 @@ namespace Ombi.Core.Tests.Engine.V2
private MusicSearchEngineV2 _engine;
private Mock<IMusicBrainzApi> _musicApi;
private Mock<ILidarrApi> _lidarrApi;
private Mock<ISettingsService<LidarrSettings>> _lidarrSettings;
private Fixture F;
@ -52,118 +48,14 @@ namespace Ombi.Core.Tests.Engine.V2
var cache = new Mock<ICacheService>();
var ombiSettings = new Mock<ISettingsService<OmbiSettings>>();
var requestSub = new Mock<IRepository<RequestSubscription>>();
_musicApi = new Mock<IMusicBrainzApi>();
_lidarrSettings = new Mock<ISettingsService<LidarrSettings>>();
_lidarrApi = new Mock<ILidarrApi>();
_lidarrSettings.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new LidarrSettings());
_engine = new MusicSearchEngineV2(principle.Object, requestService.Object, ruleEval.Object,
um.Object, cache.Object, ombiSettings.Object, requestSub.Object, _musicApi.Object,
um.Object, cache.Object, ombiSettings.Object, requestSub.Object,
_lidarrSettings.Object, _lidarrApi.Object);
}
[Test]
public async Task GetBasicArtistInformation_SingleArtist_Test()
{
_musicApi.Setup(x => x.GetArtistInformation("pretend-artist-id")).ReturnsAsync(F.Create<Artist>());
var result = await _engine.GetArtistInformation("pretend-artist-id");
Assert.That(result, Is.Not.Null);
Assert.That(result.ReleaseGroups.Any(), Is.True, "Release Groups are null");
Assert.That(result.Members.Any(), Is.False, "Members somehow populated?");
}
[Test]
public async Task GetBasicArtistInformation_Group_Test()
{
var musicReturnVal = F.Build<Artist>().With(x => x.Relations, new List<Relation>
{
new Relation
{
TypeId = RelationLinks.BandMember,
Artist = new Artist
{
Name = "Mr Artist"
},
Attributes = new []{"a nobody"},
Begin = "1992",
End = "2019",
Ended = true
},
new Relation
{
TypeId = RelationLinks.BandMember,
Artist = new Artist
{
Name = "Mr Artist2"
},
Attributes = new []{"a nobody2"},
Begin = "1993",
}
});
_musicApi.Setup(x => x.GetArtistInformation("pretend-artist-id")).ReturnsAsync(musicReturnVal.Create());
var result = await _engine.GetArtistInformation("pretend-artist-id");
Assert.That(result, Is.Not.Null);
Assert.That(result.ReleaseGroups.Any(), Is.True, "Release Groups are null");
Assert.That(result.Members.Any(), Is.True, "Members IS NULL!");
Assert.That(result.Members.FirstOrDefault(x => x.Name == "Mr Artist").End, Is.EqualTo("2019"));
Assert.That(result.Members.FirstOrDefault(x => x.Name == "Mr Artist").Attributes.Length, Is.EqualTo(1));
Assert.That(result.Members.FirstOrDefault(x => x.Name == "Mr Artist").IsCurrentMember, Is.EqualTo(false));
Assert.That(result.Members.FirstOrDefault(x => x.Name == "Mr Artist").Start, Is.EqualTo("1992"));
Assert.That(result.Members.FirstOrDefault(x => x.Name == "Mr Artist2").IsCurrentMember, Is.EqualTo(true));
}
[TestCaseSource(nameof(LinksData))]
public async Task<string> GetBasicArtistInformation_Links_Test(string url, string typeId, Func<ArtistInformation, string> func)
{
var musicReturnVal = F.Build<Artist>().With(x => x.Relations, new List<Relation>
{
new Relation
{
TypeId = typeId,
Url = new Url
{
Resource = url
}
},
});
_musicApi.Setup(x => x.GetArtistInformation("pretend-artist-id")).ReturnsAsync(musicReturnVal.Create());
var result = await _engine.GetArtistInformation("pretend-artist-id");
Assert.That(result, Is.Not.Null);
return func(result);
}
private static IEnumerable<TestCaseData> LinksData
{
get
{
yield return new TestCaseData("twitter.com", RelationLinks.SocialNetwork, new Func<ArtistInformation, string>(artist => artist.Links.Twitter)).Returns("twitter.com").SetName("ArtistInformation_Links_Twitter");
yield return new TestCaseData("allmusic", RelationLinks.AllMusic, new Func<ArtistInformation, string>(artist => artist.Links.AllMusic)).Returns("allmusic").SetName("ArtistInformation_Links_AllMusic");
yield return new TestCaseData("bbcmusic", RelationLinks.BbcMusic, new Func<ArtistInformation, string>(artist => artist.Links.BbcMusic)).Returns("bbcmusic").SetName("ArtistInformation_Links_BbcMusic");
yield return new TestCaseData("discogs", RelationLinks.Discogs, new Func<ArtistInformation, string>(artist => artist.Links.Discogs)).Returns("discogs").SetName("ArtistInformation_Links_Discogs");
yield return new TestCaseData("homepage", RelationLinks.Homepage, new Func<ArtistInformation, string>(artist => artist.Links.HomePage)).Returns("homepage").SetName("ArtistInformation_Links_Homepage");
yield return new TestCaseData("imdb", RelationLinks.Imdb, new Func<ArtistInformation, string>(artist => artist.Links.Imdb)).Returns("imdb").SetName("ArtistInformation_Links_Imdb");
yield return new TestCaseData("lastfm", RelationLinks.LastFm, new Func<ArtistInformation, string>(artist => artist.Links.LastFm)).Returns("lastfm").SetName("ArtistInformation_Links_LastFm");
yield return new TestCaseData("myspace", RelationLinks.MySpace, new Func<ArtistInformation, string>(artist => artist.Links.MySpace)).Returns("myspace").SetName("ArtistInformation_Links_MySpace");
yield return new TestCaseData("onlinecommunity", RelationLinks.OnlineCommunity, new Func<ArtistInformation, string>(artist => artist.Links.OnlineCommunity)).Returns("onlinecommunity").SetName("ArtistInformation_Links_OnlineCommunity");
yield return new TestCaseData("www.facebook.com", RelationLinks.SocialNetwork, new Func<ArtistInformation, string>(artist => artist.Links.Facebook)).Returns("www.facebook.com").SetName("ArtistInformation_Links_Facebook");
yield return new TestCaseData("www.instagram.com", RelationLinks.SocialNetwork, new Func<ArtistInformation, string>(artist => artist.Links.Instagram)).Returns("www.instagram.com").SetName("ArtistInformation_Links_insta");
yield return new TestCaseData("www.vk.com", RelationLinks.SocialNetwork, new Func<ArtistInformation, string>(artist => artist.Links.Vk)).Returns("www.vk.com").SetName("ArtistInformation_Links_vk");
yield return new TestCaseData("app.spotify.com", RelationLinks.Streams, new Func<ArtistInformation, string>(artist => artist.Links.Spotify)).Returns("app.spotify.com").SetName("ArtistInformation_Links_Spotify");
yield return new TestCaseData("deezer.com", RelationLinks.Streams, new Func<ArtistInformation, string>(artist => artist.Links.Deezer)).Returns("deezer.com").SetName("ArtistInformation_Links_Deezer");
yield return new TestCaseData("play.google.com", RelationLinks.Download, new Func<ArtistInformation, string>(artist => artist.Links.Google)).Returns("play.google.com").SetName("ArtistInformation_Links_Google");
yield return new TestCaseData("itunes.apple.com", RelationLinks.Download, new Func<ArtistInformation, string>(artist => artist.Links.Apple)).Returns("itunes.apple.com").SetName("ArtistInformation_Links_Apple");
}
}
[Test]
public async Task GetArtistInformation_WithPosters()
{
@ -200,7 +92,6 @@ namespace Ombi.Core.Tests.Engine.V2
},
}
});
_musicApi.Setup(x => x.GetArtistInformation("pretend-artist-id")).ReturnsAsync(F.Create<Artist>());
var result = await _engine.GetArtistInformation("pretend-artist-id");

@ -10,19 +10,24 @@ namespace Ombi.Core.Engine
{
public interface IMusicRequestEngine
{
Task<RequestEngineResult>ApproveAlbum(AlbumRequest request);
Task<RequestEngineResult>ApproveAlbum(MusicRequests request);
Task<RequestEngineResult> ApproveAlbumById(int requestId);
Task<RequestEngineResult> DenyAlbumById(int modelId, string reason);
Task<IEnumerable<AlbumRequest>> GetRequests();
Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, OrderFilterModel orderFilter);
Task<IEnumerable<MusicRequests>> GetRequests();
Task<RequestsViewModel<MusicRequests>> GetRequests(int count, int position, OrderFilterModel orderFilter);
Task<int> GetTotal();
Task<RequestEngineResult> MarkAvailable(int modelId);
Task<RequestEngineResult> MarkUnavailable(int modelId);
Task<RequestEngineResult> RemoveAlbumRequest(int requestId);
Task<RequestEngineResult> RequestAlbum(MusicAlbumRequestViewModel model);
Task<IEnumerable<AlbumRequest>> SearchAlbumRequest(string search);
Task<RequestEngineResult> RequestArtist(MusicArtistRequestViewModel model);
Task<IEnumerable<MusicRequests>> SearchAlbumRequest(string search);
Task<bool> UserHasRequest(string userId);
Task<RequestsViewModel<AlbumRequest>> GetRequestsByStatus(int count, int position, string sort, string sortOrder, RequestStatus available);
Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, string sort, string sortOrder);
Task<RequestQuotaCountModel> GetRemainingRequests(OmbiUser user = null);
Task<RequestsViewModel<MusicRequests>> GetRequestsByStatus(int count, int position, string sort, string sortOrder, RequestStatus available);
Task<RequestsViewModel<MusicRequests>> GetRequests(int count, int position, string sort, string sortOrder);
}
}

@ -9,6 +9,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<ArtistInformation> GetArtistInformation(string artistId);
Task<ArtistInformation> GetArtistInformationByRequestId(int requestId);
Task<AlbumArt> GetReleaseGroupArt(string musicBrainzId, CancellationToken token);
Task<ReleaseGroup> GetAlbum(string albumId);
Task<AlbumInformation> GetAlbumInformation(string albumId);
}
}

@ -73,7 +73,7 @@ namespace Ombi.Core.Engine
var userDetails = await GetUser();
var requestModel = new AlbumRequest
var requestModel = new MusicRequests
{
ForeignAlbumId = model.ForeignAlbumId,
ArtistName = album.artist?.artistName,
@ -85,8 +85,10 @@ namespace Ombi.Core.Engine
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, // This needs to be populated to send to Lidarr for new requests
RequestedByAlias = model.RequestedByAlias
ForeignArtistId = album.artist?.foreignArtistId,
RequestedByAlias = model.RequestedByAlias,
Monitor = model.Monitor,
SearchForMissingAlbums = model.SearchForMissingAlbums
};
if (requestModel.Cover.IsNullOrEmpty())
{
@ -128,6 +130,130 @@ namespace Ombi.Core.Engine
return await AddAlbumRequest(requestModel);
}
public async Task<RequestEngineResult> RequestArtist(MusicArtistRequestViewModel model)
{
var s = await _lidarrSettings.GetSettingsAsync();
var artist = await _lidarrApi.GetArtistByForeignId(model.ForeignArtistId, s.ApiKey, s.FullUri);
var userDetails = await GetUser();
var requestModel = new MusicRequests
{
ForeignArtistId = model.ForeignArtistId,
Monitored = model.Monitored,
Title = artist.artistName,
ArtistName = artist.artistName,
RequestedDate = DateTime.Now,
RequestType = RequestType.Artist,
RequestedUserId = userDetails.Id,
Monitor = model.Monitor,
SearchForMissingAlbums = model.SearchForMissingAlbums
};
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 AddArtistRequest(requestModel);
if (requestEngineResult.Result)
{
var result = await ApproveArtist(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 AddArtistRequest(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<MusicRequests>> GetRequests(int count, int position,
OrderFilterModel orderFilter)
{
var shouldHide = await HideFromOtherUsers();
IQueryable<MusicRequests> 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<MusicRequests>
{
Collection = requests,
Total = total
};
}
/// <summary>
/// Gets the requests.
/// </summary>
@ -135,11 +261,11 @@ namespace Ombi.Core.Engine
/// <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,
public async Task<RequestsViewModel<MusicRequests>> GetRequestArtist(int count, int position,
OrderFilterModel orderFilter)
{
var shouldHide = await HideFromOtherUsers();
IQueryable<AlbumRequest> allRequests;
IQueryable<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests =
@ -193,14 +319,14 @@ namespace Ombi.Core.Engine
{
await CheckForSubscription(shouldHide, x);
});
return new RequestsViewModel<AlbumRequest>
return new RequestsViewModel<MusicRequests>
{
Collection = requests,
Total = total
};
}
private IQueryable<AlbumRequest> OrderAlbums(IQueryable<AlbumRequest> allRequests, OrderType type)
private IQueryable<MusicRequests> OrderAlbums(IQueryable<MusicRequests> allRequests, OrderType type)
{
switch (type)
{
@ -234,10 +360,10 @@ namespace Ombi.Core.Engine
/// Gets the requests.
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<AlbumRequest>> GetRequests()
public async Task<IEnumerable<MusicRequests>> GetRequests()
{
var shouldHide = await HideFromOtherUsers();
List<AlbumRequest> allRequests;
List<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MusicRepository.GetWithUser(shouldHide.UserId).ToListAsync();
@ -254,8 +380,28 @@ namespace Ombi.Core.Engine
return allRequests;
}
public async Task<IEnumerable<MusicRequests>> GetRequestsArtist()
{
var shouldHide = await HideFromOtherUsers();
List<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MusicRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MusicRepository.GetWithUser().ToListAsync();
}
allRequests.ForEach(async x =>
{
await CheckForArtistSubscription(shouldHide, x);
});
return allRequests;
}
private async Task CheckForSubscription(HideResult shouldHide, List<AlbumRequest> albumRequests)
private async Task CheckForSubscription(HideResult shouldHide, List<MusicRequests> albumRequests)
{
var requestIds = albumRequests.Select(x => x.Id);
var sub = await _subscriptionRepository.GetAll().Where(s =>
@ -276,7 +422,22 @@ namespace Ombi.Core.Engine
}
}
private async Task CheckForSubscription(HideResult shouldHide, AlbumRequest x)
private async Task CheckForArtistSubscription(HideResult shouldHide, MusicRequests artistRequest)
{
if (shouldHide.UserId == artistRequest.RequestedUserId)
{
artistRequest.ShowSubscribe = false;
}
else
{
artistRequest.ShowSubscribe = true;
var sub = await _subscriptionRepository.GetAll().FirstOrDefaultAsync(s =>
s.UserId == shouldHide.UserId && s.RequestId == artistRequest.Id && s.RequestType == RequestType.Artist);
artistRequest.Subscribed = sub != null;
}
}
private async Task CheckForSubscription(HideResult shouldHide, MusicRequests x)
{
if (shouldHide.UserId == x.RequestedUserId)
{
@ -296,10 +457,10 @@ namespace Ombi.Core.Engine
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<AlbumRequest>> SearchAlbumRequest(string search)
public async Task<IEnumerable<MusicRequests>> SearchAlbumRequest(string search)
{
var shouldHide = await HideFromOtherUsers();
List<AlbumRequest> allRequests;
List<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MusicRepository.GetWithUser(shouldHide.UserId).ToListAsync();
@ -346,7 +507,60 @@ namespace Ombi.Core.Engine
};
}
public async Task<RequestEngineResult> ApproveAlbum(AlbumRequest request)
public async Task<RequestEngineResult> ApproveAlbum(MusicRequests 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, string.Empty);
if (canNotify.Success)
{
await NotificationHelper.Notify(request, NotificationType.RequestApproved);
}
if (request.Approved)
{
var result = await _musicSender.SendAlbum(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
};
}
public async Task<RequestEngineResult> ApproveArtist(MusicRequests request)
{
if (request == null)
{
@ -370,7 +584,7 @@ namespace Ombi.Core.Engine
if (request.Approved)
{
var result = await _musicSender.Send(request);
var result = await _musicSender.SendArtist(request);
if (result.Success && result.Sent)
{
return new RequestEngineResult
@ -469,7 +683,7 @@ namespace Ombi.Core.Engine
};
}
private async Task<RequestEngineResult> AddAlbumRequest(AlbumRequest model)
private async Task<RequestEngineResult> AddAlbumRequest(MusicRequests model)
{
await MusicRepository.Add(model);
@ -490,10 +704,32 @@ namespace Ombi.Core.Engine
return new RequestEngineResult { Result = true, Message = $"{model.Title} has been successfully added!", RequestId = model.Id };
}
public async Task<RequestsViewModel<AlbumRequest>> GetRequestsByStatus(int count, int position, string sortProperty, string sortOrder, RequestStatus status)
private async Task<RequestEngineResult> AddArtistRequest(MusicRequests model)
{
// Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(MusicRepository));
await MusicRepository.Add(model);
var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, string.Empty);
if (result.Success)
{
await NotificationHelper.NewRequest(model);
}
await _requestLog.Add(new RequestLog
{
UserId = (await GetUser()).Id,
RequestDate = DateTime.UtcNow,
RequestId = model.Id,
RequestType = RequestType.Artist,
});
return new RequestEngineResult { Result = true, Message = $"{model.Title} has been successfully added!", RequestId = model.Id };
}
public async Task<RequestsViewModel<MusicRequests>> GetRequestsByStatus(int count, int position, string sortProperty, string sortOrder, RequestStatus status)
{
var shouldHide = await HideFromOtherUsers();
IQueryable<AlbumRequest> allRequests;
IQueryable<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests =
@ -525,12 +761,12 @@ namespace Ombi.Core.Engine
break;
}
var prop = TypeDescriptor.GetProperties(typeof(AlbumRequest)).Find(sortProperty, true);
var prop = TypeDescriptor.GetProperties(typeof(MusicRequests)).Find(sortProperty, true);
if (sortProperty.Contains('.'))
{
// This is a navigation property currently not supported
prop = TypeDescriptor.GetProperties(typeof(AlbumRequest)).Find("RequestedDate", true);
prop = TypeDescriptor.GetProperties(typeof(MusicRequests)).Find("RequestedDate", true);
//var properties = sortProperty.Split(new []{'.'}, StringSplitOptions.RemoveEmptyEntries);
//var firstProp = TypeDescriptor.GetProperties(typeof(MovieRequests)).Find(properties[0], true);
//var propType = firstProp.PropertyType;
@ -545,17 +781,17 @@ namespace Ombi.Core.Engine
requests = requests.Skip(position).Take(count).ToList();
await CheckForSubscription(shouldHide, requests);
return new RequestsViewModel<AlbumRequest>
return new RequestsViewModel<MusicRequests>
{
Collection = requests,
Total = total
};
}
public async Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, string sortProperty, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder)
{
var shouldHide = await HideFromOtherUsers();
IQueryable<AlbumRequest> allRequests;
IQueryable<MusicRequests> allRequests;
if (shouldHide.Hide)
{
allRequests =
@ -589,7 +825,7 @@ namespace Ombi.Core.Engine
requests = requests.Skip(position).Take(count).ToList();
await CheckForSubscription(shouldHide, requests);
return new RequestsViewModel<AlbumRequest>
return new RequestsViewModel<MusicRequests>
{
Collection = requests,
Total = total

@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Api.MusicBrainz;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.Lidarr;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search.V2;
@ -23,17 +23,18 @@ namespace Ombi.Core.Engine.V2
{
public MultiSearchEngine(IPrincipal identity, IRequestServiceMain requestService, IRuleEvaluator rules,
OmbiUserManager um, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub,
IMovieDbApi movieDbApi, ISettingsService<LidarrSettings> lidarrSettings, IMusicBrainzApi musicApi)
IMovieDbApi movieDbApi, ISettingsService<LidarrSettings> lidarrSettings, ILidarrApi lidarrApi)
: base(identity, requestService, rules, um, cache, ombiSettings, sub)
{
_movieDbApi = movieDbApi;
_lidarrSettings = lidarrSettings;
_musicApi = musicApi;
_lidarrApi = lidarrApi;
}
private readonly IMovieDbApi _movieDbApi;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly IMusicBrainzApi _musicApi;
private readonly ILidarrApi _lidarrApi;
private bool _demo = DemoSingleton.Instance.Demo;
@ -43,72 +44,92 @@ namespace Ombi.Core.Engine.V2
var lang = await DefaultLanguageCode(null);
var model = new List<MultiSearchResult>();
var movieDbData = (await _movieDbApi.MultiSearch(searchTerm, lang, cancellationToken)).results;
var lidarrSettings = await _lidarrSettings.GetSettingsAsync();
if (lidarrSettings.Enabled && filter.Music)
{
var artistResult = await _musicApi.SearchArtist(searchTerm);
foreach (var artist in artistResult)
var lidarSearchResult = await _lidarrApi.Search(searchTerm, lidarrSettings.ApiKey, lidarrSettings.FullUri);
foreach (var search_result in lidarSearchResult)
{
model.Add(new MultiSearchResult
if (search_result.artist != null)
{
MediaType = "Artist",
Title = artist.Name,
Id = artist.Id
});
model.Add(new MultiSearchResult
{
MediaType = "Artist",
Title = search_result.artist.artistName,
Id = search_result.artist.foreignArtistId,
Poster = search_result.artist.remotePoster,
Monitored = search_result.artist.monitored
});
} else if (search_result.album != null)
{
model.Add(new MultiSearchResult
{
MediaType = "Album",
Title = search_result.album.title,
Id = search_result.album.foreignAlbumId,
Poster = search_result.album.remoteCover,
Monitored = search_result.album.monitored
});
}
}
}
foreach (var multiSearch in movieDbData)
if (filter.Movies || filter.TvShows)
{
var movieDbData = (await _movieDbApi.MultiSearch(searchTerm, lang, cancellationToken)).results;
if (DemoCheck(multiSearch.title) || DemoCheck(multiSearch.name))
foreach (var multiSearch in movieDbData)
{
continue;
}
var result = new MultiSearchResult
{
MediaType = multiSearch.media_type,
Poster = multiSearch.poster_path,
Overview = multiSearch.overview
};
if (multiSearch.media_type.Equals("movie", StringComparison.InvariantCultureIgnoreCase) && filter.Movies)
{
if (multiSearch.release_date.HasValue() && DateTime.TryParse(multiSearch.release_date, out var releaseDate))
if (DemoCheck(multiSearch.title) || DemoCheck(multiSearch.name))
{
result.Title = $"{multiSearch.title} ({releaseDate.Year})";
continue;
}
else
var result = new MultiSearchResult
{
MediaType = multiSearch.media_type,
Poster = multiSearch.poster_path,
Overview = multiSearch.overview
};
if (multiSearch.media_type.Equals("movie", StringComparison.InvariantCultureIgnoreCase) && filter.Movies)
{
result.Title = multiSearch.title;
if (multiSearch.release_date.HasValue() && DateTime.TryParse(multiSearch.release_date, out var releaseDate))
{
result.Title = $"{multiSearch.title} ({releaseDate.Year})";
}
else
{
result.Title = multiSearch.title;
}
}
}
else if (multiSearch.media_type.Equals("tv", StringComparison.InvariantCultureIgnoreCase) && filter.TvShows)
{
if (multiSearch.release_date.HasValue() && DateTime.TryParse(multiSearch.release_date, out var releaseDate))
else if (multiSearch.media_type.Equals("tv", StringComparison.InvariantCultureIgnoreCase) && filter.TvShows)
{
result.Title = $"{multiSearch.name} ({releaseDate.Year})";
if (multiSearch.release_date.HasValue() && DateTime.TryParse(multiSearch.release_date, out var releaseDate))
{
result.Title = $"{multiSearch.name} ({releaseDate.Year})";
}
else
{
result.Title = multiSearch.name;
}
}
else
else if (multiSearch.media_type.Equals("person", StringComparison.InvariantCultureIgnoreCase) && filter.People)
{
result.Title = multiSearch.name;
}
}
else if (multiSearch.media_type.Equals("person", StringComparison.InvariantCultureIgnoreCase) && filter.People)
{
result.Title = multiSearch.name;
}
else
{
continue;
}
else
{
continue;
}
result.Id = multiSearch.id.ToString();
model.Add(result);
result.Id = multiSearch.id.ToString();
model.Add(result);
}
}
return model;

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.MusicBrainz;
using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.Requests;
@ -20,98 +19,108 @@ using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Artist = Hqub.MusicBrainz.API.Entities.Artist;
using ReleaseGroup = Ombi.Core.Models.Search.V2.Music.ReleaseGroup;
namespace Ombi.Core.Engine.V2
{
public class MusicSearchEngineV2 : BaseMediaEngine, IMusicSearchEngineV2
{
private readonly IMusicBrainzApi _musicBrainzApi;
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ILidarrApi _lidarrApi;
public MusicSearchEngineV2(IPrincipal identity, IRequestServiceMain requestService, IRuleEvaluator rules,
OmbiUserManager um, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings,
IRepository<RequestSubscription> sub, IMusicBrainzApi musicBrainzApi, ISettingsService<LidarrSettings> lidarrSettings,
IRepository<RequestSubscription> sub, ISettingsService<LidarrSettings> lidarrSettings,
ILidarrApi lidarrApi)
: base(identity, requestService, rules, um, cache, ombiSettings, sub)
{
_musicBrainzApi = musicBrainzApi;
_lidarrSettings = lidarrSettings;
_lidarrApi = lidarrApi;
}
public async Task<ReleaseGroup> GetAlbum(string albumId)
{
var g = await _musicBrainzApi.GetAlbumInformation(albumId);
var release = new ReleaseGroup
var lidarrSettings = await GetLidarrSettings();
Task<AlbumLookup> lidarrAlbumTask = null;
var release = new ReleaseGroup{};
if (lidarrSettings.Enabled)
{
ReleaseType = g.ReleaseGroup.PrimaryType,
Id = g.Id,
Title = g.Title,
ReleaseDate = g.ReleaseGroup.FirstReleaseDate,
};
lidarrAlbumTask = _lidarrApi.GetAlbumByForeignId(albumId, lidarrSettings.ApiKey, lidarrSettings.FullUri);
var albumResult = await lidarrAlbumTask;
release = new ReleaseGroup
{
ReleaseType = albumResult.artistType,
Id = albumResult.artistId.ToString(),
Title = albumResult.title,
ReleaseDate = albumResult.releaseDate.ToString(),
};
await RunSearchRules(release);
await RunSearchRules(release);
}
return release;
}
public async Task<ArtistInformation> GetArtistInformation(string artistId)
{
var artist = await _musicBrainzApi.GetArtistInformation(artistId);
var lidarrSettings = await GetLidarrSettings();
Task<ArtistResult> lidarrArtistTask = null;
var info = new ArtistInformation { };
if (lidarrSettings.Enabled)
{
lidarrArtistTask = _lidarrApi.GetArtistByForeignId(artistId, lidarrSettings.ApiKey, lidarrSettings.FullUri);
}
info = new ArtistInformation { };
var info = new ArtistInformation
{
Id = artistId,
Name = artist.Name,
Country = artist.Country,
Region = artist.Area?.Name,
Type = artist.Type,
StartYear = artist.LifeSpan?.Begin ?? "",
EndYear = artist.LifeSpan?.End ?? "",
Disambiguation = artist.Disambiguation,
ReleaseGroups = new List<ReleaseGroup>(),
Members = new List<BandMember>()
};
foreach (var g in artist.ReleaseGroups)
{
var release = new ReleaseGroup
if (lidarrArtistTask != null)
{
ReleaseType = g.PrimaryType,
Id = g.Id,
Title = g.Title,
ReleaseDate = g.FirstReleaseDate,
};
await RunSearchRules(release);
info.ReleaseGroups.Add(release);
try
{
var artistResult = await lidarrArtistTask;
info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Overview = artistResult.overview;
info.Name = artistResult.artistName;
info.Monitored = artistResult.monitored;
}
catch (JsonSerializationException)
{
// swallow, Lidarr probably doesn't have this artist
}
}
}
info.Links = GetLinksForArtist(artist);
info.Members = GetBandMembers(artist);
if (lidarrArtistTask != null)
return info;
}
public async Task<AlbumInformation> GetAlbumInformation(string albumId)
{
var lidarrSettings = await GetLidarrSettings();
Task<AlbumLookup> lidarrAlbumTask = null;
var info = new AlbumInformation { };
if (lidarrSettings.Enabled)
{
try
{
var artistResult = await lidarrArtistTask;
info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Overview = artistResult.overview;
}
catch (JsonSerializationException)
lidarrAlbumTask = _lidarrApi.GetAlbumByForeignId(albumId, lidarrSettings.ApiKey, lidarrSettings.FullUri);
if (lidarrAlbumTask != null)
{
// swallow, Lidarr probably doesn't have this artist
try
{
var albumResult = await lidarrAlbumTask;
info.Cover = albumResult.images?.FirstOrDefault(x => x.coverType.Equals("cover", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl();
info.Title = albumResult.title;
info.Disambiguation = albumResult.disambiguation;
info.Overview = albumResult.overview;
info.Monitored = albumResult.monitored;
info.Id = albumResult.foreignAlbumId;
}
catch (JsonSerializationException)
{
// swallow, Lidarr probably doesn't have this album
}
}
}
@ -120,24 +129,24 @@ namespace Ombi.Core.Engine.V2
public async Task<AlbumArt> GetReleaseGroupArt(string musicBrainzId, CancellationToken token)
{
var art = await _musicBrainzApi.GetCoverArtForReleaseGroup(musicBrainzId, token);
// var art = await _musicBrainzApi.GetCoverArtForReleaseGroup(musicBrainzId, token);
if (art == null || !art.images.Any())
{
return new AlbumArt();
}
// if (art == null || !art.images.Any())
// {
// return new AlbumArt();
// }
foreach (var cover in art.images)
{
if ((cover.thumbnails?.small ?? string.Empty).HasValue())
{
return new AlbumArt(cover.thumbnails.small.ToHttpsUrl());
}
if ((cover.thumbnails?.large ?? string.Empty).HasValue())
{
return new AlbumArt(cover.thumbnails.large.ToHttpsUrl());
}
}
// foreach (var cover in art.images)
// {
// if ((cover.thumbnails?.small ?? string.Empty).HasValue())
// {
// return new AlbumArt(cover.thumbnails.small.ToHttpsUrl());
// }
// if ((cover.thumbnails?.large ?? string.Empty).HasValue())
// {
// return new AlbumArt(cover.thumbnails.large.ToHttpsUrl());
// }
// }
return new AlbumArt();
}
@ -151,18 +160,18 @@ namespace Ombi.Core.Engine.V2
private List<BandMember> GetBandMembers(Artist artist)
{
var members = new List<BandMember>();
var membersOfBand = artist.Relations.Where(x => x.TypeId == RelationLinks.BandMember);
foreach (var member in membersOfBand)
{
members.Add(new BandMember
{
Name = member.Artist?.Name,
Attributes = member.Attributes,
IsCurrentMember = member.Ended == null,
End = member.End,
Start = member.Begin
});
}
// var membersOfBand = artist.Relations.Where(x => x.TypeId == RelationLinks.BandMember);
// foreach (var member in membersOfBand)
// {
// members.Add(new BandMember
// {
// Name = member.Artist?.Name,
// Attributes = member.Attributes,
// IsCurrentMember = member.Ended == null,
// End = member.End,
// Start = member.Begin
// });
// }
return members;
}
@ -170,79 +179,79 @@ namespace Ombi.Core.Engine.V2
private ArtistLinks GetLinksForArtist(Artist artist)
{
var links = new ArtistLinks();
foreach (var relation in artist.Relations)
{
switch (relation.TypeId)
{
case RelationLinks.AllMusic:
links.AllMusic = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.BbcMusic:
links.BbcMusic = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.Discogs:
links.Discogs = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.Homepage:
links.HomePage = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.Imdb:
links.Imdb = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.LastFm:
links.LastFm = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.MySpace:
links.MySpace = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.OnlineCommunity:
links.OnlineCommunity = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.SocialNetwork:
if ((relation.Url?.Resource ?? string.Empty).Contains("twitter", CompareOptions.IgnoreCase))
{
links.Twitter = relation.Url?.Resource.ToHttpsUrl();
}
if ((relation.Url?.Resource ?? string.Empty).Contains("facebook", CompareOptions.IgnoreCase))
{
links.Facebook = relation.Url?.Resource.ToHttpsUrl();
}
if ((relation.Url?.Resource ?? string.Empty).Contains("instagram", CompareOptions.IgnoreCase))
{
links.Instagram = relation.Url?.Resource.ToHttpsUrl();
}
if ((relation.Url?.Resource ?? string.Empty).Contains("vk", CompareOptions.IgnoreCase))
{
links.Vk = relation.Url?.Resource.ToHttpsUrl();
}
break;
case RelationLinks.Streams:
if ((relation.Url?.Resource ?? string.Empty).Contains("spotify", CompareOptions.IgnoreCase))
{
links.Spotify = relation.Url?.Resource.ToHttpsUrl();
}
if ((relation.Url?.Resource ?? string.Empty).Contains("deezer", CompareOptions.IgnoreCase))
{
links.Deezer = relation.Url?.Resource.ToHttpsUrl();
}
// foreach (var relation in artist.Relations)
// {
// switch (relation.TypeId)
// {
// case RelationLinks.AllMusic:
// links.AllMusic = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.BbcMusic:
// links.BbcMusic = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.Discogs:
// links.Discogs = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.Homepage:
// links.HomePage = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.Imdb:
// links.Imdb = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.LastFm:
// links.LastFm = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.MySpace:
// links.MySpace = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.OnlineCommunity:
// links.OnlineCommunity = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.SocialNetwork:
// if ((relation.Url?.Resource ?? string.Empty).Contains("twitter", CompareOptions.IgnoreCase))
// {
// links.Twitter = relation.Url?.Resource.ToHttpsUrl();
// }
// if ((relation.Url?.Resource ?? string.Empty).Contains("facebook", CompareOptions.IgnoreCase))
// {
// links.Facebook = relation.Url?.Resource.ToHttpsUrl();
// }
// if ((relation.Url?.Resource ?? string.Empty).Contains("instagram", CompareOptions.IgnoreCase))
// {
// links.Instagram = relation.Url?.Resource.ToHttpsUrl();
// }
// if ((relation.Url?.Resource ?? string.Empty).Contains("vk", CompareOptions.IgnoreCase))
// {
// links.Vk = relation.Url?.Resource.ToHttpsUrl();
// }
// break;
// case RelationLinks.Streams:
// if ((relation.Url?.Resource ?? string.Empty).Contains("spotify", CompareOptions.IgnoreCase))
// {
// links.Spotify = relation.Url?.Resource.ToHttpsUrl();
// }
// if ((relation.Url?.Resource ?? string.Empty).Contains("deezer", CompareOptions.IgnoreCase))
// {
// links.Deezer = relation.Url?.Resource.ToHttpsUrl();
// }
break;
case RelationLinks.YouTube:
links.YouTube = relation.Url?.Resource.ToHttpsUrl();
break;
case RelationLinks.Download:
if ((relation.Url?.Resource ?? string.Empty).Contains("google", CompareOptions.IgnoreCase))
{
links.Google = relation.Url?.Resource.ToHttpsUrl();
}
if ((relation.Url?.Resource ?? string.Empty).Contains("apple", CompareOptions.IgnoreCase))
{
links.Apple = relation.Url?.Resource.ToHttpsUrl();
}
// break;
// case RelationLinks.YouTube:
// links.YouTube = relation.Url?.Resource.ToHttpsUrl();
// break;
// case RelationLinks.Download:
// if ((relation.Url?.Resource ?? string.Empty).Contains("google", CompareOptions.IgnoreCase))
// {
// links.Google = relation.Url?.Resource.ToHttpsUrl();
// }
// if ((relation.Url?.Resource ?? string.Empty).Contains("apple", CompareOptions.IgnoreCase))
// {
// links.Apple = relation.Url?.Resource.ToHttpsUrl();
// }
break;
}
}
// break;
// }
// }
return links;
}

@ -40,7 +40,7 @@ namespace Ombi.Core
});
}
public async Task NewRequest(AlbumRequest model)
public async Task NewRequest(MusicRequests model)
{
var notificationModel = new NotificationOptions
{
@ -55,7 +55,6 @@ namespace Ombi.Core
});
}
public async Task Notify(MovieRequests model, NotificationType type)
{
var notificationModel = new NotificationOptions
@ -88,7 +87,7 @@ namespace Ombi.Core
});
}
public async Task Notify(AlbumRequest model, NotificationType type)
public async Task Notify(MusicRequests model, NotificationType type)
{
var notificationModel = new NotificationOptions
{

@ -4,5 +4,16 @@
{
public string ForeignAlbumId { get; set; }
public string RequestedByAlias { get; set; }
public string Monitor { get; set; }
public bool SearchForMissingAlbums { get; set; }
}
public class MusicArtistRequestViewModel
{
public string ForeignArtistId { get; set; }
public bool Monitored { get; set; }
public string RequestedByAlias { get; set; }
public string Monitor { get; set; }
public bool SearchForMissingAlbums { get; set; }
}
}

@ -7,5 +7,6 @@
public string Title { get; set; }
public string Poster { get; set; }
public string Overview { get; set; }
public bool Monitored { get; set; }
}
}

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Ombi.Core.Models.Search.V2.Music
{
public class AlbumInformation
{
public string Title { get; set; }
public string Id { get; set; }
public string StartYear { get; set; }
public string EndYear { get; set; }
public bool IsEnded => string.IsNullOrEmpty(EndYear);
public bool Monitored { get; set; }
public string Type { get; set; }
public string Country { get; set; }
public string Region { get; set; }
public string Disambiguation { get; set; }
public string Cover { get; set; }
public string Overview { get; set; }
}
}

@ -18,6 +18,7 @@ namespace Ombi.Core.Models.Search.V2.Music
public string Poster { get; set; }
public string FanArt { get; set; }
public string Overview { get; set; }
public bool Monitored { get; set; }
public List<ReleaseGroup> ReleaseGroups { get; set; }
public ArtistLinks Links { get; set; }
public List<BandMember> Members { get; set; }

@ -28,7 +28,6 @@
<ProjectReference Include="..\Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj" />
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />

@ -5,6 +5,7 @@ namespace Ombi.Core.Senders
{
public interface IMusicSender
{
Task<SenderResult> Send(AlbumRequest model);
Task<SenderResult> SendAlbum(MusicRequests model);
Task<SenderResult> SendArtist(MusicRequests model);
}
}

@ -10,10 +10,10 @@ namespace Ombi.Core
{
Task NewRequest(FullBaseRequest model);
Task NewRequest(ChildRequests model);
Task NewRequest(AlbumRequest model);
Task NewRequest(MusicRequests model);
Task Notify(MovieRequests model, NotificationType type);
Task Notify(ChildRequests model, NotificationType type);
Task Notify(AlbumRequest model, NotificationType type);
Task Notify(MusicRequests model, NotificationType type);
Task Notify(NotificationOptions model);
}
}

@ -33,14 +33,14 @@ namespace Ombi.Core.Senders
private readonly IRepository<RequestQueue> _requestQueueRepository;
private readonly INotificationHelper _notificationHelper;
public async Task<SenderResult> Send(AlbumRequest model)
public async Task<SenderResult> SendAlbum(MusicRequests model)
{
try
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendToLidarr(model, settings);
return await SendAlbumToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
@ -73,7 +73,47 @@ namespace Ombi.Core.Senders
return new SenderResult { Success = false, Sent = false, Message = "Something went wrong!" };
}
private async Task<SenderResult> SendToLidarr(AlbumRequest model, LidarrSettings settings)
public async Task<SenderResult> SendArtist(MusicRequests model)
{
try
{
var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled)
{
return await SendArtistToLidarr(model, settings);
}
return new SenderResult { Success = false, Sent = false, Message = "Lidarr is not enabled" };
}
catch (Exception e)
{
_log.LogError(e, "Exception thrown when sending a music to DVR app, added to the request queue");
var existingQueue = await _requestQueueRepository.FirstOrDefaultAsync(x => x.RequestId == model.Id);
if (existingQueue != null)
{
existingQueue.RetryCount++;
existingQueue.Error = e.Message;
await _requestQueueRepository.SaveChangesAsync();
}
else
{
await _requestQueueRepository.Add(new RequestQueue
{
Dts = DateTime.UtcNow,
Error = e.Message,
RequestId = model.Id,
Type = RequestType.Album,
RetryCount = 0
});
await _notificationHelper.Notify(model, NotificationType.ItemAddedToFaultQueue);
}
}
return new SenderResult { Success = false, Sent = false, Message = "Something went wrong!" };
}
private async Task<SenderResult> SendAlbumToLidarr(MusicRequests model, LidarrSettings settings)
{
var qualityToUse = int.Parse(settings.DefaultQualityProfile);
//if (model.QualityOverride > 0)
@ -99,9 +139,9 @@ namespace Ombi.Core.Senders
foreignArtistId = model.ForeignArtistId,
addOptions = new Addoptions
{
monitored = true,
monitor = MonitorTypes.None,
searchForMissingAlbums = false,
monitored = model.Monitored,
monitor = model.Monitor,
searchForMissingAlbums = model.SearchForMissingAlbums,
AlbumsToMonitor = new[] {model.ForeignAlbumId}
},
added = DateTime.Now,
@ -160,7 +200,46 @@ namespace Ombi.Core.Senders
return new SenderResult { Success = false, Sent = false, Message = "Album is already monitored" };
}
private async Task<SenderResult> SetupAlbum(AlbumRequest model, ArtistResult artist, LidarrSettings settings)
private async Task<SenderResult> SendArtistToLidarr(MusicRequests 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)*/;
EnsureArg.IsNotNullOrEmpty(model.ForeignArtistId, nameof(model.ForeignArtistId));
EnsureArg.IsNotNullOrEmpty(rootFolderPath, nameof(rootFolderPath));
// Create artist
var newArtist = new ArtistAdd
{
foreignArtistId = model.ForeignArtistId,
addOptions = new Addoptions
{
monitor = model.Monitor,
searchForMissingAlbums = model.SearchForMissingAlbums
},
artistName = model.ArtistName,
added = DateTime.Now,
monitored = model.Monitored,
metadataProfileId = settings.MetadataProfileId,
qualityProfileId = qualityToUse,
rootFolderPath = rootFolderPath,
};
// Console.Write(newArtist);
var result = await _lidarrApi.AddArtist(newArtist, settings.ApiKey, settings.FullUri);
if (result != null && result.id > 0)
{
return new SenderResult { Message = "Artist has been requested!", Sent = true, Success = true };
}
return new SenderResult { Success = false, Sent = false, Message = "Artist is already monitored" };
}
private async Task<SenderResult> SetupAlbum(MusicRequests model, ArtistResult artist, LidarrSettings settings)
{
// Get the album id
var albums = await _lidarrApi.GetAllAlbumsByArtistId(artist.id, settings.ApiKey, settings.FullUri);

@ -261,7 +261,7 @@ namespace Ombi.Core.Senders
qualityProfileId = qualityToUse,
titleSlug = model.ParentRequest.Title,
seriesType = seriesType,
addOptions = new AddOptions
addOptions = new Ombi.Api.Sonarr.Models.AddOptions
{
ignoreEpisodesWithFiles = false, // There shouldn't be any episodes with files, this is a new season
ignoreEpisodesWithoutFiles = false, // We want all missing

@ -63,7 +63,6 @@ using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.SickRage;
using Ombi.Schedule.Processor;
using Quartz.Spi;
using Ombi.Api.MusicBrainz;
using Ombi.Api.Twilio;
using Ombi.Api.CloudService;
using Ombi.Api.RottenTomatoes;
@ -167,7 +166,6 @@ namespace Ombi.DependencyInjection
services.AddTransient<IOneSignalApi, OneSignalApi>();
services.AddTransient<ILidarrApi, LidarrApi>();
services.AddTransient<IGroupMeApi, GroupMeApi>();
services.AddTransient<IMusicBrainzApi, MusicBrainzApi>();
services.AddTransient<IWhatsAppApi, WhatsAppApi>();
services.AddTransient<ICloudMobileNotification, CloudMobileNotification>();
services.AddTransient<IEmbyApiFactory, EmbyApiFactory>();

@ -28,7 +28,6 @@
<ProjectReference Include="..\Ombi.Api.GroupMe\Ombi.Api.GroupMe.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />

@ -197,7 +197,7 @@ namespace Ombi.Notifications.Tests
public void MusicNotificationTests()
{
var notificationOptions = new NotificationOptions();
var req = F.Build<AlbumRequest>()
var req = F.Build<MusicRequests>()
.With(x => x.RequestType, RequestType.Album)
.With(x => x.Available, true)
.Create();
@ -232,7 +232,7 @@ namespace Ombi.Notifications.Tests
public string MusicNotificationTests_RequestStatus(bool available, bool denied, bool approved)
{
var notificationOptions = new NotificationOptions();
var req = F.Build<AlbumRequest>()
var req = F.Build<MusicRequests>()
.With(x => x.RequestType, RequestType.Album)
.With(x => x.Available, available)
.With(x => x.Denied, denied)

@ -46,7 +46,8 @@ namespace Ombi.Notifications
protected ChildRequests TvRequest { get; set; }
protected AlbumRequest AlbumRequest { get; set; }
protected MusicRequests AlbumRequest { get; set; }
protected MusicRequests ArtistRequest { get; set; }
protected MovieRequests MovieRequest { get; set; }
protected IQueryable<OmbiUser> Subscribed { get; private set; }

@ -88,7 +88,7 @@ namespace Ombi.Notifications
CalculateRequestStatus(req);
}
public void Setup(NotificationOptions opts, AlbumRequest req, CustomizationSettings s,
public void Setup(NotificationOptions opts, MusicRequests req, CustomizationSettings s,
UserNotificationPreferences pref)
{
LoadIssues(opts);

@ -41,7 +41,7 @@ namespace Ombi.Schedule.Jobs.Lidarr
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Lidarr Availability Check Started");
var allAlbumRequests = _requestRepository.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available);
var albumsToUpdate = new List<AlbumRequest>();
var albumsToUpdate = new List<MusicRequests>();
foreach (var request in allAlbumRequests)
{
// Check if we have it cached

@ -81,7 +81,23 @@ namespace Ombi.Schedule.Jobs.Ombi
await _requestQueue.SaveChangesAsync();
continue;
}
var result = await _musicSender.Send(musicRequest);
var result = await _musicSender.SendAlbum(musicRequest);
if (result.Success)
{
request.Completed = DateTime.UtcNow;
await _requestQueue.SaveChangesAsync();
}
}
if (request.Type == RequestType.Artist)
{
var musicRequest = await _musicRequestRepository.GetAll().FirstOrDefaultAsync(x => x.Id == request.RequestId);
if (musicRequest == null)
{
await _requestQueue.Delete(request);
await _requestQueue.SaveChangesAsync();
continue;
}
var result = await _musicSender.SendArtist(musicRequest);
if (result.Success)
{
request.Completed = DateTime.UtcNow;

@ -34,7 +34,7 @@ namespace Ombi.Store.Context
public DbSet<NotificationTemplates> NotificationTemplates { get; set; }
public DbSet<MovieRequests> MovieRequests { get; set; }
public DbSet<AlbumRequest> AlbumRequests { get; set; }
public DbSet<MusicRequests> MusicRequests { get; set; }
public DbSet<TvRequests> TvRequests { get; set; }
public DbSet<ChildRequests> ChildRequests { get; set; }
public DbSet<EpisodeRequests> EpisodeRequests { get; set; }

@ -5,5 +5,6 @@
TvShow = 0,
Movie = 1,
Album = 2,
Artist = 3
}
}

@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities.Requests
{
[Table("AlbumRequests")]
public class AlbumRequest : BaseRequest
[Table("MusicRequests")]
public class MusicRequests : BaseRequest
{
public string ForeignAlbumId { get; set; }
public string ForeignArtistId { get; set; }
@ -17,6 +17,9 @@ namespace Ombi.Store.Entities.Requests
public bool Subscribed { get; set; }
[NotMapped]
public bool ShowSubscribe { get; set; }
public bool Monitored { get; set; }
public string Monitor { get; set; }
public bool SearchForMissingAlbums { get; set; }
[NotMapped]

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiMySql
{
public partial class RenameAlbumRequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameTable("AlbumRequests", null, "MusicRequests");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiMySql
{
public partial class MusicRequestsMonitor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Monitored",
table: "MusicRequests",
type: "BOOLEAN",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Monitor",
table: "MusicRequests",
type: "longtext",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "SearchForMissingAlbums",
table: "MusicRequests",
type: "longtext",
nullable: false,
defaultValue: "");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Monitored",
table: "MusicRequests");
migrationBuilder.DropColumn(
name: "Monitor",
table: "MusicRequests");
migrationBuilder.DropColumn(
name: "SearchForMissingAlbums",
table: "MusicRequests");
}
}
}

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiSqlite
{
public partial class RenameAlbumRequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameTable("AlbumRequests", null, "MusicRequests");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiSqlite
{
public partial class MusicRequestsMonitor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Monitored",
table: "MusicRequests",
type: "BOOLEAN",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Monitor",
table: "MusicRequests",
type: "longtext",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "SearchForMissingAlbums",
table: "MusicRequests",
type: "longtext",
nullable: false,
defaultValue: "");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Monitored",
table: "MusicRequests");
migrationBuilder.DropColumn(
name: "Monitor",
table: "MusicRequests");
migrationBuilder.DropColumn(
name: "SearchForMissingAlbums",
table: "MusicRequests");
}
}
}

@ -427,76 +427,6 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.ToTable("RequestSubscription");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Approved")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<bool>("Available")
.HasColumnType("INTEGER");
b.Property<string>("Cover")
.HasColumnType("TEXT");
b.Property<bool?>("Denied")
.HasColumnType("INTEGER");
b.Property<string>("DeniedReason")
.HasColumnType("TEXT");
b.Property<string>("Disk")
.HasColumnType("TEXT");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<DateTime>("MarkedAsApproved")
.HasColumnType("TEXT");
b.Property<DateTime?>("MarkedAsAvailable")
.HasColumnType("TEXT");
b.Property<DateTime>("MarkedAsDenied")
.HasColumnType("TEXT");
b.Property<decimal>("Rating")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<int>("RequestType")
.HasColumnType("INTEGER");
b.Property<string>("RequestedByAlias")
.HasColumnType("TEXT");
b.Property<DateTime>("RequestedDate")
.HasColumnType("TEXT");
b.Property<string>("RequestedUserId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("AlbumRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
@ -736,6 +666,85 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MusicRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Approved")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<bool>("Available")
.HasColumnType("INTEGER");
b.Property<string>("Cover")
.HasColumnType("TEXT");
b.Property<bool?>("Denied")
.HasColumnType("INTEGER");
b.Property<string>("DeniedReason")
.HasColumnType("TEXT");
b.Property<string>("Disk")
.HasColumnType("TEXT");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<DateTime>("MarkedAsApproved")
.HasColumnType("TEXT");
b.Property<DateTime?>("MarkedAsAvailable")
.HasColumnType("TEXT");
b.Property<DateTime>("MarkedAsDenied")
.HasColumnType("TEXT");
b.Property<string>("Monitor")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.Property<decimal>("Rating")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<int>("RequestType")
.HasColumnType("INTEGER");
b.Property<string>("RequestedByAlias")
.HasColumnType("TEXT");
b.Property<DateTime>("RequestedDate")
.HasColumnType("TEXT");
b.Property<string>("RequestedUserId")
.HasColumnType("TEXT");
b.Property<bool>("SearchForMissingAlbums")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MusicRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
@ -1060,15 +1069,6 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.Navigation("User");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
@ -1135,6 +1135,15 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.Navigation("RequestedUser");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MusicRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")

@ -4,14 +4,14 @@ using Ombi.Store.Entities.Requests;
namespace Ombi.Store.Repository.Requests
{
public interface IMusicRequestRepository : IRepository<AlbumRequest>
public interface IMusicRequestRepository : IRepository<MusicRequests>
{
IQueryable<AlbumRequest> GetAll(string userId);
AlbumRequest GetRequest(string foreignAlbumId);
Task<AlbumRequest> GetRequestAsync(string foreignAlbumId);
IQueryable<AlbumRequest> GetWithUser();
IQueryable<AlbumRequest> GetWithUser(string userId);
IQueryable<MusicRequests> GetAll(string userId);
MusicRequests GetRequest(string foreignAlbumId);
Task<MusicRequests> GetRequestAsync(string foreignAlbumId);
IQueryable<MusicRequests> GetWithUser();
IQueryable<MusicRequests> GetWithUser(string userId);
Task Save();
Task Update(AlbumRequest request);
Task Update(MusicRequests request);
}
}

@ -9,7 +9,7 @@ using Ombi.Store.Entities.Requests;
namespace Ombi.Store.Repository.Requests
{
public class MusicRequestRepository : Repository<AlbumRequest>, IMusicRequestRepository
public class MusicRequestRepository : Repository<MusicRequests>, IMusicRequestRepository
{
public MusicRequestRepository(OmbiContext ctx) : base(ctx)
{
@ -18,48 +18,58 @@ namespace Ombi.Store.Repository.Requests
private OmbiContext Db { get; }
public Task<AlbumRequest> GetRequestAsync(string foreignAlbumId)
public Task<MusicRequests> GetRequestAsync(string foreignAlbumId)
{
return Db.AlbumRequests.Where(x => x.ForeignAlbumId == foreignAlbumId)
return Db.MusicRequests.Where(x => x.ForeignAlbumId == foreignAlbumId)
.Include(x => x.RequestedUser)
.FirstOrDefaultAsync();
}
public IQueryable<AlbumRequest> GetAll(string userId)
public IQueryable<MusicRequests> GetAll(string userId)
{
return GetWithUser().Where(x => x.RequestedUserId == userId);
}
public AlbumRequest GetRequest(string foreignAlbumId)
public MusicRequests GetRequest(string foreignAlbumId)
{
return Db.AlbumRequests.Where(x => x.ForeignAlbumId == foreignAlbumId)
return Db.MusicRequests.Where(x => x.ForeignAlbumId == foreignAlbumId)
.Include(x => x.RequestedUser)
.FirstOrDefault();
}
public IQueryable<AlbumRequest> GetWithUser()
public IQueryable<MusicRequests> GetWithUser()
{
return Db.AlbumRequests
return Db.MusicRequests
.Include(x => x.RequestedUser)
.ThenInclude(x => x.NotificationUserIds)
.AsQueryable();
}
public IQueryable<AlbumRequest> GetWithUser(string userId)
public IQueryable<MusicRequests> GetWithUser(string userId)
{
return Db.AlbumRequests
return Db.MusicRequests
.Where(x => x.RequestedUserId == userId)
.Include(x => x.RequestedUser)
.ThenInclude(x => x.NotificationUserIds)
.AsQueryable();
}
public async Task Update(AlbumRequest request)
public async Task Update(MusicRequests request)
{
if (Db.Entry(request).State == EntityState.Detached)
{
Db.AlbumRequests.Attach(request);
Db.MusicRequests.Attach(request);
Db.Update(request);
}
await InternalSaveChanges();
}
public async Task UpdateArtist(MusicRequests request)
{
if (Db.Entry(request).State == EntityState.Detached)
{
Db.MusicRequests.Attach(request);
Db.Update(request);
}
await InternalSaveChanges();

@ -112,8 +112,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Hubs", "Ombi.Hubs\Ombi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.GroupMe", "Ombi.Api.GroupMe\Ombi.Api.GroupMe.csproj", "{9266403C-B04D-4C0F-AC39-82F12C781949}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.MusicBrainz", "Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj", "{C5C1769B-4197-4410-A160-0EEF39EDDC98}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Twilio", "Ombi.Api.Twilio\Ombi.Api.Twilio.csproj", "{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.HealthChecks", "Ombi.HealthChecks\Ombi.HealthChecks.csproj", "{59D19538-0496-44EE-936E-EBBC22CF7B27}"
@ -431,6 +429,10 @@ Global
{8F19C701-7881-4BC7-8BBA-B068A6B954AD}.NonUiBuild|Any CPU.Build.0 = NonUiBuild|Any CPU
{8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F19C701-7881-4BC7-8BBA-B068A6B954AD}.Release|Any CPU.Build.0 = Release|Any CPU
{0C81C423-25CC-4A91-ABE7-9C3F97770F69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C81C423-25CC-4A91-ABE7-9C3F97770F69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C81C423-25CC-4A91-ABE7-9C3F97770F69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C81C423-25CC-4A91-ABE7-9C3F97770F69}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

@ -11,7 +11,7 @@
</div>
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image"
alt="{{result.title}}">
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<div [ngClass]="result.posterPath.includes('images/') ? 'middle-show' : 'middle'">
<a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary">
<div class="title" id="title{{result.id}}">{{result.title}}</div>
@ -26,6 +26,10 @@
<i *ngIf="loading" class="fas fa-spinner fa-pulse fa-2x fa-fw" aria-hidden="true"></i>
{{'Common.Request' | translate }}
</button>
<button id="requestButton{{result.id}}{{result.type}}{{discoverType}}" *ngIf="monitored" mat-raised-button
class="btn-spacing full-width poster-request-btn" color="accent" [disabled]>
<i class="fa-lg fas fa-check"></i> {{'Common.Monitored' | translate }}
</button>
</div>
</div>
</div>

@ -27,6 +27,7 @@ export class DiscoverCardComponent implements OnInit {
public loading: boolean;
public requestable: boolean;
public monitored: boolean;
// This data is needed to open the dialog
private tvSearchResult: ISearchTvResultV2;
@ -42,6 +43,9 @@ export class DiscoverCardComponent implements OnInit {
if (this.result.type == RequestType.movie) {
this.getExtraMovieInfo();
}
if (this.result.type == RequestType.artist) {
this.getArtistInformation();
}
if (this.result.type == RequestType.album) {
this.getAlbumInformation();
}
@ -54,7 +58,7 @@ export class DiscoverCardComponent implements OnInit {
this.updateTvItem(this.tvSearchResult);
}
public async getAlbumInformation() {
public async getArtistInformation() {
this.searchService.getArtistInformation(this.result.id.toString()).subscribe(x => {
if (x.poster) {
this.result.posterPath = x.poster;
@ -63,14 +67,38 @@ export class DiscoverCardComponent implements OnInit {
this.searchService.getReleaseGroupArt(this.result.id.toString()).subscribe(art => {
if (art.image) {
this.result.posterPath = art.image;
}
})
}
this.result.title = x.startYear ? `${x.name} (${x.startYear})` : x.name;
this.result.title = x.name;
this.result.overview = x.overview;
this.fullyLoaded = true;
if (x.monitored) {
this.requestable = false;
this.monitored = true;
} else {
this.requestable = true;
this.monitored = false;
}
});
}
public async getAlbumInformation() {
this.searchService.getAlbumInformation(this.result.id.toString()).subscribe(x => {
if (x.cover) {
this.result.posterPath = x.cover;
this.fullyLoaded = true;
}
this.result.title = x.title;
this.result.overview = x.overview;
this.fullyLoaded = true;
this.requestable = true;
if (x.monitored) {
this.requestable = false;
this.monitored = true;
} else {
this.requestable = true;
this.monitored = false;
}
});
}
@ -80,8 +108,10 @@ export class DiscoverCardComponent implements OnInit {
return `/details/movie/${this.result.id}`;
case RequestType.tvShow:
return `/details/tv/${this.result.id}`;
case RequestType.album: //Actually artist
case RequestType.artist: //Actually artist
return `/details/artist/${this.result.id}`;
case RequestType.album:
return `/details/album/${this.result.id}`;
}
}
@ -115,6 +145,27 @@ export class DiscoverCardComponent implements OnInit {
event.preventDefault();
this.loading = true;
switch (this.result.type) {
case RequestType.artist:
this.requestService.requestArtist({ foreignArtistId: this.result.id.toString(), monitored: true, monitor: "all", searchForMissingAlbums: true }).subscribe(x => {
if (x.result) {
this.result.requested = true;
this.messageService.send(x.message, "Ok");
} else {
this.messageService.send(x.errorMessage, "Ok");
}
this.loading = false;
});
return;
case RequestType.album:
this.requestService.requestAlbum({foreignAlbumId: this.result.id.toString(), monitor: 'all', monitored: true, searchForMissingAlbums: false}).subscribe(x => {
if (x.result) {
this.result.requested = true;
this.messageService.send(x.message, "Ok");
} else {
this.messageService.send(x.errorMessage, "Ok");
}
this.loading = false;
});
case RequestType.tvShow:
const dia = this.dialog.open(EpisodeRequestComponent, { width: "700px", data: { series: this.tvSearchResult, isAdmin: this.isAdmin }, panelClass: 'modal-panel' });
dia.afterClosed().subscribe(x => this.loading = false);

@ -87,6 +87,8 @@ export class DiscoverSearchResultsComponent implements OnInit {
} else if (m.mediaType == "tv") {
mediaType = RequestType.tvShow;
} else if (m.mediaType == "Artist") {
mediaType = RequestType.artist;
} else if (m.mediaType == "Album") {
mediaType = RequestType.album;
}
@ -98,10 +100,13 @@ export class DiscoverSearchResultsComponent implements OnInit {
if (mediaType === RequestType.tvShow) {
poster = "images/default_tv_poster.png"
}
if (mediaType === RequestType.album || mediaType === RequestType.artist) {
poster = "images/default-music-placeholder.png";
}
}
this.discoverResults.push({
posterPath: mediaType !== RequestType.album ? poster : "images/default-music-placeholder.png",
posterPath: poster,
requested: false,
title: m.title,
type: mediaType,
@ -154,6 +159,9 @@ export class DiscoverSearchResultsComponent implements OnInit {
if (mediaType === RequestType.tvShow) {
poster = "images/default_tv_poster.png"
}
if (mediaType === RequestType.album || mediaType === RequestType.artist) {
poster = "images/default-music-placeholder.png";
}
}
this.discoverResults.push({

@ -15,6 +15,29 @@ export interface IArtistSearchResult {
links: IArtistLinks;
members: IBandMembers[];
overview: string;
monitored: boolean;
background: any;
}
export interface IAlbumSearchResult {
title: string;
id: string;
startYear: string;
endYear: string;
type: string;
country: string;
region: string;
disambiguation: string;
banner: string;
logo: string;
cover: string;
fanArt: string;
releaseGroups: IReleaseGroups[];
links: IArtistLinks;
members: IBandMembers[];
overview: string;
monitored: boolean;
background: any;
}

@ -3,7 +3,8 @@
export enum RequestType {
tvShow = 0,
movie = 1,
album = 2,
artist = 2,
album = 3
}
// NEW WORLD
@ -44,6 +45,16 @@ export interface IAlbumRequest extends IBaseRequest {
export interface IAlbumRequestModel {
foreignAlbumId: string;
monitored: boolean;
monitor: string;
searchForMissingAlbums: boolean;
}
export interface IArtistRequestModel {
foreignArtistId: string;
monitored: boolean;
monitor: string;
searchForMissingAlbums: boolean;
}
export interface IRequestsViewModel<T> {

@ -80,8 +80,9 @@ export class IssuesDetailsComponent implements OnInit {
this.router.navigate(['/details/movie/', this.providerId]);
return;
case RequestType.album:
this.router.navigate(['/details/artist/', this.providerId]);
case RequestType.artist:
this.router.navigate(['/details/artist/', firstIssue.providerId]);
return;
case RequestType.tvShow:

@ -0,0 +1,205 @@
<div *ngIf="!album" class="justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner>
</div>
<div *ngIf="album" class="dark-theme">
<top-banner [title]="album.title" [background]="getBackground()" [tagline]="album.disambiguation"></top-banner>
<section id="info-wrapper">
<div class="small-middle-container">
<div class="row">
<media-poster [posterPath]="album.cover"></media-poster>
<!--Next to poster-->
<!-- <div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="artist.links?.homePage" [doNotAppend]="true" [imdbId]="artist.links?.imdb" [twitter]="artist.links?.twitter" [facebook]="artist.links?.facebook" [instagram]="artist.links?.instagram"></social-icons>
</div> -->
<div class="col-12 col-lg-6 col-xl-6 media-row">
<button *ngIf="!album.monitored" mat-raised-button class="btn-spacing" color="primary" (click)="requestAlbum()">
<i class="fas fa-plus"></i> {{ 'MediaDetails.RequestAlbum' | translate }}</button>
<button mat-raised-button class="btn-green btn-spacing" *ngIf="album.monitored"> {{
'Common.Requested' | translate }}</button>
<span *ngIf="!album.monitored">
<span *ngIf="album.monitored; then requestedBtn else notRequestedBtn"></span>
<!-- <ng-template #requestedBtn>
<button mat-raised-button *ngIf="!hasRequest || hasRequest && movieRequest && !movieRequest.denied"
class="btn-spacing" color="warn" [disabled]><i class="fas fa-check"></i>
{{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i> <i
*ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
<!-- </ng-template> -->
</span>
<!-- <span *ngIf="isAdmin && hasRequest">
<button (click)="approve()" mat-raised-button class="btn-spacing" color="accent">
<i class="fas fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
<button *ngIf="!movie.available" (click)="markAvailable()" mat-raised-button class="btn-spacing"
color="accent">
<i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
<button *ngIf="movieRequest && !movieRequest.denied" mat-raised-button class="btn-spacing" color="warn"
(click)="deny()">
<i class="fas fa-times"></i> {{
'Requests.Deny' | translate }}</button>
<button *ngIf="movieRequest && movieRequest.denied" [matTooltip]="movieRequest.deniedReason"
mat-raised-button class="btn-spacing" color="warn">
<i class="fas fa-times"></i> {{
'MediaDetails.Denied' | translate }}</button>
</span> -->
<!-- <button *ngIf="(hasRequest && movieRequest) || movie.available" mat-raised-button class="btn-spacing"
color="danger" (click)="issue()">
<i class="fas fa-exclamation"></i> {{
'Requests.ReportIssue' | translate }}</button> -->
</div>
</div>
<div class="row">
<!-- <div class="col-12 col-md-2">
<mat-card class="mat-elevation-z8">
<mat-card-content class="medium-font">
<album-information-panel [album]="album"></album-information-panel>
</mat-card-content>
</mat-card>
</div> -->
<div class="col-12 col-md-10">
<div class="row">
<div class="col-12">
<mat-card class=" mat-elevation-z8 spacing-below">
<mat-card-content>
<h1>{{album.title}} - {{album.disambiguation}}</h1>
<br />
{{album.overview}}
</mat-card-content>
</mat-card>
</div>
</div>
<!-- <div class="row">
<div class="col-12">
<div class="issuesPanel" *ngIf="album.requestId">
<issues-panel [requestId]="album.requestId" [isAdmin]="isAdmin"></issues-panel>
</div>
</div>
</div> -->
<!-- <div class="row">
<div class="col-12">
<mat-accordion class="mat-elevation-z8 spacing-below">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.RecommendationsTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.recommendations.results.length > 0">
<div class="col-md-2" *ngFor="let r of movie.recommendations.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}" alt="Poster"
style="display: block;">
</a>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.SimilarTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.similar.results.length > 0">
<div class="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster ">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}" alt="Poster"
style="display: block;">
</a>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.VideosTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.videos.results.length > 0">
<div class="col-md-6" *ngFor="let video of movie.videos.results">
<iframe width="100%" height="315px" [src]="'https://www.youtube.com/embed/' + video.key | safe"
frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</div> -->
</div>
</div>
</div>
<div class="bottom-page-gap">
</div>
</section>
</div>

@ -0,0 +1,126 @@
import { Component, ViewEncapsulation } from "@angular/core";
import { ImageService, SearchV2Service, RequestService, MessageService } from "../../../services";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
import { MatDialog } from "@angular/material/dialog";
import { YoutubeTrailerComponent } from "../shared/youtube-trailer.component";
import { AuthService } from "../../../auth/auth.service";
import { DenyDialogComponent } from "../shared/deny-dialog/deny-dialog.component";
import { NewIssueComponent } from "../shared/new-issue/new-issue.component";
import { IAlbumSearchResult, IReleaseGroups } from "../../../interfaces/IMusicSearchResultV2";
@Component({
templateUrl: "./album-details.component.html",
styleUrls: ["../../media-details.component.scss"],
encapsulation: ViewEncapsulation.None
})
export class AlbumDetailsComponent {
private albumId: string;
public album: IAlbumSearchResult = null;
private selectedAlbums: IReleaseGroups[] = [];
public isAdmin: boolean;
constructor(private searchService: SearchV2Service, private route: ActivatedRoute,
private sanitizer: DomSanitizer, private imageService: ImageService,
public dialog: MatDialog, private requestService: RequestService,
public messageService: MessageService, private auth: AuthService) {
this.route.params.subscribe((params: any) => {
this.albumId = params.albumId;
this.load();
});
}
public load() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.searchService.getAlbumInformation(this.albumId).subscribe(x => this.album = x);
}
public getBackground(): string {
if (this.album.cover) {
this.album.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + this.album.cover + ")");
return this.album.background
}
return this.album.background
}
public async requestAlbum() {
if (this.album.monitored) {
return;
}
this.requestService.requestAlbum({
foreignAlbumId: this.album.id,
monitored: true,
monitor: "existing",
searchForMissingAlbums: true
}).toPromise()
.then(r => {
if (r.result) {
this.album.monitored = true;
this.messageService.send(r.message);
} else {
this.messageService.send(r.errorMessage);
}
})
.catch(r => {
console.log(r);
this.messageService.send("Error when requesting album");
});
// const
}
public openDialog() {
this.dialog.open(YoutubeTrailerComponent, {
width: '560px',
// data: this.movie.videos.results[0].key
});
}
public async deny() {
const dialogRef = this.dialog.open(DenyDialogComponent, {
width: '250px',
// data: {requestId: this.movieRequest.id, requestType: RequestType.movie}
});
dialogRef.afterClosed().subscribe(result => {
// this.movieRequest.denied = result;
// if(this.movieRequest.denied) {
// this.movie.approved = false;
// }
});
}
public async issue() {
const dialogRef = this.dialog.open(NewIssueComponent, {
width: '500px',
// data: {requestId: this.movieRequest ? this.movieRequest.id : null, requestType: RequestType.movie, imdbid: this.movie.imdbId}
});
}
public async approve() {
// const result = await this.requestService.approveMovie({ id: this.movieRequest.id }).toPromise();
// if (result.result) {
// this.movie.approved = false;
// this.messageService.send("Successfully Approved", "Ok");
// } else {
// this.messageService.send(result.errorMessage, "Ok");
// }
}
public async markAvailable() {
// const result = await this.requestService.markMovieAvailable({id: this.movieRequest.id}).toPromise();
// if (result.result) {
// // this.movie.available = true;
// this.messageService.send(result.message, "Ok");
// } else {
// this.messageService.send(result.errorMessage, "Ok");
// }
}
public setAdvancedOptions(data: any) {
// this.advancedOptions = data;
}
}

@ -0,0 +1,14 @@
<div *ngIf="album">
<div>
<strong>Type:</strong>
<div>{{album.type}}</div>
</div>
<div>
<strong>Country</strong>
<div>{{album.country}}</div>
</div>
<div>
<strong>Release Date</strong>
<div>{{album.startYear}}</div>
</div>
</div>

@ -0,0 +1,12 @@
import { Component, Input, ViewEncapsulation } from "@angular/core";
import { ISearchArtistResult } from "../../../../../interfaces";
@Component({
templateUrl: "./album-information-panel.component.html",
styleUrls: ["../../../../media-details.component.scss"],
selector: "album-information-panel",
encapsulation: ViewEncapsulation.None
})
export class AlbumInformationPanel {
@Input() public album: ISearchAlbumResult;
}

@ -16,73 +16,22 @@
<!--Next to poster-->
<div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="artist.links.homePage" [doNotAppend]="true" [imdbId]="artist.links.imdb" [twitter]="artist.links.twitter" [facebook]="artist.links.facebook" [instagram]="artist.links.instagram"></social-icons>
<!-- <social-icons [homepage]="artist.links.homePage" [doNotAppend]="true" [imdbId]="artist.links.imdb" [twitter]="artist.links.twitter" [facebook]="artist.links.facebook" [instagram]="artist.links.instagram"></social-icons> -->
</div>
<div class="col-12 col-lg-6 col-xl-6 media-row">
<button mat-raised-button *ngIf="selectedAlbums.length === 0" class="btn-spacing" color="primary" (click)="requestAllAlbums()">
<i class="fas fa-plus"></i> {{ 'MediaDetails.RequestAllAlbums' | translate }}</button>
<button mat-raised-button *ngIf="selectedAlbums.length > 0" class="btn-spacing" color="primary" (click)="requestAllAlbums()">
<i class="fas fa-plus"></i> {{ 'MediaDetails.RequestSelectedAlbums' | translate }}</button>
<button mat-raised-button *ngIf="selectedAlbums.length > 0" class="btn-spacing" color="accent" (click)="clearSelection()">
<i class="fas fa-minus"></i> {{ 'MediaDetails.ClearSelection' | translate }}</button>
<!-- <button mat-raised-button class="btn-green btn-spacing" *ngIf="movie.available"> {{
'Common.Available' | translate }}</button> -->
<!-- <span *ngIf="!movie.available">
<span *ngIf="movie.requested || movie.approved; then requestedBtn else notRequestedBtn"></span>
<ng-template #requestedBtn>
<button mat-raised-button *ngIf="!hasRequest || hasRequest && movieRequest && !movieRequest.denied"
class="btn-spacing" color="warn" [disabled]><i class="fas fa-check"></i>
{{ 'Common.Requested' | translate }}</button>
</ng-template>
<ng-template #notRequestedBtn>
<button mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i> <i
*ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i> {{
'Common.Request' | translate }}</button>
</ng-template>
</span> -->
<!-- <span *ngIf="isAdmin && hasRequest">
<button (click)="approve()" mat-raised-button class="btn-spacing" color="accent">
<i class="fas fa-plus"></i> {{ 'Common.Approve' | translate }}
</button>
<button *ngIf="!movie.available" (click)="markAvailable()" mat-raised-button class="btn-spacing"
color="accent">
<i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button>
<button *ngIf="movieRequest && !movieRequest.denied" mat-raised-button class="btn-spacing" color="warn"
(click)="deny()">
<i class="fas fa-times"></i> {{
'Requests.Deny' | translate }}</button>
<button *ngIf="movieRequest && movieRequest.denied" [matTooltip]="movieRequest.deniedReason"
mat-raised-button class="btn-spacing" color="warn">
<i class="fas fa-times"></i> {{
'MediaDetails.Denied' | translate }}</button>
</span> -->
<!-- <button *ngIf="(hasRequest && movieRequest) || movie.available" mat-raised-button class="btn-spacing"
color="danger" (click)="issue()">
<i class="fas fa-exclamation"></i> {{
'Requests.ReportIssue' | translate }}</button> -->
<button mat-raised-button *ngIf="artist.monitored" class="btn-spacing" color="primary"
color="accent" [disabled]>
<i class="fa-lg fas fa-check"></i> {{'Common.Monitored' | translate }}</button>
<button mat-raised-button *ngIf="!artist.monitored && selectedAlbums.length === 0" class="btn-spacing" color="primary" (click)="requestAllAlbums()">
<i class="fas fa-plus"></i> {{ 'Common.Request' | translate }}</button>
</div>
</div>
<div class="row">
<div class="col-12 col-md-2">
<!-- <div class="col-12 col-md-2">
<mat-card class="mat-elevation-z8">
<mat-card-content class="medium-font">
@ -91,7 +40,7 @@
</mat-card>
</div>
</div> -->
<div class="col-12 col-md-10">
<div class="row">
@ -106,7 +55,7 @@
<div class="row">
<div class="col-12">
<artist-release-panel (onAlbumSelect)="albumSelected($event)" (albumLoad)="albumLoad($event)" *ngIf="artist.releaseGroups.length > 0" [releases]="artist.releaseGroups"></artist-release-panel>
<artist-release-panel (onAlbumSelect)="albumSelected($event)" (albumLoad)="albumLoad($event)" *ngIf="artist.releaseGroups?.length > 0" [releases]="artist.releaseGroups"></artist-release-panel>
</div>
</div>
@ -119,78 +68,6 @@
</div>
</div>
</div>
<!-- <div class="row">
<div class="col-12">
<mat-accordion class="mat-elevation-z8 spacing-below">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.RecommendationsTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.recommendations.results.length > 0">
<div class="col-md-2" *ngFor="let r of movie.recommendations.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}" alt="Poster"
style="display: block;">
</a>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.SimilarTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.similar.results.length > 0">
<div class="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster ">
<a [routerLink]="'/details/movie/'+r.id">
<img class="real grow" matTooltip="{{r.title}}"
src="https://image.tmdb.org/t/p/w300/{{r.poster_path}}" alt="Poster"
style="display: block;">
</a>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{'MediaDetails.VideosTitle' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.videos.results.length > 0">
<div class="col-md-6" *ngFor="let video of movie.videos.results">
<iframe width="100%" height="315px" [src]="'https://www.youtube.com/embed/' + video.key | safe"
frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</div> -->
</div>
</div>

@ -1,4 +1,4 @@
import { Component } from "@angular/core";
import { Component, ViewEncapsulation } from "@angular/core";
import { ImageService, SearchV2Service, RequestService, MessageService } from "../../../services";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
@ -13,6 +13,7 @@ import { TranslateService } from "@ngx-translate/core";
@Component({
templateUrl: "./artist-details.component.html",
styleUrls: ["../../media-details.component.scss"],
encapsulation: ViewEncapsulation.None
})
export class ArtistDetailsComponent {
private artistId: string;
@ -89,7 +90,10 @@ export class ArtistDetailsComponent {
return;
}
this.requestService.requestAlbum({
foreignAlbumId : a.id
foreignAlbumId : a.id,
monitored: true,
monitor: "all",
searchForMissingAlbums: true
}).toPromise()
.then(r => {
if (r.result) {
@ -112,7 +116,10 @@ export class ArtistDetailsComponent {
return;
}
this.requestService.requestAlbum({
foreignAlbumId : a.id
foreignAlbumId : a.id,
monitored: true,
monitor: "all",
searchForMissingAlbums: true
}).toPromise()
.then(r => {
if (r.result) {

@ -1,4 +1,20 @@
import { IssuesService, RadarrService, RequestService, SearchService, SonarrService } from "../../services";
import { MovieDetailsComponent } from "./movie/movie-details.component";
import { YoutubeTrailerComponent } from "./shared/youtube-trailer.component";
import { TvDetailsComponent } from "./tv/tv-details.component";
import { MovieInformationPanelComponent } from "./movie/panels/movie-information-panel.component";
import { TvInformationPanelComponent } from "./tv/panels/tv-information-panel/tv-information-panel.component";
import { TopBannerComponent } from "./shared/top-banner/top-banner.component";
import { SocialIconsComponent } from "./shared/social-icons/social-icons.component";
import { MediaPosterComponent } from "./shared/media-poster/media-poster.component";
import { CastCarouselComponent } from "./shared/cast-carousel/cast-carousel.component";
import { DenyDialogComponent } from "./shared/deny-dialog/deny-dialog.component";
import { TvRequestsPanelComponent } from "./tv/panels/tv-requests/tv-requests-panel.component";
import { MovieAdvancedOptionsComponent } from "./movie/panels/movie-advanced-options/movie-advanced-options.component";
import { SearchService, RequestService, RadarrService, IssuesService, SonarrService } from "../../services";
import { RequestServiceV2 } from "../../services/requestV2.service";
import { NewIssueComponent } from "./shared/new-issue/new-issue.component";
import { AlbumDetailsComponent } from "./album/album-details.component";
import { ArtistDetailsComponent } from "./artist/artist-details.component";
import { ArtistInformationPanel } from "./artist/panels/artist-information-panel/artist-information-panel.component";
@ -37,6 +53,7 @@ export const components: any[] = [
MovieAdvancedOptionsComponent,
TvAdvancedOptionsComponent,
NewIssueComponent,
AlbumDetailsComponent,
ArtistDetailsComponent,
ArtistInformationPanel,
ArtistReleasePanel,

@ -27,7 +27,7 @@ export class DenyDialogComponent {
if(this.data.requestType == RequestType.tvShow) {
result = await this.requestService.denyChild({id: this.data.requestId, reason: this.denyReason }).toPromise();
}
if(this.data.requestType == RequestType.album) {
if(this.data.requestType == RequestType.artist) {
result = await this.requestService.denyAlbum({id: this.data.requestId, reason: this.denyReason }).toPromise();
}

@ -2,7 +2,7 @@
<div class="sidebar sidebar-poster affixable affix-top">
<div class="poster mobile-poster">
<img class="real" src="{{posterPath}}" alt="Poster"
style="display: block;">
style="display: block; max-width: 400px;">
</div>
<!--Underneith poster-->
<br />

@ -95,6 +95,10 @@
line-height: 1.2;
}
#summary-wrapper .row {
margin-top: 3px;
}
#info-wrapper {
min-height: 600px;
}
@ -256,10 +260,6 @@
display: none !important;
}
#info-wrapper{
margin-top:-200px;
}
.full-screenshot.enabled.overlay{
background-image: linear-gradient(to bottom, transparent, 50%, $ombi-background-primary);
}

@ -11,6 +11,7 @@ import { PipeModule } from "../pipes/pipe.module";
import * as fromComponents from './components';
import { AuthGuard } from "../auth/auth.guard";
import { ArtistDetailsComponent } from "./components/artist/artist-details.component";
import { AlbumDetailsComponent } from "./components/album/album-details.component";
import { ReactiveFormsModule } from "@angular/forms";
@ -19,6 +20,7 @@ const routes: Routes = [
{ path: "tv/:tvdbId/:search", component: TvDetailsComponent, canActivate: [AuthGuard] },
{ path: "tv/:tvdbId", component: TvDetailsComponent, canActivate: [AuthGuard] },
{ path: "artist/:artistId", component: ArtistDetailsComponent, canActivate: [AuthGuard] },
{ path: "album/:albumId", component: AlbumDetailsComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [

@ -28,8 +28,9 @@ export class RequestOptionsComponent {
if (this.data.type === RequestType.tvShow) {
request = this.requestService.deleteChild(this.data.id);
}
if (this.data.type === RequestType.album) {
request = this.requestService.removeAlbumRequest(this.data.id);
if (this.data.type === RequestType.artist) {
await this.requestService.removeAlbumRequest(this.data.id).toPromise();
}
request.subscribe(result => {
if (result.result) {
@ -49,7 +50,7 @@ export class RequestOptionsComponent {
if (this.data.type === RequestType.tvShow) {
await this.requestService.approveChild({id: this.data.id}).toPromise();
}
if (this.data.type === RequestType.album) {
if (this.data.type === RequestType.artist) {
await this.requestService.approveAlbum({id: this.data.id}).toPromise();
}
@ -61,7 +62,7 @@ export class RequestOptionsComponent {
if (this.data.type === RequestType.movie) {
await this.requestService.markMovieAvailable({id: this.data.id}).toPromise();
}
if (this.data.type === RequestType.album) {
if (this.data.type === RequestType.artist) {
await this.requestService.markAlbumAvailable({id: this.data.id}).toPromise();
}

@ -5,7 +5,8 @@ import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { UITreeNode } from "primeng/tree";
import { FilterType, IAlbumRequest, IAlbumRequestModel, IAlbumUpdateModel, IChildRequests, IDenyAlbumModel, IDenyMovieModel, IFilter,
import {
FilterType, IAlbumRequest, IAlbumRequestModel, IArtistRequestModel, IAlbumUpdateModel, IChildRequests, IDenyAlbumModel, IDenyMovieModel, IFilter,
IMovieRequestModel, IMovieRequests, IMovieUpdateModel, IRequestEngineResult, IRequestsViewModel, ITvDenyModel, ITvRequests, ITvUpdateModel, OrderType } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@ -154,7 +155,10 @@ export class RequestService extends ServiceHelpers {
// Music
public requestAlbum(Album: IAlbumRequestModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}music/`, JSON.stringify(Album), {headers: this.headers});
return this.http.post<IRequestEngineResult>(`${this.url}album/`, JSON.stringify(Album), {headers: this.headers});
}
public requestArtist(Artist: IArtistRequestModel): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}artist/`, JSON.stringify(Artist), { headers: this.headers });
}
public getTotalAlbums(): Observable<number> {

@ -9,7 +9,7 @@ import { ServiceHelpers } from "./service.helpers";
import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2";
import { ISearchTvResultV2, IMovieCollectionsViewModel, IActorCredits } from "../interfaces/ISearchTvResultV2";
import { IArtistSearchResult, IAlbumArt, IReleaseGroups } from "../interfaces/IMusicSearchResultV2";
import { IArtistSearchResult, IAlbumSearchResult, IAlbumArt, IReleaseGroups } from "../interfaces/IMusicSearchResultV2";
import { SearchFilter } from "../my-nav/SearchFilter";
import { IMovieRatings, ITvRatings } from "../interfaces/IRatings";
import { IStreamingData } from "../interfaces/IStreams";
@ -140,6 +140,10 @@ export class SearchV2Service extends ServiceHelpers {
return this.http.get<IArtistSearchResult>(`${this.url}/artist/${artistId}`);
}
public getAlbumInformation(albumId: string): Observable<IAlbumSearchResult> {
return this.http.get<IAlbumSearchResult>(`${this.url}/artist/album/${albumId}`);
}
public getReleaseGroupArt(mbid: string): Observable<IAlbumArt> {
return this.http.get<IAlbumArt>(`${this.url}/releasegroupart/${mbid}`);
}

@ -63,7 +63,7 @@ export class RemainingRequestsComponent implements OnInit {
this.matIcon = "fas fa-tv";
break;
case RequestType.album:
case RequestType.artist:
this.requestService.getRemainingMusicRequests().subscribe(callback);
this.matIcon = "fas fa-music";

@ -23,9 +23,9 @@
"no-internal-module": false,
"quotemark": [ true, "double", "avoid-template" ],
"no-console": false,
"no-non-null-assertion": false,
"no-non-null-assertion": false
},
"compilerOptions": {
"experimentalDecorators":true,
"experimentalDecorators":false
}
}

@ -18,7 +18,7 @@ using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Ombi.Controllers.V1
{
[Authorize]
[Route("api/v1/request/music")]
[Route("api/v1/request")]
[Produces("application/json")]
[ApiController]
public class MusicRequestController : ControllerBase
@ -45,7 +45,7 @@ namespace Ombi.Controllers.V1
/// <param name="statusType"></param>
/// <param name="availabilityType"></param>
[HttpGet("{count:int}/{position:int}/{orderType:int}/{statusType:int}/{availabilityType:int}")]
public async Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, int orderType, int statusType, int availabilityType)
public async Task<RequestsViewModel<MusicRequests>> GetRequests(int count, int position, int orderType, int statusType, int availabilityType)
{
return await _engine.GetRequests(count, position, new OrderFilterModel
{
@ -68,7 +68,7 @@ namespace Ombi.Controllers.V1
/// Gets all album requests.
/// </summary>
[HttpGet]
public async Task<IEnumerable<AlbumRequest>> GetRequests()
public async Task<IEnumerable<MusicRequests>> GetRequests()
{
return await _engine.GetRequests();
}
@ -78,7 +78,7 @@ namespace Ombi.Controllers.V1
/// </summary>
/// <param name="album">The album.</param>
/// <returns></returns>
[HttpPost]
[HttpPost("album")]
public async Task<RequestEngineResult> RequestAlbum([FromBody] MusicAlbumRequestViewModel album)
{
album.RequestedByAlias = GetApiAlias();
@ -95,13 +95,36 @@ namespace Ombi.Controllers.V1
return result;
}
/// <summary>
/// Requests a artist.
/// </summary>
/// <param name="artist">The artist.</param>
/// <returns></returns>
[HttpPost("artist")]
public async Task<RequestEngineResult> RequestArtist([FromBody] MusicArtistRequestViewModel artist)
{
Console.Write(artist);
artist.RequestedByAlias = GetApiAlias();
var result = await _engine.RequestArtist(artist);
if (result.Result)
{
var voteResult = await _voteEngine.UpVote(result.RequestId, RequestType.Artist);
if (voteResult.IsError)
{
_log.LogError("Couldn't automatically add the vote for the artist {0} because {1}", artist.ForeignArtistId, voteResult.ErrorMessage);
}
}
return result;
}
/// <summary>
/// Searches for a specific album request
/// </summary>
/// <param name="searchTerm">The search term.</param>
/// <returns></returns>
[HttpGet("search/{searchTerm}")]
public async Task<IEnumerable<AlbumRequest>> Search(string searchTerm)
public async Task<IEnumerable<MusicRequests>> Search(string searchTerm)
{
return await _engine.SearchAlbumRequest(searchTerm);
}

@ -151,31 +151,31 @@ namespace Ombi.Controllers.V2
}
[HttpGet("album/available/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<AlbumRequest>> GetAvailableAlbumRequests(int count, int position, string sort, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetAvailableAlbumRequests(int count, int position, string sort, string sortOrder)
{
return await _musicRequestEngine.GetRequestsByStatus(count, position, sort, sortOrder, RequestStatus.Available);
}
[HttpGet("album/processing/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<AlbumRequest>> GetProcessingAlbumRequests(int count, int position, string sort, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetProcessingAlbumRequests(int count, int position, string sort, string sortOrder)
{
return await _musicRequestEngine.GetRequestsByStatus(count, position, sort, sortOrder, RequestStatus.ProcessingRequest);
}
[HttpGet("album/pending/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<AlbumRequest>> GetPendingAlbumRequests(int count, int position, string sort, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetPendingAlbumRequests(int count, int position, string sort, string sortOrder)
{
return await _musicRequestEngine.GetRequestsByStatus(count, position, sort, sortOrder, RequestStatus.PendingApproval);
}
[HttpGet("album/denied/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<AlbumRequest>> GetDeniedAlbumRequests(int count, int position, string sort, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetDeniedAlbumRequests(int count, int position, string sort, string sortOrder)
{
return await _musicRequestEngine.GetRequestsByStatus(count, position, sort, sortOrder, RequestStatus.Denied);
}
[HttpGet("album/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<AlbumRequest>> GetAlbumRequests(int count, int position, string sort, string sortOrder)
public async Task<RequestsViewModel<MusicRequests>> GetAlbumRequests(int count, int position, string sort, string sortOrder)
{
return await _musicRequestEngine.GetRequests(count, position, sort, sortOrder);
}

@ -429,9 +429,9 @@ namespace Ombi.Controllers.V2
[HttpGet("artist/album/{albumId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public Task<ReleaseGroup> GetAlbumInformation(string albumId)
public Task<AlbumInformation> GetAlbumInformation(string albumId)
{
return _musicEngine.GetAlbum(albumId);
return _musicEngine.GetAlbumInformation(albumId);
}
[HttpGet("releasegroupart/{musicBrainzId}")]

@ -38,7 +38,8 @@
"Update": "Update",
"tvShow": "TV Show",
"movie": "Movie",
"album": "Album"
"album": "Album",
"artist": "Artist"
},
"PasswordReset": {
"EmailAddressPlaceholder": "Email Address",
@ -298,6 +299,7 @@
"RequestAllAlbums": "Request All Albums",
"ClearSelection": "Clear Selection",
"RequestSelectedAlbums": "Request Selected Albums",
"RequestAlbum": "Request Album",
"ViewCollection":"View Collection",
"NotEnoughInfo": "Unfortunately there is not enough information about this show yet!",
"AdvancedOptions":"Advanced Options",

Loading…
Cancel
Save