using Mapster; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Roadie.Library; using Roadie.Library.Caching; using Roadie.Library.Configuration; using Roadie.Library.Encoding; using Roadie.Library.Enums; using Roadie.Library.Identity; using Roadie.Library.Imaging; using Roadie.Library.Models; using Roadie.Library.Models.Users; using Roadie.Library.SearchEngines.Imaging; using Roadie.Library.Utility; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using data = Roadie.Library.Data; namespace Roadie.Api.Services { public class ImageService : ServiceBase, IImageService { private IImageSearchEngine BingSearchEngine { get; } private IDefaultNotFoundImages DefaultNotFoundImages { get; } private IImageSearchEngine ITunesSearchEngine { get; } private string Referrer { get; } private string RequestIp { get; } public ImageService(IRoadieSettings configuration, IHttpEncoder httpEncoder, IHttpContext httpContext, data.IRoadieDbContext context, ICacheManager cacheManager, ILogger logger, IDefaultNotFoundImages defaultNotFoundImages) : base(configuration, httpEncoder, context, cacheManager, logger, httpContext) { DefaultNotFoundImages = defaultNotFoundImages; BingSearchEngine = new BingImageSearchEngine(configuration, logger, RequestIp, Referrer); ITunesSearchEngine = new ITunesSearchEngine(configuration, cacheManager, logger, RequestIp, Referrer); } public async Task> ArtistImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("ArtistImage", data.Artist.CacheRegionUrn(id), id, width, height, async () => { return await ArtistImageAction(id, etag); }, etag); } public async Task> ArtistSecondaryImage(Guid id, int imageId, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation($"ArtistSecondaryThumbnail-{imageId}", data.Release.CacheRegionUrn(id), id, width, height, async () => { return await ArtistSecondaryImageAction(id, imageId, etag); }, etag); } public async Task> ById(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("ImageById", data.Image.CacheRegionUrn(id), id, width, height, async () => { return await ImageByIdAction(id, etag); }, etag); } public async Task> CollectionImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("CollectionThumbnail", data.Collection.CacheRegionUrn(id), id, width, height, async () => { return await CollectionImageAction(id, etag); }, etag); } public async Task> Delete(User user, Guid id) { var sw = Stopwatch.StartNew(); var image = DbContext.Images .Include("Release") .Include("Artist") .FirstOrDefault(x => x.RoadieId == id); if (image == null) return new OperationResult(true, string.Format("Image Not Found [{0}]", id)); if (image.ArtistId.HasValue) CacheManager.ClearRegion(data.Artist.CacheRegionUrn(image.Artist.RoadieId)); if (image.ReleaseId.HasValue) CacheManager.ClearRegion(data.Release.CacheRegionUrn(image.Release.RoadieId)); DbContext.Images.Remove(image); await DbContext.SaveChangesAsync(); CacheManager.ClearRegion(data.Image.CacheRegionUrn(id)); Logger.LogInformation($"Deleted Image [{id}], By User [{user}]"); sw.Stop(); return new OperationResult { Data = true, IsSuccess = true, OperationTime = sw.ElapsedMilliseconds }; } public async Task>> ImageProvidersSearch(string query) { var sw = new Stopwatch(); sw.Start(); var errors = new List(); IEnumerable searchResults = null; try { var manager = new ImageSearchManager(Configuration, CacheManager, Logger); searchResults = await manager.ImageSearch(query); } catch (Exception ex) { Logger.LogError(ex); errors.Add(ex); } sw.Stop(); return new OperationResult> { Data = searchResults, IsSuccess = !errors.Any(), OperationTime = sw.ElapsedMilliseconds, Errors = errors }; } public async Task> LabelImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("LabelThumbnail", data.Label.CacheRegionUrn(id), id, width, height, async () => { return await LabelImageAction(id, etag); }, etag); } public async Task> PlaylistImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("PlaylistThumbnail", data.Playlist.CacheRegionUrn(id), id, width, height, async () => { return await PlaylistImageAction(id, etag); }, etag); } public async Task> ReleaseImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("ReleaseThumbnail", data.Release.CacheRegionUrn(id), id, width, height, async () => { return await ReleaseImageAction(id, etag); }, etag); } public async Task> ReleaseSecondaryImage(Guid id, int imageId, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation($"ReleaseSecondaryThumbnail-{imageId}", data.Release.CacheRegionUrn(id), id, width, height, async () => { return await ReleaseSecondaryImageAction(id, imageId, etag); }, etag); } public async Task>> Search(string query, int resultsCount = 10) { var sw = Stopwatch.StartNew(); var result = new List(); if (WebHelper.IsStringUrl(query)) { var s = ImageHelper.ImageSearchResultForImageUrl(query); if (s != null) result.Add(s); } var bingResults = await BingSearchEngine.PerformImageSearch(query, resultsCount); if (bingResults != null) result.AddRange(bingResults); var iTunesResults = await ITunesSearchEngine.PerformImageSearch(query, resultsCount); if (iTunesResults != null) result.AddRange(iTunesResults); sw.Stop(); return new OperationResult> { IsSuccess = true, Data = result, OperationTime = sw.ElapsedMilliseconds }; } public async Task> TrackImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("TrackThumbnail", data.Track.CacheRegionUrn(id), id, width, height, async () => { return await TrackImageAction(id, width, height, etag); }, etag); } public async Task> UserImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("UserById", ApplicationUser.CacheRegionUrn(id), id, width, height, async () => { return await UserImageAction(id, etag); }, etag); } private Task> ArtistImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var artist = GetArtist(id); if (artist == null) return Task.FromResult(new FileOperationResult(true, string.Format("Artist Not Found [{0}]", id))); byte[] imageBytes = null; string artistFolder = null; try { // See if artist images exists in artist folder artistFolder = artist.ArtistFileFolder(Configuration, Configuration.LibraryFolder); if (!Directory.Exists(artistFolder)) { Logger.LogWarning($"Artist Folder [{artistFolder}], Not Found For Artist `{artist}`"); } else { var artistImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistFolder), ImageType.Artist); if (artistImages.Any()) imageBytes = File.ReadAllBytes(artistImages.First().FullName); } } catch (Exception ex) { Logger.LogError(ex, $"Error Reading Folder [{artistFolder}] For Artist [{artist.Id}]"); } imageBytes = imageBytes ?? artist.Thumbnail; var image = new data.Image { Bytes = imageBytes, CreatedDate = artist.CreatedDate, LastUpdated = artist.LastUpdated }; if (imageBytes == null || !imageBytes.Any()) image = DefaultNotFoundImages.Artist; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Artist Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> ArtistSecondaryImageAction(Guid id, int imageId, EntityTagHeaderValue etag = null) { try { var artist = GetArtist(id); if (artist == null) return Task.FromResult(new FileOperationResult(true, string.Format("Release Not Found [{0}]", id))); byte[] imageBytes = null; string artistFolder = null; try { // See if cover art file exists in release folder artistFolder = artist.ArtistFileFolder(Configuration, Configuration.LibraryFolder); if (!Directory.Exists(artistFolder)) { Logger.LogWarning($"Artist Folder [{artistFolder}], Not Found For Artist `{artist}`"); } else { var artistSecondaryImages = ImageHelper .FindImageTypeInDirectory(new DirectoryInfo(artistFolder), ImageType.ArtistSecondary) .ToArray(); if (artistSecondaryImages.Length >= imageId && artistSecondaryImages[imageId] != null) imageBytes = File.ReadAllBytes(artistSecondaryImages[imageId].FullName); } } catch (Exception ex) { Logger.LogError(ex, $"Error Reading Artist Folder [{artistFolder}] For Artist `{artist}`"); } var image = new data.Image { Bytes = imageBytes, CreatedDate = artist.CreatedDate, LastUpdated = artist.LastUpdated }; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Release Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> CollectionImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var collection = GetCollection(id); if (collection == null) return Task.FromResult(new FileOperationResult(true, string.Format("Collection Not Found [{0}]", id))); var image = new data.Image { Bytes = collection.Thumbnail, CreatedDate = collection.CreatedDate, LastUpdated = collection.LastUpdated }; if (collection.Thumbnail == null || !collection.Thumbnail.Any()) image = DefaultNotFoundImages.Collection; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Collection Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private FileOperationResult GenerateFileOperationResult(Guid id, data.Image image, EntityTagHeaderValue etag = null, string contentType = "image/jpeg") { var imageEtag = EtagHelper.GenerateETag(HttpEncoder, image.Bytes); if (EtagHelper.CompareETag(HttpEncoder, etag, imageEtag)) return new FileOperationResult(OperationMessages.NotModified); if (!image?.Bytes?.Any() ?? false) return new FileOperationResult(string.Format("ImageById Not Set [{0}]", id)); return new FileOperationResult(image?.Bytes?.Any() ?? false ? OperationMessages.OkMessage : OperationMessages.NoImageDataFound) { IsSuccess = true, Data = image.Adapt(), ContentType = contentType, LastModified = image.LastUpdated ?? image.CreatedDate, ETag = imageEtag }; } private async Task> GetImageFileOperation(string type, string regionUrn, Guid id, int? width, int? height, Func>> action, EntityTagHeaderValue etag = null) { try { var sw = Stopwatch.StartNew(); var result = (await CacheManager.GetAsync($"urn:{type}_by_id_operation:{id}", action, regionUrn)) .Adapt>(); if (!result.IsSuccess) return new FileOperationResult(result.IsNotFoundResult, result.Messages); if (result.ETag == etag) return new FileOperationResult(OperationMessages.NotModified); if ((width.HasValue || height.HasValue) && result?.Data?.Bytes != null) { result.Data.Bytes = ImageHelper.ResizeImage(result?.Data?.Bytes, width.Value, height.Value); result.ETag = EtagHelper.GenerateETag(HttpEncoder, result.Data.Bytes); result.LastModified = DateTime.UtcNow; if (width.Value != Configuration.ThumbnailImageSize.Width || height.Value != Configuration.ThumbnailImageSize.Height) Logger.LogTrace($"{type}: Resized [{id}], Width [{width.Value}], Height [{height.Value}]"); } sw.Stop(); return new FileOperationResult(result.Messages) { Data = result.Data, ETag = result.ETag, LastModified = result.LastModified, ContentType = result.ContentType, Errors = result?.Errors, IsSuccess = result?.IsSuccess ?? false, OperationTime = sw.ElapsedMilliseconds }; } catch (Exception ex) { Logger.LogError(ex, $"GetImageFileOperation Error, Type [{type}], id [{id}]"); } return new FileOperationResult("System Error"); } private Task> ImageByIdAction(Guid id, EntityTagHeaderValue etag = null) { try { var image = DbContext.Images .Include("Release") .Include("Artist") .FirstOrDefault(x => x.RoadieId == id); if (image == null) return Task.FromResult(new FileOperationResult(true, string.Format("ImageById Not Found [{0}]", id))); return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Image [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> LabelImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var label = GetLabel(id); if (label == null) return Task.FromResult(new FileOperationResult(true, string.Format("Label Not Found [{0}]", id))); var image = new data.Image { Bytes = label.Thumbnail, CreatedDate = label.CreatedDate, LastUpdated = label.LastUpdated }; if (label.Thumbnail == null || !label.Thumbnail.Any()) image = DefaultNotFoundImages.Label; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Label Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> PlaylistImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var playlist = GetPlaylist(id); if (playlist == null) return Task.FromResult(new FileOperationResult(true, string.Format("Playlist Not Found [{0}]", id))); var image = new data.Image { Bytes = playlist.Thumbnail, CreatedDate = playlist.CreatedDate, LastUpdated = playlist.LastUpdated }; if (playlist.Thumbnail == null || !playlist.Thumbnail.Any()) image = DefaultNotFoundImages.Playlist; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Playlist Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> ReleaseImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var release = GetRelease(id); if (release == null) return Task.FromResult(new FileOperationResult(true, string.Format("Release Not Found [{0}]", id))); byte[] imageBytes = null; string artistFolder = null; string releaseFolder = null; try { // See if cover art file exists in release folder artistFolder = release.Artist.ArtistFileFolder(Configuration, Configuration.LibraryFolder); if (!Directory.Exists(artistFolder)) { Logger.LogWarning($"Artist Folder [{artistFolder}], Not Found For Artist `{release.Artist}`"); } else { releaseFolder = release.ReleaseFileFolder(artistFolder); if (!Directory.Exists(releaseFolder)) { Logger.LogWarning($"Release Folder [{releaseFolder}], Not Found For Release `{release}`"); } else { var releaseCoverFiles = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(releaseFolder), ImageType.Release); if (releaseCoverFiles.Any()) imageBytes = File.ReadAllBytes(releaseCoverFiles.First().FullName); } } } catch (Exception ex) { Logger.LogError(ex, $"Error Reading Release Folder [{releaseFolder}] Artist Folder [{artistFolder}] For Artist `{release.Artist.Id}`"); } imageBytes = imageBytes ?? release.Thumbnail; var image = new data.Image { Bytes = imageBytes, CreatedDate = release.CreatedDate, LastUpdated = release.LastUpdated }; if (release.Thumbnail == null || !release.Thumbnail.Any()) image = DefaultNotFoundImages.Release; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Release Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private Task> ReleaseSecondaryImageAction(Guid id, int imageId, EntityTagHeaderValue etag = null) { try { var release = GetRelease(id); if (release == null) return Task.FromResult(new FileOperationResult(true, string.Format("Release Not Found [{0}]", id))); byte[] imageBytes = null; string artistFolder = null; string releaseFolder = null; try { // See if cover art file exists in release folder artistFolder = release.Artist.ArtistFileFolder(Configuration, Configuration.LibraryFolder); if (!Directory.Exists(artistFolder)) { Logger.LogWarning($"Artist Folder [{artistFolder}], Not Found For Artist `{release.Artist}`"); } else { releaseFolder = release.ReleaseFileFolder(artistFolder); if (!Directory.Exists(releaseFolder)) { Logger.LogWarning($"Release Folder [{releaseFolder}], Not Found For Release `{release}`"); } else { var releaseSecondaryImages = ImageHelper .FindImageTypeInDirectory(new DirectoryInfo(releaseFolder), ImageType.ReleaseSecondary) .ToArray(); if (releaseSecondaryImages.Length >= imageId && releaseSecondaryImages[imageId] != null) imageBytes = File.ReadAllBytes(releaseSecondaryImages[imageId].FullName); } } } catch (Exception ex) { Logger.LogError(ex, $"Error Reading Release Folder [{releaseFolder}] Artist Folder [{artistFolder}] For Artist `{release.Artist.Id}`"); } var image = new data.Image { Bytes = imageBytes, CreatedDate = release.CreatedDate, LastUpdated = release.LastUpdated }; return Task.FromResult(GenerateFileOperationResult(id, image, etag)); } catch (Exception ex) { Logger.LogError($"Error fetching Release Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } private async Task> TrackImageAction(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { try { var track = GetTrack(id); if (track == null) return new FileOperationResult(true, string.Format("Track Not Found [{0}]", id)); var imageBytes = track.Thumbnail; var trackThumbnailImages = ImageHelper.FindImageTypeInDirectory( new DirectoryInfo(track.PathToTrackThumbnail(Configuration, Configuration.LibraryFolder)), ImageType.Track, SearchOption.TopDirectoryOnly); if (trackThumbnailImages.Any()) imageBytes = File.ReadAllBytes(trackThumbnailImages.First().FullName); var image = new data.Image { Bytes = track.Thumbnail, CreatedDate = track.CreatedDate, LastUpdated = track.LastUpdated }; if (track.Thumbnail == null || !track.Thumbnail.Any()) // If no track image is found then return image for release return await ReleaseImage(track.ReleaseMedia.Release.RoadieId, width, height, etag); return GenerateFileOperationResult(id, image, etag); } catch (Exception ex) { Logger.LogError($"Error fetching Track Thumbnail [{id}]", ex); } return new FileOperationResult(OperationMessages.ErrorOccured); } private Task> UserImageAction(Guid id, EntityTagHeaderValue etag = null) { try { var user = GetUser(id); if (user == null) return Task.FromResult(new FileOperationResult(true, string.Format("User Not Found [{0}]", id))); var image = new data.Image { Bytes = user.Avatar, CreatedDate = user.CreatedDate.Value, LastUpdated = user.LastUpdated }; if (user.Avatar == null || !user.Avatar.Any()) image = DefaultNotFoundImages.User; return Task.FromResult(GenerateFileOperationResult(id, image, etag, "image/png")); } catch (Exception ex) { Logger.LogError($"Error fetching User Thumbnail [{id}]", ex); } return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } } }