using System; using System.Collections.Generic; using System.Linq; using System.Net; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Music; using NzbDrone.Core.Profiles.Metadata; namespace NzbDrone.Core.MetadataSource.SkyHook { public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo, ISearchForNewAlbum, ISearchForNewEntity { private readonly IHttpClient _httpClient; private readonly Logger _logger; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; private readonly IMetadataRequestBuilder _requestBuilder; private readonly IMetadataProfileService _metadataProfileService; private readonly ICached> _cache; private static readonly List NonAudioMedia = new List { "DVD", "DVD-Video", "Blu-ray", "HD-DVD", "VCD", "SVCD", "UMD", "VHS" }; private static readonly List SkippedTracks = new List { "[data track]" }; public SkyHookProxy(IHttpClient httpClient, IMetadataRequestBuilder requestBuilder, IArtistService artistService, IAlbumService albumService, Logger logger, IMetadataProfileService metadataProfileService, ICacheManager cacheManager) { _httpClient = httpClient; _metadataProfileService = metadataProfileService; _requestBuilder = requestBuilder; _artistService = artistService; _albumService = albumService; _cache = cacheManager.GetCache>(GetType()); _logger = logger; } public HashSet GetChangedArtists(DateTime startTime) { var startTimeUtc = (DateTimeOffset)DateTime.SpecifyKind(startTime, DateTimeKind.Utc); var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "recent/artist") .AddQueryParam("since", startTimeUtc.ToUnixTimeSeconds()) .Build(); httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.Resource.Limited) { return null; } return new HashSet(httpResponse.Resource.Items); } public Artist GetArtistInfo(string foreignArtistId, int metadataProfileId) { _logger.Debug("Getting Artist with LidarrAPI.MetadataID of {0}", foreignArtistId); var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "artist/" + foreignArtistId) .Build(); httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.HasHttpError) { if (httpResponse.StatusCode == HttpStatusCode.NotFound) { throw new ArtistNotFoundException(foreignArtistId); } else if (httpResponse.StatusCode == HttpStatusCode.BadRequest) { throw new BadRequestException(foreignArtistId); } else { throw new HttpException(httpRequest, httpResponse); } } var artist = new Artist(); artist.Metadata = MapArtistMetadata(httpResponse.Resource); artist.CleanName = Parser.Parser.CleanArtistName(artist.Metadata.Value.Name); artist.SortName = Parser.Parser.NormalizeTitle(artist.Metadata.Value.Name); artist.Albums = FilterAlbums(httpResponse.Resource.Albums, metadataProfileId) .Select(x => MapAlbum(x, null)).ToList(); return artist; } public HashSet GetChangedAlbums(DateTime startTime) { return _cache.Get("ChangedAlbums", () => GetChangedAlbumsUncached(startTime), TimeSpan.FromMinutes(30)); } private HashSet GetChangedAlbumsUncached(DateTime startTime) { var startTimeUtc = (DateTimeOffset)DateTime.SpecifyKind(startTime, DateTimeKind.Utc); var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "recent/album") .AddQueryParam("since", startTimeUtc.ToUnixTimeSeconds()) .Build(); httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.Resource.Limited) { return null; } return new HashSet(httpResponse.Resource.Items); } public IEnumerable FilterAlbums(IEnumerable albums, int metadataProfileId) { var metadataProfile = _metadataProfileService.Exists(metadataProfileId) ? _metadataProfileService.Get(metadataProfileId) : _metadataProfileService.All().First(); var primaryTypes = new HashSet(metadataProfile.PrimaryAlbumTypes.Where(s => s.Allowed).Select(s => s.PrimaryAlbumType.Name)); var secondaryTypes = new HashSet(metadataProfile.SecondaryAlbumTypes.Where(s => s.Allowed).Select(s => s.SecondaryAlbumType.Name)); var releaseStatuses = new HashSet(metadataProfile.ReleaseStatuses.Where(s => s.Allowed).Select(s => s.ReleaseStatus.Name)); return albums.Where(album => primaryTypes.Contains(album.Type) && ((!album.SecondaryTypes.Any() && secondaryTypes.Contains("Studio")) || album.SecondaryTypes.Any(x => secondaryTypes.Contains(x))) && album.ReleaseStatuses.Any(x => releaseStatuses.Contains(x))); } public Tuple> GetAlbumInfo(string foreignAlbumId) { _logger.Debug("Getting Album with LidarrAPI.MetadataID of {0}", foreignAlbumId); var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "album/" + foreignAlbumId) .Build(); httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.HasHttpError) { if (httpResponse.StatusCode == HttpStatusCode.NotFound) { throw new AlbumNotFoundException(foreignAlbumId); } else if (httpResponse.StatusCode == HttpStatusCode.BadRequest) { throw new BadRequestException(foreignAlbumId); } else { throw new HttpException(httpRequest, httpResponse); } } var artists = httpResponse.Resource.Artists.Select(MapArtistMetadata).ToList(); var artistDict = artists.ToDictionary(x => x.ForeignArtistId, x => x); var album = MapAlbum(httpResponse.Resource, artistDict); album.ArtistMetadata = artistDict[httpResponse.Resource.ArtistId]; return new Tuple>(httpResponse.Resource.ArtistId, album, artists); } public List SearchForNewArtist(string title) { try { var lowerTitle = title.ToLowerInvariant(); if (IsMbidQuery(lowerTitle)) { var slug = lowerTitle.Split(':')[1].Trim(); Guid searchGuid; bool isValid = Guid.TryParse(slug, out searchGuid); if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || isValid == false) { return new List(); } try { var existingArtist = _artistService.FindById(searchGuid.ToString()); if (existingArtist != null) { return new List { existingArtist }; } var metadataProfile = _metadataProfileService.All().First().Id; // Change this to Use last Used profile? return new List { GetArtistInfo(searchGuid.ToString(), metadataProfile) }; } catch (ArtistNotFoundException) { return new List(); } } var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "search") .AddQueryParam("type", "artist") .AddQueryParam("query", title.ToLower().Trim()) .Build(); var httpResponse = _httpClient.Get>(httpRequest); return httpResponse.Resource.SelectList(MapSearchResult); } catch (HttpException) { throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title); } catch (Exception ex) { _logger.Warn(ex, ex.Message); throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title); } } public List SearchForNewAlbum(string title, string artist) { try { var lowerTitle = title.ToLowerInvariant(); if (IsMbidQuery(lowerTitle)) { var slug = lowerTitle.Split(':')[1].Trim(); Guid searchGuid; bool isValid = Guid.TryParse(slug, out searchGuid); if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || isValid == false) { return new List(); } try { var existingAlbum = _albumService.FindById(searchGuid.ToString()); if (existingAlbum == null) { var data = GetAlbumInfo(searchGuid.ToString()); var album = data.Item2; album.Artist = _artistService.FindById(data.Item1) ?? new Artist { Metadata = data.Item3.Single(x => x.ForeignArtistId == data.Item1) }; return new List { album }; } existingAlbum.Artist = _artistService.GetArtist(existingAlbum.ArtistId); return new List { existingAlbum }; } catch (AlbumNotFoundException) { return new List(); } } var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "search") .AddQueryParam("type", "album") .AddQueryParam("query", title.ToLower().Trim()) .AddQueryParam("artist", artist.IsNotNullOrWhiteSpace() ? artist.ToLower().Trim() : string.Empty) .AddQueryParam("includeTracks", "1") .Build(); var httpResponse = _httpClient.Get>(httpRequest); return httpResponse.Resource.Select(MapSearchResult) .Where(x => x != null) .ToList(); } catch (HttpException) { throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title); } catch (Exception ex) { _logger.Warn(ex, ex.Message); throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title); } } public List SearchForNewAlbumByRecordingIds(List recordingIds) { var ids = recordingIds.Where(x => x.IsNotNullOrWhiteSpace()).Distinct(); var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "search/fingerprint") .Build(); httpRequest.SetContent(ids.ToJson()); httpRequest.Headers.ContentType = "application/json"; var httpResponse = _httpClient.Post>(httpRequest); return httpResponse.Resource.Select(MapSearchResult) .Where(x => x != null) .ToList(); } public List SearchForNewEntity(string title) { var lowerTitle = title.ToLowerInvariant(); if (IsMbidQuery(lowerTitle)) { var artist = SearchForNewArtist(lowerTitle); if (artist.Any()) { return new List { artist.First() }; } var album = SearchForNewAlbum(lowerTitle, null); if (album.Any()) { var result = album.Where(x => x.AlbumReleases.Value.Any()).FirstOrDefault(); if (result != null) { return new List { result }; } else { return new List(); } } } try { var httpRequest = _requestBuilder.GetRequestBuilder().Create() .SetSegment("route", "search") .AddQueryParam("type", "all") .AddQueryParam("query", lowerTitle.Trim()) .Build(); var httpResponse = _httpClient.Get>(httpRequest); return httpResponse.Resource.Select(MapSearchResult) .Where(x => x != null) .ToList(); } catch (HttpException) { throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title); } catch (Exception ex) { _logger.Warn(ex, ex.Message); throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title); } } private static bool IsMbidQuery(string query) { return query.StartsWith("lidarr:") || query.StartsWith("lidarrid:") || query.StartsWith("mbid:"); } private Artist MapSearchResult(ArtistResource resource) { var artist = _artistService.FindById(resource.Id); if (artist == null) { artist = new Artist(); artist.Metadata = MapArtistMetadata(resource); } return artist; } private Album MapSearchResult(AlbumResource resource) { var artists = resource.Artists.Select(MapArtistMetadata).ToDictionary(x => x.ForeignArtistId, x => x); var artist = _artistService.FindById(resource.ArtistId); if (artist == null) { artist = new Artist(); artist.Metadata = artists[resource.ArtistId]; } var album = _albumService.FindById(resource.Id) ?? MapAlbum(resource, artists); album.Artist = artist; album.ArtistMetadata = artist.Metadata.Value; if (!album.AlbumReleases.Value.Any()) { return null; } return album; } private object MapSearchResult(EntityResource resource) { if (resource.Artist != null) { return MapSearchResult(resource.Artist); } else { return MapSearchResult(resource.Album); } } private static Album MapAlbum(AlbumResource resource, Dictionary artistDict) { Album album = new Album(); album.ForeignAlbumId = resource.Id; album.OldForeignAlbumIds = resource.OldIds; album.Title = resource.Title; album.Overview = resource.Overview; album.Disambiguation = resource.Disambiguation; album.ReleaseDate = resource.ReleaseDate; if (resource.Images != null) { album.Images = resource.Images.Select(MapImage).ToList(); } album.AlbumType = resource.Type; album.SecondaryTypes = resource.SecondaryTypes.Select(MapSecondaryTypes).ToList(); album.Ratings = MapRatings(resource.Rating); album.Links = resource.Links?.Select(MapLink).ToList(); album.Genres = resource.Genres; album.CleanTitle = Parser.Parser.CleanArtistName(album.Title); if (resource.Releases != null) { album.AlbumReleases = resource.Releases.Select(x => MapRelease(x, artistDict)).Where(x => x.TrackCount > 0).ToList(); // Monitor the release with most tracks var mostTracks = album.AlbumReleases.Value.OrderByDescending(x => x.TrackCount).FirstOrDefault(); if (mostTracks != null) { mostTracks.Monitored = true; } } else { album.AlbumReleases = new List(); } album.AnyReleaseOk = true; return album; } private static AlbumRelease MapRelease(ReleaseResource resource, Dictionary artistDict) { AlbumRelease release = new AlbumRelease(); release.ForeignReleaseId = resource.Id; release.OldForeignReleaseIds = resource.OldIds; release.Title = resource.Title; release.Status = resource.Status; release.Label = resource.Label; release.Disambiguation = resource.Disambiguation; release.Country = resource.Country; release.ReleaseDate = resource.ReleaseDate; // Get the complete set of media/tracks returned by the API, adding missing media if necessary var allMedia = resource.Media.Select(MapMedium).ToList(); var allTracks = resource.Tracks.Select(x => MapTrack(x, artistDict)); if (!allMedia.Any()) { foreach (int n in allTracks.Select(x => x.MediumNumber).Distinct()) { allMedia.Add(new Medium { Name = "Unknown", Number = n, Format = "Unknown" }); } } // Skip non-audio media var audioMediaNumbers = allMedia.Where(x => !NonAudioMedia.Contains(x.Format)).Select(x => x.Number); // Get tracks on the audio media and omit any that are skipped release.Tracks = allTracks.Where(x => audioMediaNumbers.Contains(x.MediumNumber) && !SkippedTracks.Contains(x.Title)).ToList(); release.TrackCount = release.Tracks.Value.Count; // Only include the media that contain the tracks we have selected var usedMediaNumbers = release.Tracks.Value.Select(track => track.MediumNumber); release.Media = allMedia.Where(medium => usedMediaNumbers.Contains(medium.Number)).ToList(); release.Duration = release.Tracks.Value.Sum(x => x.Duration); return release; } private static Medium MapMedium(MediumResource resource) { Medium medium = new Medium { Name = resource.Name, Number = resource.Position, Format = resource.Format }; return medium; } private static Track MapTrack(TrackResource resource, Dictionary artistDict) { Track track = new Track { ArtistMetadata = artistDict[resource.ArtistId], Title = resource.TrackName, ForeignTrackId = resource.Id, OldForeignTrackIds = resource.OldIds, ForeignRecordingId = resource.RecordingId, OldForeignRecordingIds = resource.OldRecordingIds, TrackNumber = resource.TrackNumber, AbsoluteTrackNumber = resource.TrackPosition, Duration = resource.DurationMs, MediumNumber = resource.MediumNumber }; return track; } private static ArtistMetadata MapArtistMetadata(ArtistResource resource) { ArtistMetadata artist = new ArtistMetadata(); artist.Name = resource.ArtistName; artist.Aliases = resource.ArtistAliases; artist.ForeignArtistId = resource.Id; artist.OldForeignArtistIds = resource.OldIds; artist.Genres = resource.Genres; artist.Overview = resource.Overview; artist.Disambiguation = resource.Disambiguation; artist.Type = resource.Type; artist.Status = MapArtistStatus(resource.Status); artist.Ratings = MapRatings(resource.Rating); artist.Images = resource.Images?.Select(MapImage).ToList(); artist.Links = resource.Links?.Select(MapLink).ToList(); return artist; } private static ArtistStatusType MapArtistStatus(string status) { if (status == null) { return ArtistStatusType.Continuing; } if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) { return ArtistStatusType.Ended; } return ArtistStatusType.Continuing; } private static Ratings MapRatings(RatingResource rating) { if (rating == null) { return new Ratings(); } return new Ratings { Votes = rating.Count, Value = rating.Value }; } private static MediaCover.MediaCover MapImage(ImageResource arg) { return new MediaCover.MediaCover { Url = arg.Url, CoverType = MapCoverType(arg.CoverType) }; } private static Links MapLink(LinkResource arg) { return new Links { Url = arg.Target, Name = arg.Type }; } private static MediaCoverTypes MapCoverType(string coverType) { switch (coverType.ToLower()) { case "poster": return MediaCoverTypes.Poster; case "banner": return MediaCoverTypes.Banner; case "fanart": return MediaCoverTypes.Fanart; case "cover": return MediaCoverTypes.Cover; case "disc": return MediaCoverTypes.Disc; case "logo": return MediaCoverTypes.Logo; default: return MediaCoverTypes.Unknown; } } public static SecondaryAlbumType MapSecondaryTypes(string albumType) { switch (albumType.ToLowerInvariant()) { case "compilation": return SecondaryAlbumType.Compilation; case "soundtrack": return SecondaryAlbumType.Soundtrack; case "spokenword": return SecondaryAlbumType.Spokenword; case "interview": return SecondaryAlbumType.Interview; case "audiobook": return SecondaryAlbumType.Audiobook; case "live": return SecondaryAlbumType.Live; case "remix": return SecondaryAlbumType.Remix; case "dj-mix": return SecondaryAlbumType.DJMix; case "mixtape/street": return SecondaryAlbumType.Mixtape; case "demo": return SecondaryAlbumType.Demo; case "audio drama": return SecondaryAlbumType.Audiodrama; default: return SecondaryAlbumType.Studio; } } } }