From 85dd7352c53ac81da2edd6ad99a1b2e72a35dd26 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Mon, 27 Jan 2020 21:44:13 -0600 Subject: [PATCH] Performance improvements via Roslynator. --- .../Imaging/ITunesSearchEngine.cs | 4 +- .../MetaData/Discogs/DiscogsHelper.cs | 6 +- .../MetaData/LastFm/LastFmHelper.cs | 4 +- .../MetaData/Spotify/SpotifyHelper.cs | 6 +- Roadie.Api.Services/ArtistService.cs | 420 +++++++------- Roadie.Api.Services/ReleaseService.cs | 523 +++++++++--------- Roadie.Api.Services/StatisticsService.cs | 7 +- Roadie.Api.Services/TrackService.cs | 233 ++++---- Roadie.Api.Services/UserService.cs | 352 ++++++------ Roadie.Api/wwwroot/Images/Roadie.png | Bin 0 -> 104943 bytes 10 files changed, 761 insertions(+), 794 deletions(-) create mode 100644 Roadie.Api/wwwroot/Images/Roadie.png diff --git a/Roadie.Api.Library/SearchEngines/Imaging/ITunesSearchEngine.cs b/Roadie.Api.Library/SearchEngines/Imaging/ITunesSearchEngine.cs index 2f13ab0..42a50b2 100644 --- a/Roadie.Api.Library/SearchEngines/Imaging/ITunesSearchEngine.cs +++ b/Roadie.Api.Library/SearchEngines/Imaging/ITunesSearchEngine.cs @@ -32,7 +32,7 @@ namespace Roadie.Library.SearchEngines.Imaging try { var request = BuildRequest(query, 1, "musicArtist"); - var response = await _client.ExecuteTaskAsync(request); + var response = await _client.ExecuteAsync(request); if (response.ResponseStatus == ResponseStatus.Error) { if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -116,7 +116,7 @@ namespace Roadie.Library.SearchEngines.Imaging public async Task>> PerformReleaseSearch(string artistName, string query, int resultsCount) { var request = BuildRequest(query, 1, "album"); - var response = await _client.ExecuteTaskAsync(request); + var response = await _client.ExecuteAsync(request); if (response.ResponseStatus == ResponseStatus.Error) { if (response.StatusCode == HttpStatusCode.Unauthorized) diff --git a/Roadie.Api.Library/SearchEngines/MetaData/Discogs/DiscogsHelper.cs b/Roadie.Api.Library/SearchEngines/MetaData/Discogs/DiscogsHelper.cs index 1e1b182..9d6d832 100644 --- a/Roadie.Api.Library/SearchEngines/MetaData/Discogs/DiscogsHelper.cs +++ b/Roadie.Api.Library/SearchEngines/MetaData/Discogs/DiscogsHelper.cs @@ -40,7 +40,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Discogs UserAgent = WebHelper.UserAgent }; - var response = await client.ExecuteTaskAsync(request); + var response = await client.ExecuteAsync(request); if (response.ResponseStatus == ResponseStatus.Error) { @@ -126,7 +126,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Discogs var client = new RestClient("https://api.discogs.com/database"); client.UserAgent = WebHelper.UserAgent; - var response = await client.ExecuteTaskAsync(request); + var response = await client.ExecuteAsync(request); if (response.ResponseStatus == ResponseStatus.Error) { @@ -207,7 +207,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Discogs Timeout = SafeParser.ToNumber(Configuration.Integrations.DiscogsTimeout) }; - var response = await client.ExecuteTaskAsync(request); + var response = await client.ExecuteAsync(request); if (response?.ResponseStatus == null || response.ResponseStatus == ResponseStatus.Error) { if (response.StatusCode == HttpStatusCode.Unauthorized) diff --git a/Roadie.Api.Library/SearchEngines/MetaData/LastFm/LastFmHelper.cs b/Roadie.Api.Library/SearchEngines/MetaData/LastFm/LastFmHelper.cs index 9e639ad..4c7706a 100644 --- a/Roadie.Api.Library/SearchEngines/MetaData/LastFm/LastFmHelper.cs +++ b/Roadie.Api.Library/SearchEngines/MetaData/LastFm/LastFmHelper.cs @@ -86,7 +86,7 @@ namespace Roadie.Library.MetaData.LastFm }; var request = new RestRequest(Method.GET); var client = new RestClient(BuildUrl("auth.getSession", parameters)); - var responseXML = await client.ExecuteTaskAsync(request); + var responseXML = await client.ExecuteAsync(request); var doc = new XmlDocument(); doc.LoadXml(responseXML.Content); var sessionKey = doc.GetElementsByTagName("key")[0].InnerText; @@ -205,7 +205,7 @@ namespace Roadie.Library.MetaData.LastFm { var request = new RestRequest(Method.GET); var client = new RestClient(string.Format("http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={0}&artist={1}&album={2}&format=xml", ApiKey.Key, artistName, query)); - var responseData = await client.ExecuteTaskAsync(request); + var responseData = await client.ExecuteAsync(request); ReleaseSearchResult result = null; diff --git a/Roadie.Api.Library/SearchEngines/MetaData/Spotify/SpotifyHelper.cs b/Roadie.Api.Library/SearchEngines/MetaData/Spotify/SpotifyHelper.cs index e7435d1..59abe23 100644 --- a/Roadie.Api.Library/SearchEngines/MetaData/Spotify/SpotifyHelper.cs +++ b/Roadie.Api.Library/SearchEngines/MetaData/Spotify/SpotifyHelper.cs @@ -37,7 +37,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Spotify var client = new RestClient("http://api.spotify.com/v1"); client.UserAgent = WebHelper.UserAgent; - var response = await client.ExecuteTaskAsync(request); + var response = await client.ExecuteAsync(request); if (response.ResponseStatus == ResponseStatus.Error) { @@ -104,7 +104,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Spotify } var client = new RestClient(string.Format("http://api.spotify.com/v1/albums/{0}", spotifyAlbum.id)); - var albumResult = await client.ExecuteTaskAsync(request); + var albumResult = await client.ExecuteAsync(request); if (albumResult != null && albumResult.Data != null) { var sa = albumResult.Data; @@ -213,7 +213,7 @@ namespace Roadie.Library.SearchEngines.MetaData.Spotify var client = new RestClient(string.Format( "http://api.spotify.com/v1/artists/{0}/albums?offset=0&limit=25&album_type=album&market=US", spotifyId)); - var artistAlbumsResponse = await client.ExecuteTaskAsync(request); + var artistAlbumsResponse = await client.ExecuteAsync(request); result = artistAlbumsResponse != null && artistAlbumsResponse.Data != null ? artistAlbumsResponse.Data : null; diff --git a/Roadie.Api.Services/ArtistService.cs b/Roadie.Api.Services/ArtistService.cs index c6bf054..ae7660d 100644 --- a/Roadie.Api.Services/ArtistService.cs +++ b/Roadie.Api.Services/ArtistService.cs @@ -10,7 +10,6 @@ using Roadie.Library.Encoding; using Roadie.Library.Engines; using Roadie.Library.Enums; using Roadie.Library.Extensions; -using Roadie.Library.Identity; using Roadie.Library.Imaging; using Roadie.Library.MetaData.Audio; using Roadie.Library.Models; @@ -71,7 +70,7 @@ namespace Roadie.Api.Services FileDirectoryProcessorService = fileDirectoryProcessorService; } - public async Task> ById(Library.Models.Users.User roadieUser, Guid id, IEnumerable includes) + public async Task> ById(User roadieUser, Guid id, IEnumerable includes) { var timings = new Dictionary(); var tsw = new Stopwatch(); @@ -82,19 +81,19 @@ namespace Roadie.Api.Services var result = await CacheManager.GetAsync(cacheKey, async () => { tsw.Restart(); - var rr = await ArtistByIdAction(id, includes); + var rr = await ArtistByIdAction(id, includes).ConfigureAwait(false); tsw.Stop(); timings.Add("ArtistByIdAction", tsw.ElapsedMilliseconds); return rr; - }, data.Artist.CacheRegionUrn(id)); + }, data.Artist.CacheRegionUrn(id)).ConfigureAwait(false); if (result?.Data != null && roadieUser != null) { tsw.Restart(); - var artist = await GetArtist(id); + var artist = await GetArtist(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getArtist", tsw.ElapsedMilliseconds); tsw.Restart(); - var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Artist); + var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Artist).ConfigureAwait(false); if (userBookmarkResult.IsSuccess) { result.Data.UserBookmarked = userBookmarkResult?.Rows?.FirstOrDefault(x => x?.Bookmark?.Value == artist?.RoadieId.ToString()) != null; @@ -125,7 +124,7 @@ namespace Roadie.Api.Services select cr).ToArray(); foreach (var comment in result.Data.Comments) { - var userCommentReaction = userCommentReactions.FirstOrDefault(x => x.CommentId == comment.DatabaseId); + var userCommentReaction = Array.Find(userCommentReactions, x => x.CommentId == comment.DatabaseId); comment.IsDisliked = userCommentReaction?.ReactionValue == CommentReaction.Dislike; comment.IsLiked = userCommentReaction?.ReactionValue == CommentReaction.Like; } @@ -155,10 +154,10 @@ namespace Roadie.Api.Services if (artist != null) { DbContext.Artists.Remove(artist); - await DbContext.SaveChangesAsync(); - if(deleteFolder) + await DbContext.SaveChangesAsync().ConfigureAwait(false); + if (deleteFolder) { - // Delete all image files for Artist + // Delete all image files for Artist foreach (var file in ImageHelper.ImageFilesInFolder(artist.ArtistFileFolder(Configuration), SearchOption.TopDirectoryOnly)) { try @@ -175,8 +174,8 @@ namespace Roadie.Api.Services var artistDir = new DirectoryInfo(artist.ArtistFileFolder(Configuration)); FolderPathHelper.DeleteEmptyFolders(artistDir.Parent); } - await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Artist, artist.Id); - await UpdatePlaylistCountsForArtist(artist.Id, DateTime.UtcNow); + await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Artist, artist.Id).ConfigureAwait(false); + await UpdatePlaylistCountsForArtist(artist.Id, DateTime.UtcNow).ConfigureAwait(false); CacheManager.ClearRegion(artist.CacheRegion); Logger.LogWarning("User `{0}` deleted Artist `{1}]`", user, artist); isSuccess = true; @@ -198,7 +197,7 @@ namespace Roadie.Api.Services }; } - public async Task> List(Library.Models.Users.User roadieUser, PagedRequest request, bool? doRandomize = false, bool? onlyIncludeWithReleases = true) + public async Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false, bool? onlyIncludeWithReleases = true) { var sw = new Stopwatch(); sw.Start(); @@ -255,7 +254,9 @@ namespace Roadie.Api.Services if (filter.StartsWith('"') && filter.EndsWith('"')) { isEqualFilter = true; +#pragma warning disable IDE0057 // Use range operator request.Filter = filter.Substring(1, filter.Length - 2); +#pragma warning restore IDE0057 // Use range operator } } var normalizedFilterValue = !string.IsNullOrEmpty(request.FilterValue) @@ -267,9 +268,9 @@ namespace Roadie.Api.Services if (doRandomize ?? false) { var randomLimit = request.Limit ?? roadieUser?.RandomReleaseLimit ?? request.LimitValue; - randomArtistData = await DbContext.RandomArtistIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly); + randomArtistData = await DbContext.RandomArtistIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly).ConfigureAwait(false); randomArtistIds = randomArtistData.Select(x => x.Value).ToArray(); - rowCount = DbContext.Artists.Count(); + rowCount = await DbContext.Artists.CountAsync().ConfigureAwait(false); } var result = (from a in DbContext.Artists where !onlyWithReleases || a.ReleaseCount > 0 @@ -314,11 +315,11 @@ namespace Roadie.Api.Services }); ArtistList[] rows; - rowCount = rowCount ?? result.Count(); + rowCount ??= result.Count(); if (doRandomize ?? false) { - var resultData = await result.ToArrayAsync(); + var resultData = await result.ToArrayAsync().ConfigureAwait(false); rows = (from r in resultData join ra in randomArtistData on r.DatabaseId equals ra.Value orderby ra.Key @@ -328,7 +329,7 @@ namespace Roadie.Api.Services else { string sortBy; - if (request.ActionValue == Library.Models.Users.User.ActionKeyUserRated) + if (request.ActionValue == User.ActionKeyUserRated) { sortBy = string.IsNullOrEmpty(request.Sort) ? request.OrderValue(new Dictionary { { "Rating", "DESC" }, { "Artist.Text", "ASC" } }) @@ -338,20 +339,20 @@ namespace Roadie.Api.Services { sortBy = request.OrderValue(new Dictionary { { "SortName", "ASC" }, { "Artist.Text", "ASC" } }); } - rows = await result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArrayAsync(); + rows = await result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArrayAsync().ConfigureAwait(false); } - if (rows.Any() && roadieUser != null) + if (rows.Length > 0 && roadieUser != null) { var rowIds = rows.Select(x => x.DatabaseId).ToArray(); var userArtistRatings = await (from ua in DbContext.UserArtists - where ua.UserId == roadieUser.Id - where rowIds.Contains(ua.ArtistId) - select ua).ToArrayAsync(); + where ua.UserId == roadieUser.Id + where rowIds.Contains(ua.ArtistId) + select ua).ToArrayAsync().ConfigureAwait(false); foreach (var userArtistRating in userArtistRatings.Where(x => rows.Select(r => r.DatabaseId).Contains(x.ArtistId))) { - var row = rows.FirstOrDefault(x => x.DatabaseId == userArtistRating.ArtistId); + var row = Array.Find(rows, x => x.DatabaseId == userArtistRating.ArtistId); if (row != null) { row.UserRating = new UserArtist @@ -364,19 +365,16 @@ namespace Roadie.Api.Services } } } - if (!string.IsNullOrEmpty(request.Filter) && rowCount == 0) + if (!string.IsNullOrEmpty(request.Filter) && rowCount == 0 && Configuration.RecordNoResultSearches) { - if (Configuration.RecordNoResultSearches) + // Create request for no artist found + var req = new data.Request { - // Create request for no artist found - var req = new data.Request - { - UserId = roadieUser?.Id, - Description = request.Filter - }; - DbContext.Requests.Add(req); - await DbContext.SaveChangesAsync(); - } + UserId = roadieUser?.Id, + Description = request.Filter + }; + DbContext.Requests.Add(req); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } sw.Stop(); return new Library.Models.Pagination.PagedResult @@ -392,9 +390,8 @@ namespace Roadie.Api.Services /// /// Merge one Artist into another one /// - /// The Artist to be merged - /// The Artist to merge into - /// + /// The Artist to be merged + /// The Artist to merge into public async Task> MergeArtists(Library.Identity.User user, Guid artistToMergeId, Guid artistToMergeIntoId) { var sw = new Stopwatch(); @@ -423,7 +420,7 @@ namespace Roadie.Api.Services try { - var result = await MergeArtists(user, artistToMerge, mergeIntoArtist); + var result = await MergeArtists(user, artistToMerge, mergeIntoArtist).ConfigureAwait(false); if (!result.IsSuccess) { CacheManager.ClearRegion(artistToMerge.CacheRegion); @@ -442,8 +439,8 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), - Data = !errors.Any(), + IsSuccess = errors.Count == 0, + Data = errors.Count == 0, OperationTime = sw.ElapsedMilliseconds, Errors = errors }; @@ -472,7 +469,7 @@ namespace Roadie.Api.Services artistSearch = await ArtistLookupEngine.PerformMetaDataProvidersArtistSearch(new AudioMetaData { Artist = artist.Name - }); + }).ConfigureAwait(false); } catch (Exception ex) { @@ -482,11 +479,11 @@ namespace Roadie.Api.Services if (artistSearch.IsSuccess) { // Do metadata search for Artist like if new Artist then set some overides and merge - var mergeResult = await MergeArtists(user, artistSearch.Data, artist); + var mergeResult = await MergeArtists(user, artistSearch.Data, artist).ConfigureAwait(false); if (mergeResult.IsSuccess) { artist = mergeResult.Data; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); sw.Stop(); CacheManager.ClearRegion(artist.CacheRegion); Logger.LogTrace("Scanned RefreshArtistMetadata [{0}], OperationTime [{1}]", @@ -502,6 +499,7 @@ namespace Roadie.Api.Services { Logger.LogError(ex, ex.Serialize()); resultErrors.Add(ex); + result = false; } return new OperationResult @@ -544,9 +542,10 @@ namespace Roadie.Api.Services if (artist.Releases != null) { foreach (var release in artist.Releases) + { try { - result = result && (await ReleaseService.ScanReleaseFolder(user, Guid.Empty, doJustInfo, release)).Data; + result = result && (await ReleaseService.ScanReleaseFolder(user, Guid.Empty, doJustInfo, release).ConfigureAwait(false)).Data; releaseScannedCount++; scannedArtistFolders.Add(release.ReleaseFileFolder(artistFolder)); } @@ -554,11 +553,12 @@ namespace Roadie.Api.Services { Logger.LogError(ex, ex.Serialize()); } + } } var artistImage = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistFolder), ImageType.Artist).FirstOrDefault(); if (artistImage != null) { - // See if image file is valid image if not delete it + // See if image file is valid image if not delete it if (ImageHelper.ConvertToJpegFormat(File.ReadAllBytes(artistImage.FullName)) == null) { artistImage.Delete(); @@ -571,7 +571,7 @@ namespace Roadie.Api.Services select d; foreach (var folder in nonReleaseFolders) { - await FileDirectoryProcessorService.Process(user, new DirectoryInfo(folder), doJustInfo); + await FileDirectoryProcessorService.Process(user, new DirectoryInfo(folder), doJustInfo).ConfigureAwait(false); } if (!doJustInfo) { @@ -596,10 +596,7 @@ namespace Roadie.Api.Services }; } - public async Task> SetReleaseImageByUrl(Library.Identity.User user, Guid id, string imageUrl) - { - return await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)); - } + public async Task> SetReleaseImageByUrl(Library.Identity.User user, Guid id, string imageUrl) => await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)).ConfigureAwait(false); public async Task> UpdateArtist(Library.Identity.User user, Artist model) { @@ -695,7 +692,7 @@ namespace Roadie.Api.Services File.WriteAllBytes(artistImageName, ImageHelper.ConvertToJpegFormat(artistImage)); } - if (model.NewSecondaryImagesData != null && model.NewSecondaryImagesData.Any()) + if (model.NewSecondaryImagesData?.Any() == true) { // Additional images to add to artist var looper = 0; @@ -721,7 +718,7 @@ namespace Roadie.Api.Services } } - if (model.Genres != null && model.Genres.Any()) + if (model.Genres?.Any() == true) { // Remove existing Genres not in model list foreach (var genre in artist.Genres.ToList()) @@ -739,21 +736,23 @@ namespace Roadie.Api.Services { var g = DbContext.Genres.FirstOrDefault(x => x.RoadieId == genreId); if (g != null) + { artist.Genres.Add(new data.ArtistGenre { ArtistId = artist.Id, GenreId = g.Id, Genre = g }); + } } } } - else if (model.Genres == null || !model.Genres.Any()) + else if (model.Genres?.Any() != true) { artist.Genres.Clear(); } - if (model.AssociatedArtistsTokens != null && model.AssociatedArtistsTokens.Any()) + if (model.AssociatedArtistsTokens?.Any() == true) { var associatedArtists = DbContext.ArtistAssociations.Include(x => x.AssociatedArtist) .Where(x => x.ArtistId == artist.Id).ToList(); @@ -776,21 +775,23 @@ namespace Roadie.Api.Services { var a = DbContext.Artists.FirstOrDefault(x => x.RoadieId == associatedArtistId); if (a != null) + { DbContext.ArtistAssociations.Add(new data.ArtistAssociation { ArtistId = artist.Id, AssociatedArtistId = a.Id }); + } } } } - else if (model.AssociatedArtistsTokens == null || !model.AssociatedArtistsTokens.Any()) + else if (model.AssociatedArtistsTokens?.Any() != true) { var associatedArtists = DbContext.ArtistAssociations.Include(x => x.AssociatedArtist).Where(x => x.ArtistId == artist.Id || x.AssociatedArtistId == artist.Id).ToList(); DbContext.ArtistAssociations.RemoveRange(associatedArtists); } - if (model.SimilarArtistsTokens != null && model.SimilarArtistsTokens.Any()) + if (model.SimilarArtistsTokens?.Any() == true) { var similarArtists = DbContext.ArtistSimilar.Include(x => x.SimilarArtist) .Where(x => x.ArtistId == artist.Id).ToList(); @@ -812,21 +813,23 @@ namespace Roadie.Api.Services { var a = DbContext.Artists.FirstOrDefault(x => x.RoadieId == similarArtistId); if (a != null) + { DbContext.ArtistSimilar.Add(new data.ArtistSimilar { ArtistId = artist.Id, SimilarArtistId = a.Id }); + } } } } - else if (model.SimilarArtistsTokens == null || !model.SimilarArtistsTokens.Any()) + else if (model.SimilarArtistsTokens?.Any() != true) { var similarArtists = DbContext.ArtistSimilar.Include(x => x.SimilarArtist).Where(x => x.ArtistId == artist.Id || x.SimilarArtistId == artist.Id).ToList(); DbContext.ArtistSimilar.RemoveRange(similarArtists); } artist.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); if (didRenameArtist) { // Many contributing artists do not have releases and will not have an empty Artist folder @@ -836,7 +839,7 @@ namespace Roadie.Api.Services foreach (var mp3 in Directory.GetFiles(newArtistFolder, "*.mp3", SearchOption.AllDirectories)) { var trackFileInfo = new FileInfo(mp3); - var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo); + var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo).ConfigureAwait(false); if (audioMetaData != null) { audioMetaData.Artist = artist.Name; @@ -844,7 +847,7 @@ namespace Roadie.Api.Services } } - await ScanArtistReleasesFolders(user, artist.RoadieId, Configuration.LibraryFolder, false); + await ScanArtistReleasesFolders(user, artist.RoadieId, Configuration.LibraryFolder, false).ConfigureAwait(false); } } @@ -861,8 +864,8 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), - Data = !errors.Any(), + IsSuccess = errors.Count == 0, + Data = errors.Count == 0, OperationTime = sw.ElapsedMilliseconds, Errors = errors }; @@ -877,7 +880,7 @@ namespace Roadie.Api.Services bytes = ms.ToArray(); } - return await SaveImageBytes(user, id, bytes); + return await SaveImageBytes(user, id, bytes).ConfigureAwait(false); } private async Task> ArtistByIdAction(Guid id, IEnumerable includes) @@ -889,14 +892,14 @@ namespace Roadie.Api.Services sw.Start(); tsw.Restart(); - var artist = await GetArtist(id); + var artist = await GetArtist(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getArtist", tsw.ElapsedMilliseconds); if (artist == null) return new OperationResult(true, $"Artist Not Found [{id}]"); tsw.Restart(); var result = artist.Adapt(); - result.BandStatus = result.BandStatus ?? BandStatus.Unknown.ToString(); + result.BandStatus ??= nameof(BandStatus.Unknown); result.BeginDate = result.BeginDate == null || result.BeginDate == DateTime.MinValue ? null : result.BeginDate; @@ -913,11 +916,10 @@ namespace Roadie.Api.Services result.Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, id); result.MediumThumbnail = ImageHelper.MakeThumbnailImage(Configuration, HttpContext, id, "artist", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height); - - if (includes != null && includes.Any()) + if (includes?.Any() == true) { tsw.Restart(); - if(includes.Contains("genres")) + if (includes.Contains("genres")) { var artistGenreIds = artist.Genres.Select(x => x.GenreId).ToArray(); result.Genres = (await (from g in DbContext.Genres @@ -928,7 +930,7 @@ namespace Roadie.Api.Services where rg.GenreId == g.Id select rg.Id).Count() where artistGenreIds.Contains(g.Id) - select new { g, releaseCount, artistCount }).ToListAsync()) + select new { g, releaseCount, artistCount }).ToListAsync().ConfigureAwait(false)) .Select(x => GenreList.FromDataGenre(x.g, ImageHelper.MakeGenreThumbnailImage(Configuration, HttpContext, x.g.RoadieId), x.artistCount, @@ -937,10 +939,8 @@ namespace Roadie.Api.Services tsw.Stop(); timings.Add("genres", tsw.ElapsedMilliseconds); - } - if (includes.Contains("releases")) { tsw.Restart(); @@ -950,12 +950,13 @@ namespace Roadie.Api.Services .Include("Medias.Tracks") .Include("Medias.Tracks") .Where(x => x.ArtistId == artist.Id) - .ToArrayAsync()) + .ToArrayAsync().ConfigureAwait(false)) { var releaseList = release.Adapt(); releaseList.Thumbnail = ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, release.RoadieId); var dtoReleaseMedia = new List(); if (includes.Contains("tracks")) + { foreach (var releasemedia in release.Medias.OrderBy(x => x.MediaNumber).ToArray()) { var dtoMedia = releasemedia.Adapt(); @@ -963,7 +964,7 @@ namespace Roadie.Api.Services foreach (var t in await DbContext.Tracks .Where(x => x.ReleaseMediaId == releasemedia.Id) .OrderBy(x => x.TrackNumber) - .ToArrayAsync()) + .ToArrayAsync().ConfigureAwait(false)) { var track = t.Adapt(); ArtistList trackArtist = null; @@ -979,6 +980,8 @@ namespace Roadie.Api.Services dtoMedia.Tracks = tracks; dtoReleaseMedia.Add(dtoMedia); } + } + releaseList.Media = dtoReleaseMedia; dtoReleases.Add(releaseList); } @@ -989,6 +992,7 @@ namespace Roadie.Api.Services } if (includes.Contains("stats")) + { try { tsw.Restart(); @@ -1027,6 +1031,7 @@ namespace Roadie.Api.Services { Logger.LogError(ex, $"Error Getting Statistics for Artist `{artist}`"); } + } if (includes.Contains("images")) { @@ -1035,7 +1040,7 @@ namespace Roadie.Api.Services var artistImagesInFolder = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistFolder), ImageType.ArtistSecondary, SearchOption.TopDirectoryOnly); if (artistImagesInFolder.Any()) { - result.Images = artistImagesInFolder.Select((x, i) => ImageHelper.MakeFullsizeSecondaryImage(Configuration, HttpContext, id, ImageType.ArtistSecondary, i)); + result.Images = artistImagesInFolder.Select((_, i) => ImageHelper.MakeFullsizeSecondaryImage(Configuration, HttpContext, id, ImageType.ArtistSecondary, i)); } tsw.Stop(); @@ -1046,52 +1051,52 @@ namespace Roadie.Api.Services { tsw.Restart(); var associatedWithArtists = await (from aa in DbContext.ArtistAssociations - join a in DbContext.Artists on aa.AssociatedArtistId equals a.Id - where aa.ArtistId == artist.Id - select new ArtistList - { - DatabaseId = a.Id, - Id = a.RoadieId, - Artist = new DataToken - { - Text = a.Name, - Value = a.RoadieId.ToString() - }, - Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), - Rating = a.Rating, - Rank = (double?)a.Rank, - CreatedDate = a.CreatedDate, - LastUpdated = a.LastUpdated, - LastPlayed = a.LastPlayed, - PlayedCount = a.PlayedCount, - ReleaseCount = a.ReleaseCount, - TrackCount = a.TrackCount, - SortName = a.SortName - }).ToArrayAsync(); + join a in DbContext.Artists on aa.AssociatedArtistId equals a.Id + where aa.ArtistId == artist.Id + select new ArtistList + { + DatabaseId = a.Id, + Id = a.RoadieId, + Artist = new DataToken + { + Text = a.Name, + Value = a.RoadieId.ToString() + }, + Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), + Rating = a.Rating, + Rank = (double?)a.Rank, + CreatedDate = a.CreatedDate, + LastUpdated = a.LastUpdated, + LastPlayed = a.LastPlayed, + PlayedCount = a.PlayedCount, + ReleaseCount = a.ReleaseCount, + TrackCount = a.TrackCount, + SortName = a.SortName + }).ToArrayAsync().ConfigureAwait(false); var associatedArtists = await (from aa in DbContext.ArtistAssociations - join a in DbContext.Artists on aa.ArtistId equals a.Id - where aa.AssociatedArtistId == artist.Id - select new ArtistList - { - DatabaseId = a.Id, - Id = a.RoadieId, - Artist = new DataToken - { - Text = a.Name, - Value = a.RoadieId.ToString() - }, - Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), - Rating = a.Rating, - Rank = (double?)a.Rank, - CreatedDate = a.CreatedDate, - LastUpdated = a.LastUpdated, - LastPlayed = a.LastPlayed, - PlayedCount = a.PlayedCount, - ReleaseCount = a.ReleaseCount, - TrackCount = a.TrackCount, - SortName = a.SortName - }).ToArrayAsync(); + join a in DbContext.Artists on aa.ArtistId equals a.Id + where aa.AssociatedArtistId == artist.Id + select new ArtistList + { + DatabaseId = a.Id, + Id = a.RoadieId, + Artist = new DataToken + { + Text = a.Name, + Value = a.RoadieId.ToString() + }, + Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), + Rating = a.Rating, + Rank = (double?)a.Rank, + CreatedDate = a.CreatedDate, + LastUpdated = a.LastUpdated, + LastPlayed = a.LastPlayed, + PlayedCount = a.PlayedCount, + ReleaseCount = a.ReleaseCount, + TrackCount = a.TrackCount, + SortName = a.SortName + }).ToArrayAsync().ConfigureAwait(false); result.AssociatedArtists = associatedArtists.Union(associatedWithArtists, new ArtistListComparer()).OrderBy(x => x.SortName); result.AssociatedArtistsTokens = result.AssociatedArtists.Select(x => x.Artist).ToArray(); tsw.Stop(); @@ -1102,52 +1107,52 @@ namespace Roadie.Api.Services { tsw.Restart(); var similarWithArtists = await (from aa in DbContext.ArtistSimilar - join a in DbContext.Artists on aa.SimilarArtistId equals a.Id - where aa.ArtistId == artist.Id - select new ArtistList - { - DatabaseId = a.Id, - Id = a.RoadieId, - Artist = new DataToken - { - Text = a.Name, - Value = a.RoadieId.ToString() - }, - Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), - Rating = a.Rating, - Rank = (double?)a.Rank, - CreatedDate = a.CreatedDate, - LastUpdated = a.LastUpdated, - LastPlayed = a.LastPlayed, - PlayedCount = a.PlayedCount, - ReleaseCount = a.ReleaseCount, - TrackCount = a.TrackCount, - SortName = a.SortName - }).ToArrayAsync(); + join a in DbContext.Artists on aa.SimilarArtistId equals a.Id + where aa.ArtistId == artist.Id + select new ArtistList + { + DatabaseId = a.Id, + Id = a.RoadieId, + Artist = new DataToken + { + Text = a.Name, + Value = a.RoadieId.ToString() + }, + Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), + Rating = a.Rating, + Rank = (double?)a.Rank, + CreatedDate = a.CreatedDate, + LastUpdated = a.LastUpdated, + LastPlayed = a.LastPlayed, + PlayedCount = a.PlayedCount, + ReleaseCount = a.ReleaseCount, + TrackCount = a.TrackCount, + SortName = a.SortName + }).ToArrayAsync().ConfigureAwait(false); var similarArtists = await (from aa in DbContext.ArtistSimilar - join a in DbContext.Artists on aa.ArtistId equals a.Id - where aa.SimilarArtistId == artist.Id - select new ArtistList - { - DatabaseId = a.Id, - Id = a.RoadieId, - Artist = new DataToken - { - Text = a.Name, - Value = a.RoadieId.ToString() - }, - Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), - Rating = a.Rating, - Rank = (double?)a.Rank, - CreatedDate = a.CreatedDate, - LastUpdated = a.LastUpdated, - LastPlayed = a.LastPlayed, - PlayedCount = a.PlayedCount, - ReleaseCount = a.ReleaseCount, - TrackCount = a.TrackCount, - SortName = a.SortName - }).ToArrayAsync(); + join a in DbContext.Artists on aa.ArtistId equals a.Id + where aa.SimilarArtistId == artist.Id + select new ArtistList + { + DatabaseId = a.Id, + Id = a.RoadieId, + Artist = new DataToken + { + Text = a.Name, + Value = a.RoadieId.ToString() + }, + Thumbnail = ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, a.RoadieId), + Rating = a.Rating, + Rank = (double?)a.Rank, + CreatedDate = a.CreatedDate, + LastUpdated = a.LastUpdated, + LastPlayed = a.LastPlayed, + PlayedCount = a.PlayedCount, + ReleaseCount = a.ReleaseCount, + TrackCount = a.TrackCount, + SortName = a.SortName + }).ToArrayAsync().ConfigureAwait(false); result.SimilarArtists = similarWithArtists.Union(similarArtists, new ArtistListComparer()).OrderBy(x => x.SortName); result.SimilarArtistsTokens = result.SimilarArtists.Select(x => x.Artist).ToArray(); tsw.Stop(); @@ -1161,7 +1166,7 @@ namespace Roadie.Api.Services { Limit = 100 }; - var r = await CollectionService.List(null, collectionPagedRequest, artistId: artist.RoadieId); + var r = await CollectionService.List(null, collectionPagedRequest, artistId: artist.RoadieId).ConfigureAwait(false); if (r.IsSuccess) result.CollectionsWithArtistReleases = r.Rows.ToArray(); tsw.Stop(); timings.Add("collections", tsw.ElapsedMilliseconds); @@ -1173,8 +1178,8 @@ namespace Roadie.Api.Services var artistComments = await DbContext.Comments.Include(x => x.User) .Where(x => x.ArtistId == artist.Id) .OrderByDescending(x => x.CreatedDate) - .ToArrayAsync(); - if (artistComments.Any()) + .ToArrayAsync().ConfigureAwait(false); + if (artistComments.Length > 0) { var comments = new List(); var commentIds = artistComments.Select(x => x.Id).ToArray(); @@ -1208,7 +1213,7 @@ namespace Roadie.Api.Services { FilterToArtistId = artist.RoadieId }; - var r = await PlaylistService.List(pg); + var r = await PlaylistService.List(pg).ConfigureAwait(false); if (r.IsSuccess) { result.PlaylistsWithArtistReleases = r.Rows.ToArray(); @@ -1230,10 +1235,10 @@ namespace Roadie.Api.Services if (artistContributingTracks?.Any() ?? false) { result.ArtistContributionReleases = (await (from r in DbContext.Releases.Include(x => x.Artist) - where artistContributingTracks.Contains(r.Id) - select r) + where artistContributingTracks.Contains(r.Id) + select r) .OrderBy(x => x.Title) - .ToArrayAsync()) + .ToArrayAsync().ConfigureAwait(false)) .Select(r => ReleaseList.FromDataRelease(r, r.Artist, HttpContext.BaseUrl, ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, r.Artist.RoadieId), ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, r.RoadieId))); result.ArtistContributionReleases = result.ArtistContributionReleases.Any() @@ -1248,12 +1253,12 @@ namespace Roadie.Api.Services { tsw.Restart(); result.ArtistLabels = (await (from l in DbContext.Labels - join rl in DbContext.ReleaseLabels on l.Id equals rl.LabelId - join r in DbContext.Releases on rl.ReleaseId equals r.Id - where r.ArtistId == artist.Id - orderby l.SortName - select LabelList.FromDataLabel(l, ImageHelper.MakeLabelThumbnailImage(Configuration, HttpContext, l.RoadieId))) - .ToArrayAsync()) + join rl in DbContext.ReleaseLabels on l.Id equals rl.LabelId + join r in DbContext.Releases on rl.ReleaseId equals r.Id + where r.ArtistId == artist.Id + orderby l.SortName + select LabelList.FromDataLabel(l, ImageHelper.MakeLabelThumbnailImage(Configuration, HttpContext, l.RoadieId))) + .ToArrayAsync().ConfigureAwait(false)) .GroupBy(x => x.Label.Value) .Select(x => x.First()) .OrderBy(x => x.SortName) @@ -1275,29 +1280,6 @@ namespace Roadie.Api.Services }; } - private OperationResult GetByExternalIds(string musicBrainzId = null, string iTunesId = null, string amgId = null, string spotifyId = null) - { - var sw = new Stopwatch(); - sw.Start(); - var artist = (from a in DbContext.Artists - where a.MusicBrainzId != null && musicBrainzId != null && a.MusicBrainzId == musicBrainzId || - a.ITunesId != null || iTunesId != null && a.ITunesId == iTunesId || a.AmgId != null || - amgId != null && a.AmgId == amgId || a.SpotifyId != null || - spotifyId != null && a.SpotifyId == spotifyId - select a).FirstOrDefault(); - sw.Stop(); - if (artist == null || !artist.IsValid) - Logger.LogTrace( - "ArtistFactory: Artist Not Found By External Ids: MusicbrainzId [{0}], iTunesIs [{1}], AmgId [{2}], SpotifyId [{3}]", - musicBrainzId, iTunesId, amgId, spotifyId); - return new OperationResult - { - IsSuccess = artist != null, - OperationTime = sw.ElapsedMilliseconds, - Data = artist - }; - } - private async Task> MergeArtists(Library.Identity.User user, data.Artist artistToMerge, data.Artist artistToMergeInto) { SimpleContract.Requires(artistToMerge != null, "Invalid Artist"); @@ -1312,21 +1294,21 @@ namespace Roadie.Api.Services var artistToMergeFolder = artistToMerge.ArtistFileFolder(Configuration); var artistToMergeIntoFolder = artistToMergeInto.ArtistFileFolder(Configuration); - artistToMergeInto.RealName = artistToMergeInto.RealName ?? artistToMerge.RealName; - artistToMergeInto.MusicBrainzId = artistToMergeInto.MusicBrainzId ?? artistToMerge.MusicBrainzId; - artistToMergeInto.ITunesId = artistToMergeInto.ITunesId ?? artistToMerge.ITunesId; - artistToMergeInto.AmgId = artistToMergeInto.AmgId ?? artistToMerge.AmgId; - artistToMergeInto.SpotifyId = artistToMergeInto.SpotifyId ?? artistToMerge.SpotifyId; - artistToMergeInto.Profile = artistToMergeInto.Profile ?? artistToMerge.Profile; - artistToMergeInto.BirthDate = artistToMergeInto.BirthDate ?? artistToMerge.BirthDate; - artistToMergeInto.BeginDate = artistToMergeInto.BeginDate ?? artistToMerge.BeginDate; - artistToMergeInto.EndDate = artistToMergeInto.EndDate ?? artistToMerge.EndDate; + artistToMergeInto.RealName ??= artistToMerge.RealName; + artistToMergeInto.MusicBrainzId ??= artistToMerge.MusicBrainzId; + artistToMergeInto.ITunesId ??= artistToMerge.ITunesId; + artistToMergeInto.AmgId ??= artistToMerge.AmgId; + artistToMergeInto.SpotifyId ??= artistToMerge.SpotifyId; + artistToMergeInto.Profile ??= artistToMerge.Profile; + artistToMergeInto.BirthDate ??= artistToMerge.BirthDate; + artistToMergeInto.BeginDate ??= artistToMerge.BeginDate; + artistToMergeInto.EndDate ??= artistToMerge.EndDate; if (!string.IsNullOrEmpty(artistToMerge.ArtistType) && !artistToMerge.ArtistType.Equals("Other", StringComparison.OrdinalIgnoreCase)) { - artistToMergeInto.ArtistType = artistToMergeInto.ArtistType ?? artistToMerge.ArtistType; + artistToMergeInto.ArtistType ??= artistToMerge.ArtistType; } - artistToMergeInto.BioContext = artistToMergeInto.BioContext ?? artistToMerge.BioContext; - artistToMergeInto.DiscogsId = artistToMergeInto.DiscogsId ?? artistToMerge.DiscogsId; + artistToMergeInto.BioContext ??= artistToMerge.BioContext; + artistToMergeInto.DiscogsId ??= artistToMerge.DiscogsId; artistToMergeInto.Tags = artistToMergeInto.Tags.AddToDelimitedList(artistToMerge.Tags.ToListFromDelimited()); var altNames = artistToMerge.AlternateNames.ToListFromDelimited().ToList(); altNames.Add(artistToMerge.Name); @@ -1344,7 +1326,7 @@ namespace Roadie.Api.Services var existingArtistGenres = DbContext.ArtistGenres.Where(x => x.ArtistId == artistToMergeInto.Id).ToArray(); foreach (var artistGenre in artistGenres) { - var existing = existingArtistGenres.FirstOrDefault(x => x.GenreId == artistGenre.GenreId); + var existing = Array.Find(existingArtistGenres, x => x.GenreId == artistGenre.GenreId); // If not exist then add new for artist to merge into if (existing == null) { @@ -1379,7 +1361,7 @@ namespace Roadie.Api.Services } } // Secondary Artist images - if (artistToMergeSecondaryImages.Any()) + if (artistToMergeSecondaryImages.Count > 0) { var looper = 0; foreach (var artistSecondaryImage in artistToMergeSecondaryImages) @@ -1418,7 +1400,7 @@ namespace Roadie.Api.Services var artistToMergeHasRelease = DbContext.Releases.FirstOrDefault(x => x.ArtistId == artistToMerge.Id && x.Title == artistRelease.Title); if (artistToMergeHasRelease != null) { - await ReleaseService.MergeReleases(user, artistRelease, artistToMergeHasRelease, false); + await ReleaseService.MergeReleases(user, artistRelease, artistToMergeHasRelease, false).ConfigureAwait(false); } else { @@ -1435,11 +1417,11 @@ namespace Roadie.Api.Services foreach (var release in DbContext.Releases.Include("Artist").Where(x => x.ArtistId == artistToMerge.Id).ToArray()) { var originalReleaseFolder = release.ReleaseFileFolder(artistToMergeFolder); - await ReleaseService.UpdateRelease(user, release.Adapt(), originalReleaseFolder); + await ReleaseService.UpdateRelease(user, release.Adapt(), originalReleaseFolder).ConfigureAwait(false); } - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); - await Delete(user, artistToMerge, true); + await Delete(user, artistToMerge, true).ConfigureAwait(false); result = true; @@ -1473,7 +1455,7 @@ namespace Roadie.Api.Services File.WriteAllBytes(artistImage, imageBytes); artist.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); CacheManager.ClearRegion(artist.CacheRegion); Logger.LogInformation($"SaveImageBytes `{artist}` By User `{user}`"); } @@ -1492,7 +1474,7 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), + IsSuccess = errors.Count == 0, Data = ImageHelper.MakeThumbnailImage(Configuration, HttpContext, id, "artist", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height, true), OperationTime = sw.ElapsedMilliseconds, Errors = errors diff --git a/Roadie.Api.Services/ReleaseService.cs b/Roadie.Api.Services/ReleaseService.cs index 025793e..7dc9de6 100644 --- a/Roadie.Api.Services/ReleaseService.cs +++ b/Roadie.Api.Services/ReleaseService.cs @@ -10,7 +10,6 @@ using Roadie.Library.Encoding; using Roadie.Library.Engines; using Roadie.Library.Enums; using Roadie.Library.Extensions; -using Roadie.Library.Identity; using Roadie.Library.Imaging; using Roadie.Library.MetaData.Audio; using Roadie.Library.Models; @@ -36,7 +35,7 @@ namespace Roadie.Api.Services { public class ReleaseService : ServiceBase, IReleaseService { - private List _addedTrackIds = new List(); + private readonly List _addedTrackIds = new List(); public IEnumerable AddedTrackIds => _addedTrackIds; private IArtistLookupEngine ArtistLookupEngine { get; } @@ -66,7 +65,7 @@ namespace Roadie.Api.Services AudioMetaDataHelper = audioMetaDataHelper; } - public async Task> ById(Library.Models.Users.User roadieUser, Guid id, IEnumerable includes = null) + public async Task> ById(User roadieUser, Guid id, IEnumerable includes = null) { var timings = new Dictionary(); var tsw = new Stopwatch(); @@ -77,35 +76,32 @@ namespace Roadie.Api.Services var result = await CacheManager.GetAsync(cacheKey, async () => { tsw.Restart(); - var rr = await ReleaseByIdAction(id, includes); + var rr = await ReleaseByIdAction(id, includes).ConfigureAwait(false); tsw.Stop(); timings.Add("ReleaseByIdAction", tsw.ElapsedMilliseconds); return rr; - }, data.Artist.CacheRegionUrn(id)); + }, data.Artist.CacheRegionUrn(id)).ConfigureAwait(false); if (result?.Data != null && roadieUser != null) { tsw.Restart(); - var release = await GetRelease(id); + var release = await GetRelease(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getRelease", tsw.ElapsedMilliseconds); - var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Release); + var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Release).ConfigureAwait(false); if (userBookmarkResult.IsSuccess) { - result.Data.UserBookmarked = userBookmarkResult?.Rows?.FirstOrDefault(x => x?.Bookmark?.Value == release?.RoadieId.ToString()) != null; + result.Data.UserBookmarked = userBookmarkResult?.Rows?.FirstOrDefault(x => x?.Bookmark?.Value == release?.RoadieId.ToString()) != null; } if (result.Data.Medias != null) { tsw.Restart(); - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); tsw.Stop(); timings.Add("getUser", tsw.ElapsedMilliseconds); tsw.Restart(); - Parallel.ForEach(result.Data.Medias.SelectMany(x => x.Tracks), track => - { - track.TrackPlayUrl = MakeTrackPlayUrl(user, HttpContext.BaseUrl, track.Id); - }); + Parallel.ForEach(result.Data.Medias.SelectMany(x => x.Tracks), track => track.TrackPlayUrl = MakeTrackPlayUrl(user, HttpContext.BaseUrl, track.Id)); tsw.Stop(); timings.Add("makeTrackPlayUrls", tsw.ElapsedMilliseconds); @@ -121,7 +117,7 @@ namespace Roadie.Api.Services t, ut }).ToArray(); - if (releaseUserTracks != null && releaseUserTracks.Any()) + if (releaseUserTracks?.Any() == true) { foreach (var releaseUserTrack in releaseUserTracks) { @@ -169,7 +165,7 @@ namespace Roadie.Api.Services select cr).ToArray(); foreach (var comment in result.Data.Comments) { - var userCommentReaction = userCommentReactions.FirstOrDefault(x => x.CommentId == comment.DatabaseId); + var userCommentReaction = Array.Find(userCommentReactions, x => x.CommentId == comment.DatabaseId); comment.IsDisliked = userCommentReaction?.ReactionValue == CommentReaction.Dislike; comment.IsLiked = userCommentReaction?.ReactionValue == CommentReaction.Like; } @@ -196,7 +192,6 @@ namespace Roadie.Api.Services /// /// Release that has been modified /// Folder for release before any changes - /// public async Task> CheckAndChangeReleaseTitle(data.Release release, string oldReleaseFolder) { SimpleContract.Requires(release != null, "Invalid Release"); @@ -223,6 +218,7 @@ namespace Roadie.Api.Services var releaseDirectoryInfo = new DirectoryInfo(newReleaseFolder); // Update and move tracks under new release folder foreach (var releaseMedia in DbContext.ReleaseMedias.Where(x => x.ReleaseId == release.Id).ToArray()) + { // Update the track path to have the new album title. This is needed because future scans might not work properly without updating track title. foreach (var track in DbContext.Tracks.Where(x => x.ReleaseMediaId == releaseMedia.Id).ToArray()) { @@ -232,7 +228,7 @@ namespace Roadie.Api.Services if (trackFileInfo.Exists) { // Update the tracks release tags - var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo); + var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo).ConfigureAwait(false); audioMetaData.Release = release.Title; AudioMetaDataHelper.WriteTags(audioMetaData, trackFileInfo); @@ -247,9 +243,11 @@ namespace Roadie.Api.Services track.Status = Statuses.Missing; } CacheManager.ClearRegion(track.CacheRegion); + result = true; } + } - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); // Clean up any empty folders for the artist FolderPathHelper.DeleteEmptyFoldersForArtist(Configuration, release.Artist); @@ -281,16 +279,16 @@ namespace Roadie.Api.Services join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId join t in DbContext.Tracks on rm.Id equals t.ReleaseMediaId where r.Id == release.Id - select t).ToArrayAsync(); + select t).ToArrayAsync().ConfigureAwait(false); - var releaseLabelIds = await DbContext.ReleaseLabels.Where(x => x.ReleaseId == release.Id).Select(x => x.LabelId).ToArrayAsync(); + var releaseLabelIds = await DbContext.ReleaseLabels.Where(x => x.ReleaseId == release.Id).Select(x => x.LabelId).ToArrayAsync().ConfigureAwait(false); var playlistIdsWithReleaseTracks = await (from r in DbContext.Releases - join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId - join tr in DbContext.Tracks on rm.Id equals tr.ReleaseMediaId - join plt in DbContext.PlaylistTracks on tr.Id equals plt.TrackId - where r.Id == release.Id - select plt.PlayListId).ToListAsync(); + join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId + join tr in DbContext.Tracks on rm.Id equals tr.ReleaseMediaId + join plt in DbContext.PlaylistTracks on tr.Id equals plt.TrackId + where r.Id == release.Id + select plt.PlayListId).ToListAsync().ConfigureAwait(false); if (doDeleteFiles) { foreach (var track in releaseTracks) @@ -311,8 +309,8 @@ namespace Roadie.Api.Services } } - // Delete all image files for Release - foreach(var file in ImageHelper.ImageFilesInFolder(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)), SearchOption.AllDirectories)) + // Delete all image files for Release + foreach (var file in ImageHelper.ImageFilesInFolder(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)), SearchOption.AllDirectories)) { try { @@ -335,7 +333,7 @@ namespace Roadie.Api.Services } } DbContext.Releases.Remove(release); - var i = await DbContext.SaveChangesAsync(); + var i = await DbContext.SaveChangesAsync().ConfigureAwait(false); result = true; try { @@ -354,25 +352,25 @@ namespace Roadie.Api.Services releaseArtistIds.AddRange(releaseTracks.Where(x => x.ArtistId.HasValue).Select(x => x.ArtistId.Value)); foreach (var artistId in releaseArtistIds.Distinct()) { - await UpdateArtistCounts(artistId, now); - } + await UpdateArtistCounts(artistId, now).ConfigureAwait(false); + } } - if (releaseLabelIds != null && releaseLabelIds.Any()) + if (releaseLabelIds?.Any() == true) { foreach (var releaseLabelId in releaseLabelIds) { - await UpdateLabelCounts(releaseLabelId, now); + await UpdateLabelCounts(releaseLabelId, now).ConfigureAwait(false); } } sw.Stop(); - await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Release, release.Id); - foreach(var releaseTrack in releaseTracks) + await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Release, release.Id).ConfigureAwait(false); + foreach (var releaseTrack in releaseTracks) { - await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Track, releaseTrack.Id); + await BookmarkService.RemoveAllBookmarksForItem(BookmarkType.Track, releaseTrack.Id).ConfigureAwait(false); } - foreach(var playlistIdWithReleaseTracks in playlistIdsWithReleaseTracks) + foreach (var playlistIdWithReleaseTracks in playlistIdsWithReleaseTracks) { - await UpdatePlaylistCounts(playlistIdWithReleaseTracks, now); + await UpdatePlaylistCounts(playlistIdWithReleaseTracks, now).ConfigureAwait(false); } Logger.LogWarning("User `{0}` deleted Release `{1}]`", user, release); return new OperationResult @@ -403,13 +401,13 @@ namespace Roadie.Api.Services foreach (var release in releases) { - var defaultResult = await Delete(user, release, doDeleteFiles, false); + var defaultResult = await Delete(user, release, doDeleteFiles, false).ConfigureAwait(false); result &= defaultResult.IsSuccess; } foreach (var artistId in artistIds) { - await UpdateArtistCounts(artistId, now); + await UpdateArtistCounts(artistId, now).ConfigureAwait(false); } sw.Stop(); @@ -421,7 +419,7 @@ namespace Roadie.Api.Services }; } - public async Task> List(Library.Models.Users.User roadieUser, PagedRequest request, bool? doRandomize = false, IEnumerable includes = null) + public async Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false, IEnumerable includes = null) { var sw = new Stopwatch(); sw.Start(); @@ -457,7 +455,7 @@ namespace Roadie.Api.Services .Distinct(); isFilteredToGenre = true; } - else if (!string.IsNullOrEmpty(request.FilterByGenre) || !string.IsNullOrEmpty(request.Filter) && request.Filter.StartsWith(":genre", StringComparison.OrdinalIgnoreCase)) + else if (!string.IsNullOrEmpty(request.FilterByGenre) || (!string.IsNullOrEmpty(request.Filter) && request.Filter.StartsWith(":genre", StringComparison.OrdinalIgnoreCase))) { var genreFilter = request.FilterByGenre ?? (request.Filter ?? string.Empty).Replace(":genre ", "", StringComparison.OrdinalIgnoreCase); genreReleaseIds = (from rg in DbContext.ReleaseGenres @@ -492,7 +490,9 @@ namespace Roadie.Api.Services if (filter.StartsWith('"') && filter.EndsWith('"')) { isEqualFilter = true; +#pragma warning disable IDE0057 // Use range operator request.Filter = filter.Substring(1, filter.Length - 2); +#pragma warning restore IDE0057 // Use range operator } } @@ -505,7 +505,7 @@ namespace Roadie.Api.Services if (doRandomize ?? false) { var randomLimit = request.Limit ?? roadieUser?.RandomReleaseLimit ?? request.LimitValue; - randomReleaseData = await DbContext.RandomReleaseIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly); + randomReleaseData = await DbContext.RandomReleaseIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly).ConfigureAwait(false); randomReleaseIds = randomReleaseData.Select(x => x.Value).ToArray(); rowCount = DbContext.Releases.Count(); } @@ -519,9 +519,9 @@ namespace Roadie.Api.Services where !request.FilterFavoriteOnly || favoriteReleaseIds.Contains(r.Id) where !isFilteredToGenre || genreReleaseIds.Contains(r.Id) where request.FilterFromYear == null || - r.ReleaseDate != null && r.ReleaseDate.Value.Year <= request.FilterFromYear + (r.ReleaseDate != null && r.ReleaseDate.Value.Year <= request.FilterFromYear) where request.FilterToYear == null || - r.ReleaseDate != null && r.ReleaseDate.Value.Year >= request.FilterToYear + (r.ReleaseDate != null && r.ReleaseDate.Value.Year >= request.FilterToYear) where request.FilterValue == "" || r.Title.Contains(request.FilterValue) || r.AlternateNames.Contains(request.FilterValue) || @@ -563,11 +563,11 @@ namespace Roadie.Api.Services ReleaseList[] rows = null; - rowCount = rowCount ?? result.Count(); + rowCount ??= result.Count(); if (doRandomize ?? false) { - var resultData = await result.ToArrayAsync(); + var resultData = await result.ToArrayAsync().ConfigureAwait(false); rows = (from r in resultData join ra in randomReleaseData on r.DatabaseId equals ra.Value orderby ra.Key @@ -577,7 +577,7 @@ namespace Roadie.Api.Services else { string sortBy = null; - if (request.ActionValue == Library.Models.Users.User.ActionKeyUserRated) + if (request.ActionValue == User.ActionKeyUserRated) { sortBy = request.OrderValue(new Dictionary { { "Rating", "DESC" } }); } @@ -602,7 +602,7 @@ namespace Roadie.Api.Services if (request.FilterToCollectionId.HasValue) { - rows = await result.ToArrayAsync(); + rows = await result.ToArrayAsync().ConfigureAwait(false); } else { @@ -612,52 +612,52 @@ namespace Roadie.Api.Services .OrderBy(sortBy) .Skip(request.SkipValue) .Take(request.LimitValue) - .ToArrayAsync(); + .ToArrayAsync().ConfigureAwait(false); } } - if (rows.Any()) + if (rows.Length > 0) { var rowIds = rows.Select(x => x.DatabaseId).ToArray(); var genreData = await (from rg in DbContext.ReleaseGenres - join g in DbContext.Genres on rg.GenreId equals g.Id - where rowIds.Contains(rg.ReleaseId) - orderby rg.Id - select new - { - rg.ReleaseId, - dt = new DataToken - { - Text = g.Name, - Value = g.RoadieId.ToString() - } - }).ToArrayAsync(); + join g in DbContext.Genres on rg.GenreId equals g.Id + where rowIds.Contains(rg.ReleaseId) + orderby rg.Id + select new + { + rg.ReleaseId, + dt = new DataToken + { + Text = g.Name, + Value = g.RoadieId.ToString() + } + }).ToArrayAsync().ConfigureAwait(false); foreach (var release in rows) { - var genre = genreData.FirstOrDefault(x => x.ReleaseId == release.DatabaseId); + var genre = Array.Find(genreData, x => x.ReleaseId == release.DatabaseId); release.Genre = genre?.dt ?? new DataToken(); } if (request.FilterToCollectionId.HasValue) { var newRows = new List(rows); - var collection = await GetCollection(request.FilterToCollectionId.Value); + var collection = await GetCollection(request.FilterToCollectionId.Value).ConfigureAwait(false); var collectionReleases = from c in DbContext.Collections join cr in DbContext.CollectionReleases on c.Id equals cr.CollectionId where c.RoadieId == request.FilterToCollectionId select cr; - var pars = collection.PositionArtistReleases().ToArray(); - foreach (var par in pars) + foreach (var par in collection.PositionArtistReleases().ToArray()) { var cr = collectionReleases.FirstOrDefault(x => x.ListNumber == par.Position); // Release is known for Collection CSV if (cr != null) { - var parRelease = rows.FirstOrDefault(x => x.DatabaseId == cr.ReleaseId); - if (parRelease != null) - if (!parRelease.ListNumber.HasValue) - parRelease.ListNumber = par.Position; + var parRelease = Array.Find(rows, x => x.DatabaseId == cr.ReleaseId); + if (parRelease?.ListNumber.HasValue == false) + { + parRelease.ListNumber = par.Position; + } } // Release is not known add missing dummy release to rows else @@ -698,12 +698,12 @@ namespace Roadie.Api.Services var userReleaseRatings = await (from ur in DbContext.UserReleases where ur.UserId == roadieUser.Id where rowIds.Contains(ur.ReleaseId) - select ur).ToArrayAsync(); + select ur).ToArrayAsync().ConfigureAwait(false); foreach (var userReleaseRating in userReleaseRatings.Where(x => rows.Select(r => r.DatabaseId).Contains(x.ReleaseId))) { - var row = rows.FirstOrDefault(x => x.DatabaseId == userReleaseRating.ReleaseId); + var row = Array.Find(rows, x => x.DatabaseId == userReleaseRating.ReleaseId); if (row != null) { var isDisliked = userReleaseRating.IsDisliked ?? false; @@ -722,52 +722,49 @@ namespace Roadie.Api.Services } } - if (includes != null && includes.Any()) + if (includes?.Any() == true && includes.Contains("tracks")) { - if (includes.Contains("tracks")) + var rowIds = rows.Select(x => x.DatabaseId).ToArray(); + var userRatingsForResult = await (from ut in DbContext.UserTracks.Include(x => x.Track) + join t in DbContext.Tracks on ut.TrackId equals t.Id + join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rowIds.Contains(rm.ReleaseId) + where ut.UserId == roadieUser.Id + select ut) + .ToArrayAsync().ConfigureAwait(false) ?? new data.UserTrack[0]; + + foreach (var release in rows) { - var rowIds = rows.Select(x => x.DatabaseId).ToArray(); - var userRatingsForResult = await (from ut in DbContext.UserTracks.Include(x => x.Track) - join t in DbContext.Tracks on ut.TrackId equals t.Id - join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id - where rowIds.Contains(rm.ReleaseId) - where ut.UserId == roadieUser.Id - select ut) - .ToArrayAsync() ?? new data.UserTrack[0]; + release.Media = DbContext.ReleaseMedias + .Include(x => x.Tracks) + .Where(x => x.ReleaseId == release.DatabaseId) + .ToArray() + .AsQueryable() + .ProjectToType() + .OrderBy(x => x.MediaNumber) + .ToArray(); // Async operation on Project Mapping async throws error - foreach (var release in rows) + Parallel.ForEach(release.Media, (media) => { - release.Media = DbContext.ReleaseMedias - .Include(x => x.Tracks) - .Where(x => x.ReleaseId == release.DatabaseId) - .ToArray() - .AsQueryable() - .ProjectToType() - .OrderBy(x => x.MediaNumber) - .ToArray(); // Async operation on Project Mapping async throws error - - Parallel.ForEach(release.Media, (media) => + media.Tracks = media.Tracks.OrderBy(x => x.TrackNumber); + var userTracksForMedia = userRatingsForResult.Where(x => x.Track.ReleaseMediaId == media.DatabaseId).ToArray(); + if (userTracksForMedia.Length > 0) { - media.Tracks = media.Tracks.OrderBy(x => x.TrackNumber); - var userTracksForMedia = userRatingsForResult.Where(x => x.Track.ReleaseMediaId == media.DatabaseId).ToArray(); - if (userTracksForMedia.Any()) + Parallel.ForEach(userTracksForMedia, (userTrack) => { - Parallel.ForEach(userTracksForMedia, (userTrack) => + var mediaTrack = media.Tracks.FirstOrDefault(x => x.DatabaseId == userTrack.TrackId); + if (mediaTrack != null) { - var mediaTrack = media.Tracks.FirstOrDefault(x => x.DatabaseId == userTrack.TrackId); - if(mediaTrack != null) + mediaTrack.UserRating = new UserTrack { - mediaTrack.UserRating = new UserTrack - { - Rating = userTrack.Rating, - IsFavorite = userTrack.IsFavorite ?? false, - IsDisliked = userTrack.IsDisliked ?? false - }; - } - }); - } - }); - } + Rating = userTrack.Rating, + IsFavorite = userTrack.IsFavorite ?? false, + IsDisliked = userTrack.IsDisliked ?? false + }; + } + }); + } + }); } } @@ -822,7 +819,7 @@ namespace Roadie.Api.Services try { - await MergeReleases(user, releaseToMerge, releaseToMergeInfo, addAsMedia); + await MergeReleases(user, releaseToMerge, releaseToMergeInfo, addAsMedia).ConfigureAwait(false); } catch (Exception ex) { @@ -835,8 +832,8 @@ namespace Roadie.Api.Services releaseToMerge, releaseToMergeInfo, user); return new OperationResult { - IsSuccess = !errors.Any(), - Data = !errors.Any(), + IsSuccess = errors.Count == 0, + Data = errors.Count == 0, OperationTime = sw.ElapsedMilliseconds, Errors = errors }; @@ -848,7 +845,6 @@ namespace Roadie.Api.Services /// The release to be merged /// The release to merge into /// If true then add a ReleaseMedia to the release to be merged into - /// public async Task> MergeReleases(Library.Identity.User user, data.Release releaseToMerge, data.Release releaseToMergeInto, bool addAsMedia) { SimpleContract.Requires(releaseToMerge != null, "Invalid Release"); @@ -865,7 +861,7 @@ namespace Roadie.Api.Services var mergedFilesToDelete = new List(); var mergedTracksToMove = new List(); - releaseToMergeInto.MediaCount = releaseToMergeInto.MediaCount ?? 0; + releaseToMergeInto.MediaCount ??= 0; var now = DateTime.UtcNow; var releaseToMergeReleaseMedia = DbContext.ReleaseMedias.Where(x => x.ReleaseId == releaseToMerge.Id).ToList(); @@ -873,7 +869,8 @@ namespace Roadie.Api.Services var releaseToMergeIntoLastMediaNumber = releaseToMergeIntoReleaseMedia.Max(x => x.MediaNumber); // Add new ReleaseMedia - if (addAsMedia || !releaseToMergeIntoReleaseMedia.Any()) + if (addAsMedia || releaseToMergeIntoReleaseMedia.Count == 0) + { foreach (var rm in releaseToMergeReleaseMedia) { releaseToMergeIntoLastMediaNumber++; @@ -883,12 +880,14 @@ namespace Roadie.Api.Services releaseToMergeInto.MediaCount++; releaseToMergeInto.TrackCount += rm.TrackCount; } + } // Merge into existing ReleaseMedia else + { // See if each media exists and merge details of each including tracks foreach (var rm in releaseToMergeReleaseMedia) { - var existingReleaseMedia = releaseToMergeIntoReleaseMedia.FirstOrDefault(x => x.MediaNumber == rm.MediaNumber); + var existingReleaseMedia = releaseToMergeIntoReleaseMedia.Find(x => x.MediaNumber == rm.MediaNumber); var mergeTracks = DbContext.Tracks.Where(x => x.ReleaseMediaId == rm.Id).ToArray(); if (existingReleaseMedia == null) { @@ -908,7 +907,7 @@ namespace Roadie.Api.Services var mergeIntoTracks = DbContext.Tracks.Where(x => x.ReleaseMediaId == existingReleaseMedia.Id).ToArray(); foreach (var mergeTrack in mergeTracks) { - var existingTrack = mergeIntoTracks.FirstOrDefault(x => x.TrackNumber == mergeTrack.TrackNumber); + var existingTrack = Array.Find(mergeIntoTracks, x => x.TrackNumber == mergeTrack.TrackNumber); if (existingTrack == null) { // Track does not exist, update to existing ReleaseMedia and update ReleaseToMergeInfo counts @@ -922,50 +921,60 @@ namespace Roadie.Api.Services else { // Track does exist merge two tracks together - existingTrack.MusicBrainzId = existingTrack.MusicBrainzId ?? mergeTrack.MusicBrainzId; - existingTrack.SpotifyId = existingTrack.SpotifyId ?? mergeTrack.SpotifyId; - existingTrack.AmgId = existingTrack.AmgId ?? mergeTrack.AmgId; - existingTrack.ISRC = existingTrack.ISRC ?? mergeTrack.ISRC; - existingTrack.AmgId = existingTrack.AmgId ?? mergeTrack.AmgId; - existingTrack.LastFMId = existingTrack.LastFMId ?? mergeTrack.LastFMId; - existingTrack.PartTitles = existingTrack.PartTitles ?? mergeTrack.PartTitles; + existingTrack.MusicBrainzId ??= mergeTrack.MusicBrainzId; + existingTrack.SpotifyId ??= mergeTrack.SpotifyId; + existingTrack.AmgId ??= mergeTrack.AmgId; + existingTrack.ISRC ??= mergeTrack.ISRC; + existingTrack.AmgId ??= mergeTrack.AmgId; + existingTrack.LastFMId ??= mergeTrack.LastFMId; + existingTrack.PartTitles ??= mergeTrack.PartTitles; existingTrack.PlayedCount = (existingTrack.PlayedCount ?? 0) + (mergeTrack.PlayedCount ?? 0); if (mergeTrack.LastPlayed.HasValue && existingTrack.LastPlayed.HasValue && mergeTrack.LastPlayed > existingTrack.LastPlayed) + { existingTrack.LastPlayed = mergeTrack.LastPlayed; - existingTrack.MusicBrainzId = existingTrack.MusicBrainzId ?? mergeTrack.MusicBrainzId; + } + + existingTrack.MusicBrainzId ??= mergeTrack.MusicBrainzId; existingTrack.Tags = existingTrack.Tags.AddToDelimitedList(mergeTrack.Tags.ToListFromDelimited()); if (!mergeTrack.Title.Equals(existingTrack.Title, StringComparison.OrdinalIgnoreCase)) + { existingTrack.AlternateNames = existingTrack.AlternateNames.AddToDelimitedList(new[] {mergeTrack.Title, mergeTrack.Title.ToAlphanumericName()}); + } + existingTrack.AlternateNames = existingTrack.AlternateNames.AddToDelimitedList(mergeTrack.AlternateNames.ToListFromDelimited()); existingTrack.LastUpdated = now; var mergedTrackFileName = mergeTrack.PathToTrack(Configuration); var trackFileName = existingTrack.PathToTrack(Configuration); if (!trackFileName.Equals(mergedTrackFileName, StringComparison.Ordinal) && - File.Exists(trackFileName)) mergedFilesToDelete.Add(mergedTrackFileName); + File.Exists(trackFileName)) + { + mergedFilesToDelete.Add(mergedTrackFileName); + } } } } } + } var releaseToMergeFolder = releaseToMerge.ReleaseFileFolder(releaseToMerge.Artist.ArtistFileFolder(Configuration)); var releaseToMergeIntoArtistFolder = releaseToMergeInto.Artist.ArtistFileFolder(Configuration); var releaseToMergeIntoDirectory = new DirectoryInfo(releaseToMergeInto.ReleaseFileFolder(releaseToMergeIntoArtistFolder)); // Move tracks for releaseToMergeInto into correct folders - if (mergedTracksToMove.Any()) + if (mergedTracksToMove.Count > 0) { foreach (var track in mergedTracksToMove) { var oldTrackPath = track.PathToTrack(Configuration); var newTrackPath = FolderPathHelper.TrackFullPath(Configuration, releaseToMergeInto.Artist, releaseToMergeInto, track); var trackFile = new FileInfo(oldTrackPath); - if(!newTrackPath.Equals(oldTrackPath, StringComparison.OrdinalIgnoreCase)) + if (!newTrackPath.Equals(oldTrackPath, StringComparison.OrdinalIgnoreCase)) { - var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFile); + var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFile).ConfigureAwait(false); track.FilePath = FolderPathHelper.TrackPath(Configuration, releaseToMergeInto.Artist, releaseToMergeInto, track); track.Hash = HashHelper.CreateMD5(releaseToMergeInto.ArtistId + trackFile.LastWriteTimeUtc.GetHashCode().ToString() + audioMetaData.GetHashCode()); track.LastUpdated = now; @@ -975,7 +984,7 @@ namespace Roadie.Api.Services } // Cleanup folders - Services.FileDirectoryProcessorService.DeleteEmptyFolders(new DirectoryInfo(releaseToMergeIntoArtistFolder), Logger); + FileDirectoryProcessorService.DeleteEmptyFolders(new DirectoryInfo(releaseToMergeIntoArtistFolder), Logger); // Now Merge release details releaseToMergeInto.AlternateNames = releaseToMergeInto.AlternateNames.AddToDelimitedList(new[] @@ -986,25 +995,28 @@ namespace Roadie.Api.Services releaseToMergeInto.Tags = releaseToMergeInto.Tags.AddToDelimitedList(releaseToMerge.Tags.ToListFromDelimited()); releaseToMergeInto.URLs.AddToDelimitedList(releaseToMerge.URLs.ToListFromDelimited()); - releaseToMergeInto.MusicBrainzId = releaseToMergeInto.MusicBrainzId ?? releaseToMerge.MusicBrainzId; - releaseToMergeInto.Profile = releaseToMergeInto.Profile ?? releaseToMerge.Profile; - releaseToMergeInto.ReleaseDate = releaseToMergeInto.ReleaseDate ?? releaseToMerge.ReleaseDate; - releaseToMergeInto.MusicBrainzId = releaseToMergeInto.MusicBrainzId ?? releaseToMerge.MusicBrainzId; - releaseToMergeInto.DiscogsId = releaseToMergeInto.DiscogsId ?? releaseToMerge.DiscogsId; - releaseToMergeInto.ITunesId = releaseToMergeInto.ITunesId ?? releaseToMerge.ITunesId; - releaseToMergeInto.AmgId = releaseToMergeInto.AmgId ?? releaseToMerge.AmgId; - releaseToMergeInto.LastFMId = releaseToMergeInto.LastFMId ?? releaseToMerge.LastFMId; - releaseToMergeInto.LastFMSummary = releaseToMergeInto.LastFMSummary ?? releaseToMerge.LastFMSummary; - releaseToMergeInto.SpotifyId = releaseToMergeInto.SpotifyId ?? releaseToMerge.SpotifyId; + releaseToMergeInto.MusicBrainzId ??= releaseToMerge.MusicBrainzId; + releaseToMergeInto.Profile ??= releaseToMerge.Profile; + releaseToMergeInto.ReleaseDate ??= releaseToMerge.ReleaseDate; + releaseToMergeInto.MusicBrainzId ??= releaseToMerge.MusicBrainzId; + releaseToMergeInto.DiscogsId ??= releaseToMerge.DiscogsId; + releaseToMergeInto.ITunesId ??= releaseToMerge.ITunesId; + releaseToMergeInto.AmgId ??= releaseToMerge.AmgId; + releaseToMergeInto.LastFMId ??= releaseToMerge.LastFMId; + releaseToMergeInto.LastFMSummary ??= releaseToMerge.LastFMSummary; + releaseToMergeInto.SpotifyId ??= releaseToMerge.SpotifyId; if (releaseToMergeInto.ReleaseType == ReleaseType.Unknown && releaseToMerge.ReleaseType != ReleaseType.Unknown) + { releaseToMergeInto.ReleaseType = releaseToMerge.ReleaseType; + } + releaseToMergeInto.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); // Update any collection pointers for release to be merged var collectionRecords = DbContext.CollectionReleases.Where(x => x.ReleaseId == releaseToMerge.Id); - if (collectionRecords != null && collectionRecords.Any()) + if (collectionRecords?.Any() == true) { foreach (var cr in collectionRecords) { @@ -1012,7 +1024,7 @@ namespace Roadie.Api.Services cr.LastUpdated = now; } - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } // Update any existing playlist for release to be merged @@ -1026,16 +1038,16 @@ namespace Roadie.Api.Services rm, pl }).ToArray(); - if (playListTrackInfos != null && playListTrackInfos.Any()) + if (playListTrackInfos?.Any() == true) { foreach (var playListTrackInfo in playListTrackInfos) { - var matchingTrack = (from t in DbContext.Tracks - join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id - where rm.ReleaseId == releaseToMergeInto.Id - where rm.MediaNumber == playListTrackInfo.rm.MediaNumber - where t.TrackNumber == playListTrackInfo.track.TrackNumber - select t).FirstOrDefault(); + var matchingTrack = await (from t in DbContext.Tracks + join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rm.ReleaseId == releaseToMergeInto.Id + where rm.MediaNumber == playListTrackInfo.rm.MediaNumber + where t.TrackNumber == playListTrackInfo.track.TrackNumber + select t).FirstOrDefaultAsync().ConfigureAwait(false); if (matchingTrack != null) { playListTrackInfo.pl.TrackId = matchingTrack.Id; @@ -1043,14 +1055,16 @@ namespace Roadie.Api.Services } } - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } - await Delete(user, releaseToMerge); + await Delete(user, releaseToMerge).ConfigureAwait(false); // Delete any files flagged to be deleted (duplicate as track already exists on merged to release) - if (mergedFilesToDelete.Any()) + if (mergedFilesToDelete.Count > 0) + { foreach (var mergedFileToDelete in mergedFilesToDelete) + { try { if (File.Exists(mergedFileToDelete)) @@ -1062,6 +1076,8 @@ namespace Roadie.Api.Services catch { } + } + } // Clear cache regions for manipulated records CacheManager.ClearRegion(releaseToMergeInto.CacheRegion); @@ -1087,9 +1103,9 @@ namespace Roadie.Api.Services }; } - public async Task> ReleaseZipped(Library.Models.Users.User roadieUser, Guid id) + public async Task> ReleaseZipped(User roadieUser, Guid id) { - var release = await GetRelease(id); + var release = await GetRelease(id).ConfigureAwait(false); if (release == null) return new FileOperationResult(true, string.Format("Release Not Found [{0}]", id)); @@ -1113,7 +1129,7 @@ namespace Roadie.Api.Services foreach (var releaseFile in releaseFiles) { var fileInfo = new FileInfo(releaseFile); - if (fileInfo.Extension.ToLower() == ".mp3" || fileInfo.Extension.ToLower() == ".jpg") + if (string.Equals(fileInfo.Extension, ".mp3", StringComparison.OrdinalIgnoreCase) || string.Equals(fileInfo.Extension, ".jpg", StringComparison.OrdinalIgnoreCase)) { var entry = zip.CreateEntry(fileInfo.Name); using (var entryStream = entry.Open()) @@ -1152,7 +1168,7 @@ namespace Roadie.Api.Services /// public async Task> ScanReleaseFolder(Library.Identity.User user, Guid releaseId, bool doJustInfo, data.Release releaseToScan = null) { - SimpleContract.Requires(releaseId != Guid.Empty && releaseToScan == null || releaseToScan != null, "Invalid ReleaseId"); + SimpleContract.Requires((releaseId != Guid.Empty && releaseToScan == null) || releaseToScan != null, "Invalid ReleaseId"); _addedTrackIds.Clear(); @@ -1186,9 +1202,9 @@ namespace Roadie.Api.Services // Get the First Track for the Release from DB (if any) and see if the path on it is set, if so rename to "correct" path // this happens with the logic change from folder naming and almost always case change which causes issues with Linux systems. firstTrack = (from t in DbContext.Tracks - join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id - where rm.ReleaseId == release.Id - select t).FirstOrDefault(); + join rm in DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rm.ReleaseId == release.Id + select t).FirstOrDefault(); if (firstTrack?.FilePath != null) { var trackPath = Path.GetDirectoryName(firstTrack.PathToTrack(Configuration)); @@ -1215,7 +1231,7 @@ namespace Roadie.Api.Services releaseTrack.LastUpdated = now; } release.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); Logger.LogWarning($"Release `{ release }`: Updated Release Track Path From [{ trackPath }] to [{ releasePath }]"); } else @@ -1253,7 +1269,7 @@ namespace Roadie.Api.Services if (foundMissingTracks) { - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -1275,7 +1291,7 @@ namespace Roadie.Api.Services { int? trackArtistId = null; string partTitles = null; - var audioMetaData = await AudioMetaDataHelper.GetInfo(file, doJustInfo); + var audioMetaData = await AudioMetaDataHelper.GetInfo(file, doJustInfo).ConfigureAwait(false); // This is the path for the new track not in the database but the found MP3 file to be added to library var trackPath = FolderPathHelper.TrackPath(Configuration, release.Artist.Id, release.Artist.SortNameValue, release.SortTitleValue, release.ReleaseDate.Value, audioMetaData.Title, audioMetaData.TrackNumber.Value); @@ -1283,7 +1299,7 @@ namespace Roadie.Api.Services { var trackHash = HashHelper.CreateMD5(release.ArtistId + file.LastWriteTimeUtc.GetHashCode().ToString() + audioMetaData.GetHashCode()); totalNumberOfTracksFound++; - totalTrackCount = totalTrackCount ?? (short)(audioMetaData.TotalTrackNumbers ?? 0); + totalTrackCount ??= (short)(audioMetaData.TotalTrackNumbers ?? 0); var releaseMediaNumber = (short)(audioMetaData.Disc ?? 1); if (!releaseMediaTotalNumberOfTracks.ContainsKey(releaseMediaNumber)) { @@ -1293,7 +1309,7 @@ namespace Roadie.Api.Services { releaseMediaTotalNumberOfTracks[releaseMediaNumber] = releaseMediaTotalNumberOfTracks[releaseMediaNumber].TakeLarger((short)(audioMetaData.TotalTrackNumbers ?? 0)); } - var releaseMedia = existingReleaseMedia.FirstOrDefault(x => x.MediaNumber == releaseMediaNumber); + var releaseMedia = existingReleaseMedia.Find(x => x.MediaNumber == releaseMediaNumber); if (releaseMedia == null) { // New ReleaseMedia - Not Found In Database @@ -1304,7 +1320,7 @@ namespace Roadie.Api.Services MediaNumber = releaseMediaNumber }; DbContext.ReleaseMedias.Add(releaseMedia); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); existingReleaseMedia.Add(releaseMedia); modifiedRelease = true; } @@ -1342,7 +1358,7 @@ namespace Roadie.Api.Services { if (audioMetaData.TrackArtists.Count() == 1) { - var trackArtistData = await ArtistLookupEngine.GetByName(new AudioMetaData { Artist = audioMetaData.TrackArtist }, true); + var trackArtistData = await ArtistLookupEngine.GetByName(new AudioMetaData { Artist = audioMetaData.TrackArtist }, true).ConfigureAwait(false); if (trackArtistData.IsSuccess && release.ArtistId != trackArtistData.Data.Id) { trackArtistId = trackArtistData.Data.Id; @@ -1369,7 +1385,7 @@ namespace Roadie.Api.Services track.ArtistId = trackArtistId; track.PartTitles = partTitles; DbContext.Tracks.Add(track); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); _addedTrackIds.Add(track.Id); modifiedRelease = true; } @@ -1379,7 +1395,7 @@ namespace Roadie.Api.Services { if (audioMetaData.TrackArtists.Count() == 1) { - var trackArtistData = await ArtistLookupEngine.GetByName(new AudioMetaData { Artist = audioMetaData.TrackArtist }, true); + var trackArtistData = await ArtistLookupEngine.GetByName(new AudioMetaData { Artist = audioMetaData.TrackArtist }, true).ConfigureAwait(false); if (trackArtistData.IsSuccess && release.ArtistId != trackArtistData.Data.Id) { trackArtistId = trackArtistData.Data.Id; @@ -1465,20 +1481,20 @@ namespace Roadie.Api.Services releaseMedia.TrackCount = kp.Value; releaseMedia.LastUpdated = now; releaseMedia.Status = areTracksForRelaseMediaSequential ? Statuses.Ok : Statuses.Incomplete; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); modifiedRelease = true; } } var foundInFolderTrackNumbers = foundInFolderTracks.Select(x => x.TrackNumber).OrderBy(x => x).ToArray(); - if (modifiedRelease || !foundInFolderTrackNumbers.Count().Equals(release.TrackCount) || - releaseMediaNumbersFound.Count() != (release.MediaCount ?? 0)) + if (modifiedRelease || !foundInFolderTrackNumbers.Length.Equals(release.TrackCount) || + releaseMediaNumbersFound.Count != (release.MediaCount ?? 0)) { var areTracksForRelaseSequential = foundInFolderTrackNumbers.Zip(foundInFolderTrackNumbers.Skip(1), (a, b) => a + 1 == b).All(x => x); - var maxFoundInFolderTrackNumbers = foundInFolderTrackNumbers.Any() ? foundInFolderTrackNumbers.Max() : (short)0; + var maxFoundInFolderTrackNumbers = foundInFolderTrackNumbers.Length > 0 ? foundInFolderTrackNumbers.Max() : (short)0; release.Status = areTracksForRelaseSequential ? Statuses.Ok : Statuses.Incomplete; - release.TrackCount = (short)foundInFolderTrackNumbers.Count(); - release.MediaCount = (short)releaseMediaNumbersFound.Count(); + release.TrackCount = (short)foundInFolderTrackNumbers.Length; + release.MediaCount = (short)releaseMediaNumbersFound.Count; if (release.TrackCount < maxFoundInFolderTrackNumbers) { release.TrackCount = maxFoundInFolderTrackNumbers; @@ -1491,7 +1507,7 @@ namespace Roadie.Api.Services ? Statuses.Complete : Statuses.Incomplete; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); CacheManager.ClearRegion(release.Artist.CacheRegion); CacheManager.ClearRegion(release.CacheRegion); } @@ -1500,15 +1516,15 @@ namespace Roadie.Api.Services var releaseCoverImage = ImageHelper.FindImageTypeInDirectory(releaseDirectory, ImageType.Release).FirstOrDefault(); var doesReleaseHaveCoverImage = releaseCoverImage != null; - if(doesReleaseHaveCoverImage) + if (doesReleaseHaveCoverImage) { - // See if image file is valid image if not delete it - if(ImageHelper.ConvertToJpegFormat(File.ReadAllBytes(releaseCoverImage.FullName)) == null) + // See if image file is valid image if not delete it + if (ImageHelper.ConvertToJpegFormat(File.ReadAllBytes(releaseCoverImage.FullName)) == null) { releaseCoverImage.Delete(); doesReleaseHaveCoverImage = false; Logger.LogWarning($"Deleted invalid cover image for Release `{ release }`"); - } + } } if (!doesReleaseHaveCoverImage) { @@ -1520,8 +1536,8 @@ namespace Roadie.Api.Services select t).FirstOrDefault(); if (firstTrack?.FilePath != null) { - var metaData = await AudioMetaDataHelper.GetInfo(new FileInfo(firstTrack.PathToTrack(Configuration)), doJustInfo); - if (metaData.Images != null && metaData.Images.Any()) + var metaData = await AudioMetaDataHelper.GetInfo(new FileInfo(firstTrack.PathToTrack(Configuration)), doJustInfo).ConfigureAwait(false); + if (metaData.Images?.Any() == true) { var imageBytes = ImageHelper.ConvertToJpegFormat(metaData.Images.FirstOrDefault(x => x.Data != null)?.Data); if (imageBytes != null) @@ -1536,19 +1552,19 @@ namespace Roadie.Api.Services sw.Stop(); - await UpdateReleaseCounts(release.Id, now); - await UpdatePlaylistCountsForRelease(release.Id, now); - await UpdateArtistCountsForRelease(release.Id, now); - await UpdateReleaseRank(release.Id); + await UpdateReleaseCounts(release.Id, now).ConfigureAwait(false); + await UpdatePlaylistCountsForRelease(release.Id, now).ConfigureAwait(false); + await UpdateArtistCountsForRelease(release.Id, now).ConfigureAwait(false); + await UpdateReleaseRank(release.Id).ConfigureAwait(false); CacheManager.ClearRegion(release.Artist.CacheRegion); CacheManager.ClearRegion(release.CacheRegion); - if (release.Labels != null && release.Labels.Any()) + if (release.Labels?.Any() == true) { foreach (var label in release.Labels) { - await UpdateLabelCounts(label.Id, now); + await UpdateLabelCounts(label.Id, now).ConfigureAwait(false); } } Logger.LogInformation("Scanned Release `{0}` Folder [{1}], Modified Release [{2}], OperationTime [{3}]", release.ToString(), releasePath, modifiedRelease, sw.ElapsedMilliseconds); @@ -1569,10 +1585,7 @@ namespace Roadie.Api.Services }; } - public async Task> SetReleaseImageByUrl(Library.Identity.User user, Guid id, string imageUrl) - { - return await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)); - } + public async Task> SetReleaseImageByUrl(Library.Identity.User user, Guid id, string imageUrl) => await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)).ConfigureAwait(false); public async Task> UpdateRelease(Library.Identity.User user, Release model, string originalReleaseFolder = null) { @@ -1608,7 +1621,7 @@ namespace Roadie.Api.Services { var now = DateTime.UtcNow; var artistFolder = release.Artist.ArtistFileFolder(Configuration); - originalReleaseFolder = originalReleaseFolder ?? release.ReleaseFileFolder(artistFolder); + originalReleaseFolder ??= release.ReleaseFileFolder(artistFolder); release.IsLocked = model.IsLocked; release.IsVirtual = model.IsVirtual; release.Status = SafeParser.ToEnum(model.Status); @@ -1656,7 +1669,7 @@ namespace Roadie.Api.Services File.WriteAllBytes(coverFileName, ImageHelper.ConvertToJpegFormat(releaseImage)); } var releaseFolder = release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)); - if (model.NewSecondaryImagesData != null && model.NewSecondaryImagesData.Any()) + if (model.NewSecondaryImagesData?.Any() == true) { // Additional images to add to artist var looper = 0; @@ -1684,7 +1697,7 @@ namespace Roadie.Api.Services } } - if (model.Genres != null && model.Genres.Any()) + if (model.Genres?.Any() == true) { // Remove existing Genres not in model list foreach (var genre in release.Genres.ToList()) @@ -1705,21 +1718,23 @@ namespace Roadie.Api.Services { var g = DbContext.Genres.FirstOrDefault(x => x.RoadieId == genreId); if (g != null) + { release.Genres.Add(new data.ReleaseGenre { ReleaseId = release.Id, GenreId = g.Id, Genre = g }); + } } } } - else if (model.Genres == null || !model.Genres.Any()) + else if (model.Genres?.Any() != true) { release.Genres.Clear(); } - if(model.Labels == null || !model.Labels.Any()) + if (model.Labels?.Any() != true) { // Delete all existing labels for release var releaseLabelsToDelete = (from l in DbContext.ReleaseLabels @@ -1727,7 +1742,7 @@ namespace Roadie.Api.Services select l).ToArray(); DbContext.ReleaseLabels.RemoveRange(releaseLabelsToDelete); } - else if (model.Labels != null && model.Labels.Any()) + else if (model.Labels?.Any() == true) { var releaseLabelIds = model.Labels.Select(x => x.Id).ToArray(); // Delete any labels not in given model (removed by edit operation) @@ -1737,12 +1752,12 @@ namespace Roadie.Api.Services select l).ToArray(); DbContext.ReleaseLabels.RemoveRange(releaseLabelsToDelete); // Update any existing - foreach(var label in model.Labels) + foreach (var label in model.Labels) { var trackLabel = DbContext.ReleaseLabels.FirstOrDefault(x => x.RoadieId == label.Id); if (trackLabel == null) { - // Add new + // Add new trackLabel = new data.ReleaseLabel { ReleaseId = release.Id, @@ -1753,7 +1768,7 @@ namespace Roadie.Api.Services trackLabel.CatalogNumber = label.CatalogNumber; trackLabel.BeginDate = label.BeginDate; trackLabel.EndDate = label.EndDate; - var releaseLabel = await GetLabel(SafeParser.ToGuid(label.Label.Label.Value).Value); + var releaseLabel = await GetLabel(SafeParser.ToGuid(label.Label.Label.Value).Value).ConfigureAwait(false); trackLabel.LabelId = releaseLabel.Id; trackLabel.IsLocked = label.IsLocked; trackLabel.Status = SafeParser.ToEnum(label.Status); @@ -1761,23 +1776,22 @@ namespace Roadie.Api.Services } } - if (model.Credits == null || !model.Credits.Any()) + if (model.Credits?.Any() != true) { // Delete all existing credits for release var releaseCreditsToDelete = (from c in DbContext.Credits - where c.ReleaseId == release.Id - select c).ToArray(); + where c.ReleaseId == release.Id + select c).ToArray(); DbContext.Credits.RemoveRange(releaseCreditsToDelete); - } - else if (model.Credits != null && model.Credits.Any()) + else if (model.Credits?.Any() == true) { var releaseCreditIds = model.Credits.Select(x => x.Id).ToArray(); // Delete any credits not given in model (removed by edit operation) var releaseCreditsToDelete = (from c in DbContext.Credits - where c.TrackId == release.Id - where !releaseCreditIds.Contains(c.RoadieId) - select c).ToArray(); + where c.TrackId == release.Id + where !releaseCreditIds.Contains(c.RoadieId) + select c).ToArray(); DbContext.Credits.RemoveRange(releaseCreditsToDelete); // Update any existing foreach (var credit in model.Credits) @@ -1785,7 +1799,7 @@ namespace Roadie.Api.Services var trackCredit = DbContext.Credits.FirstOrDefault(x => x.RoadieId == credit.Id); if (trackCredit == null) { - // Add new + // Add new trackCredit = new data.Credit { ReleaseId = release.Id, @@ -1796,7 +1810,7 @@ namespace Roadie.Api.Services data.Artist artistForCredit = null; if (credit.Artist != null) { - artistForCredit = await GetArtist(credit.Artist.Id); + artistForCredit = await GetArtist(credit.Artist.Id).ConfigureAwait(false); } var creditCategory = DbContext.CreditCategory.FirstOrDefault(x => x.RoadieId.ToString() == credit.Category.Value); trackCredit.CreditCategoryId = creditCategory.Id; @@ -1811,15 +1825,14 @@ namespace Roadie.Api.Services } } - release.LastUpdated = now; - await DbContext.SaveChangesAsync(); - await CheckAndChangeReleaseTitle(release, originalReleaseFolder); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + await CheckAndChangeReleaseTitle(release, originalReleaseFolder).ConfigureAwait(false); // Update release ID3 Tags foreach (var mp3 in Directory.GetFiles(releaseFolder, "*.mp3", SearchOption.AllDirectories)) { var trackFileInfo = new FileInfo(mp3); - var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo); + var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo).ConfigureAwait(false); if (audioMetaData != null) { audioMetaData.Year = release.ReleaseYear; @@ -1844,8 +1857,8 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), - Data = !errors.Any(), + IsSuccess = errors.Count == 0, + Data = errors.Count == 0, OperationTime = sw.ElapsedMilliseconds, Errors = errors }; @@ -1860,7 +1873,7 @@ namespace Roadie.Api.Services bytes = ms.ToArray(); } - return await SaveImageBytes(user, id, bytes); + return await SaveImageBytes(user, id, bytes).ConfigureAwait(false); } private async Task> ReleaseByIdAction(Guid id, IEnumerable includes = null) @@ -1872,7 +1885,7 @@ namespace Roadie.Api.Services sw.Start(); tsw.Restart(); - var release = await GetRelease(id); + var release = await GetRelease(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getRelease", tsw.ElapsedMilliseconds); @@ -1904,29 +1917,26 @@ namespace Roadie.Api.Services { tsw.Restart(); var submission = DbContext.Submissions.Include(x => x.User).FirstOrDefault(x => x.Id == release.SubmissionId); - if (submission != null) + if (submission != null && (!submission.User.IsPrivate ?? false)) { - if (!submission.User.IsPrivate ?? false) + result.Submission = new ReleaseSubmission { - result.Submission = new ReleaseSubmission + User = new DataToken { - User = new DataToken - { - Text = submission.User.UserName, - Value = submission.User.RoadieId.ToString() - }, - UserThumbnail = ImageHelper.MakeUserThumbnailImage(Configuration, HttpContext, submission.User.RoadieId), - SubmittedDate = submission.CreatedDate - }; - } + Text = submission.User.UserName, + Value = submission.User.RoadieId.ToString() + }, + UserThumbnail = ImageHelper.MakeUserThumbnailImage(Configuration, HttpContext, submission.User.RoadieId), + SubmittedDate = submission.CreatedDate + }; } tsw.Stop(); timings.Add("submissions", tsw.ElapsedMilliseconds); } - if (includes != null && includes.Any()) + if (includes?.Any() == true) { - if(includes.Contains("credits")) + if (includes.Contains("credits")) { tsw.Restart(); @@ -1936,17 +1946,18 @@ namespace Roadie.Api.Services from a in agg.DefaultIfEmpty() where c.ReleaseId == release.Id select new { c, cc, a }) - .ToListAsync()) + .ToListAsync().ConfigureAwait(false)) .Select(x => new CreditList { - Id = x.c.RoadieId, - Artist = x.a == null ? null : ArtistList.FromDataArtist(x.a, ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, x.a.RoadieId)), - Category = new DataToken { - Text =x.cc.Name, - Value = x.cc.RoadieId.ToString() - }, - CreditName = x.a?.Name ?? x.c.CreditToName, - Description = x.c.Description + Id = x.c.RoadieId, + Artist = x.a == null ? null : ArtistList.FromDataArtist(x.a, ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, x.a.RoadieId)), + Category = new DataToken + { + Text = x.cc.Name, + Value = x.cc.RoadieId.ToString() + }, + CreditName = x.a?.Name ?? x.c.CreditToName, + Description = x.c.Description }).ToArray(); tsw.Stop(); timings.Add("credits", tsw.ElapsedMilliseconds); @@ -1956,14 +1967,14 @@ namespace Roadie.Api.Services tsw.Restart(); var releaseGenreIds = release.Genres.Select(x => x.GenreId).ToArray(); result.Genres = (await (from g in DbContext.Genres - let releaseCount = (from rg in DbContext.ReleaseGenres - where rg.GenreId == g.Id - select rg.Id).Count() - let artistCount = (from rg in DbContext.ArtistGenres - where rg.GenreId == g.Id - select rg.Id).Count() - where releaseGenreIds.Contains(g.Id) - select new { g, releaseCount, artistCount }).ToListAsync()) + let releaseCount = (from rg in DbContext.ReleaseGenres + where rg.GenreId == g.Id + select rg.Id).Count() + let artistCount = (from rg in DbContext.ArtistGenres + where rg.GenreId == g.Id + select rg.Id).Count() + where releaseGenreIds.Contains(g.Id) + select new { g, releaseCount, artistCount }).ToListAsync().ConfigureAwait(false)) .Select(x => GenreList.FromDataGenre(x.g, ImageHelper.MakeGenreThumbnailImage(Configuration, HttpContext, x.g.RoadieId), x.artistCount, @@ -2020,7 +2031,7 @@ namespace Roadie.Api.Services var releaseImagesInFolder = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(releaseFolder), ImageType.ReleaseSecondary, SearchOption.TopDirectoryOnly); if (releaseImagesInFolder.Any()) { - result.Images = releaseImagesInFolder.Select((x, i) => ImageHelper.MakeFullsizeSecondaryImage(Configuration, HttpContext, id, ImageType.ReleaseSecondary, i)); + result.Images = releaseImagesInFolder.Select((_,i) => ImageHelper.MakeFullsizeSecondaryImage(Configuration, HttpContext, id, ImageType.ReleaseSecondary, i)); } tsw.Stop(); timings.Add("images", tsw.ElapsedMilliseconds); @@ -2033,7 +2044,7 @@ namespace Roadie.Api.Services { FilterToReleaseId = release.RoadieId }; - var r = await PlaylistService.List(pg); + var r = await PlaylistService.List(pg).ConfigureAwait(false); if (r.IsSuccess) result.Playlists = r.Rows.ToArray(); tsw.Stop(); timings.Add("playlists", tsw.ElapsedMilliseconds); @@ -2140,7 +2151,7 @@ namespace Roadie.Api.Services .Where(x => x.ReleaseId == release.Id) .OrderByDescending(x => x.CreatedDate) .ToArray(); - if (releaseComments.Any()) + if (releaseComments.Length > 0) { var comments = new List(); var commentIds = releaseComments.Select(x => x.Id).ToArray(); @@ -2227,7 +2238,7 @@ namespace Roadie.Api.Services File.WriteAllBytes(coverFileName, imageBytes); release.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); CacheManager.ClearRegion(release.CacheRegion); Logger.LogInformation($"SaveImageBytes `{release}` By User `{user}`"); } @@ -2242,7 +2253,7 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), + IsSuccess = errors.Count == 0, Data = ImageHelper.MakeThumbnailImage(Configuration, HttpContext, id, "release", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height, true), OperationTime = sw.ElapsedMilliseconds, Errors = errors diff --git a/Roadie.Api.Services/StatisticsService.cs b/Roadie.Api.Services/StatisticsService.cs index ccd011a..d22f908 100644 --- a/Roadie.Api.Services/StatisticsService.cs +++ b/Roadie.Api.Services/StatisticsService.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using data = Roadie.Library.Data; namespace Roadie.Api.Services { @@ -209,9 +208,9 @@ namespace Roadie.Api.Services year = SafeParser.ToNumber(x.Key), count = x.Count() }); - if (decadeInfos != null && decadeInfos.Any()) + if (decadeInfos?.Any() == true) { - var decadeInterval = 10; + const int decadeInterval = 10; var startingDecade = (decadeInfos.Min(x => x.year) / 10) * 10; var endingDecade = (decadeInfos.Max(x => x.year) / 10) * 10; for (int decade = startingDecade; decade <= endingDecade; decade += decadeInterval) @@ -236,7 +235,5 @@ namespace Roadie.Api.Services Data = result }); } - - } } \ No newline at end of file diff --git a/Roadie.Api.Services/TrackService.cs b/Roadie.Api.Services/TrackService.cs index 2972821..354cc6f 100644 --- a/Roadie.Api.Services/TrackService.cs +++ b/Roadie.Api.Services/TrackService.cs @@ -10,7 +10,6 @@ using Roadie.Library.Data.Context; using Roadie.Library.Encoding; using Roadie.Library.Enums; using Roadie.Library.Extensions; -using Roadie.Library.Identity; using Roadie.Library.Imaging; using Roadie.Library.MetaData.Audio; using Roadie.Library.Models; @@ -56,7 +55,7 @@ namespace Roadie.Api.Services public static long DetermineByteEndFromHeaders(IHeaderDictionary headers, long fileLength) { var defaultFileLength = fileLength - 1; - if (headers == null || !headers.Any(x => x.Key == "Range")) + if (headers?.Any(x => x.Key == "Range") != true) { return defaultFileLength; } @@ -87,7 +86,7 @@ namespace Roadie.Api.Services public static long DetermineByteStartFromHeaders(IHeaderDictionary headers) { - if (headers == null || !headers.Any(x => x.Key == "Range")) + if (headers?.Any(x => x.Key == "Range") != true) { return 0; } @@ -110,7 +109,7 @@ namespace Roadie.Api.Services return result; } - public async Task> ById(Library.Models.Users.User roadieUser, Guid id, IEnumerable includes) + public async Task> ById(User roadieUser, Guid id, IEnumerable includes) { var timings = new Dictionary(); var tsw = new Stopwatch(); @@ -121,27 +120,27 @@ namespace Roadie.Api.Services var result = await CacheManager.GetAsync(cacheKey, async () => { tsw.Restart(); - var rr = await TrackByIdAction(id, includes); + var rr = await TrackByIdAction(id, includes).ConfigureAwait(false); tsw.Stop(); timings.Add("TrackByIdAction", tsw.ElapsedMilliseconds); return rr; - }, data.Track.CacheRegionUrn(id)); + }, data.Track.CacheRegionUrn(id)).ConfigureAwait(false); if (result?.Data != null && roadieUser != null) { tsw.Restart(); - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); tsw.Stop(); timings.Add("getUser", tsw.ElapsedMilliseconds); tsw.Restart(); - var track = await GetTrack(id); + var track = await GetTrack(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getTrack", tsw.ElapsedMilliseconds); result.Data.TrackPlayUrl = MakeTrackPlayUrl(user, HttpContext.BaseUrl, track.RoadieId); tsw.Restart(); - var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Track); + var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Track).ConfigureAwait(false); if (userBookmarkResult.IsSuccess) { result.Data.UserBookmarked = userBookmarkResult?.Rows?.FirstOrDefault(x => x?.Bookmark?.Value == track?.RoadieId.ToString()) != null; @@ -175,8 +174,7 @@ namespace Roadie.Api.Services select cr).ToArray(); foreach (var comment in result.Data.Comments) { - var userCommentReaction = - userCommentReactions.FirstOrDefault(x => x.CommentId == comment.DatabaseId); + var userCommentReaction = Array.Find(userCommentReactions, x => x.CommentId == comment.DatabaseId); comment.IsDisliked = userCommentReaction?.ReactionValue == CommentReaction.Dislike; comment.IsLiked = userCommentReaction?.ReactionValue == CommentReaction.Like; } @@ -197,7 +195,7 @@ namespace Roadie.Api.Services }; } - public async Task> List(PagedRequest request, Library.Models.Users.User roadieUser, bool? doRandomize = false, Guid? releaseId = null) + public async Task> List(PagedRequest request, User roadieUser, bool? doRandomize = false, Guid? releaseId = null) { try { @@ -226,17 +224,17 @@ namespace Roadie.Api.Services if (request.FilterToPlaylistId.HasValue) { var playlistTrackInfos = await (from plt in DbContext.PlaylistTracks - join p in DbContext.Playlists on plt.PlayListId equals p.Id - join t in DbContext.Tracks on plt.TrackId equals t.Id - where p.RoadieId == request.FilterToPlaylistId.Value - orderby plt.ListNumber - select new - { - plt.ListNumber, - t.Id - }).ToArrayAsync(); + join p in DbContext.Playlists on plt.PlayListId equals p.Id + join t in DbContext.Tracks on plt.TrackId equals t.Id + where p.RoadieId == request.FilterToPlaylistId.Value + orderby plt.ListNumber + select new + { + plt.ListNumber, + t.Id + }).ToArrayAsync().ConfigureAwait(false); - rowCount = playlistTrackInfos.Count(); + rowCount = playlistTrackInfos.Length; playListTrackPositions = playlistTrackInfos .Skip(request.SkipValue) .Take(request.LimitValue) @@ -254,16 +252,16 @@ namespace Roadie.Api.Services request.Limit = roadieUser?.PlayerTrackLimit ?? 50; collectionTrackIds = await (from cr in DbContext.CollectionReleases - join c in DbContext.Collections on cr.CollectionId equals c.Id - join r in DbContext.Releases on cr.ReleaseId equals r.Id - join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId - join t in DbContext.Tracks on rm.Id equals t.ReleaseMediaId - where c.RoadieId == request.FilterToCollectionId.Value - orderby cr.ListNumber, rm.MediaNumber, t.TrackNumber - select t.Id) + join c in DbContext.Collections on cr.CollectionId equals c.Id + join r in DbContext.Releases on cr.ReleaseId equals r.Id + join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId + join t in DbContext.Tracks on rm.Id equals t.ReleaseMediaId + where c.RoadieId == request.FilterToCollectionId.Value + orderby cr.ListNumber, rm.MediaNumber, t.TrackNumber + select t.Id) .Skip(request.SkipValue) .Take(request.LimitValue) - .ToArrayAsync(); + .ToArrayAsync().ConfigureAwait(false); } IQueryable topTrackids = null; @@ -287,7 +285,7 @@ namespace Roadie.Api.Services if (doRandomize ?? false) { var randomLimit = roadieUser?.RandomReleaseLimit ?? request.LimitValue; - randomTrackData = await DbContext.RandomTrackIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly); + randomTrackData = await DbContext.RandomTrackIds(roadieUser?.Id ?? -1, randomLimit, request.FilterFavoriteOnly, request.FilterRatedOnly).ConfigureAwait(false); randomTrackIds = randomTrackData.Select(x => x.Value).ToArray(); rowCount = DbContext.Releases.Count(); } @@ -325,7 +323,9 @@ namespace Roadie.Api.Services if (filter.StartsWith('"') && filter.EndsWith('"')) { isEqualFilter = true; +#pragma warning disable IDE0057 // Use range operator request.Filter = filter.Substring(1, filter.Length - 2); +#pragma warning restore IDE0057 // Use range operator } } @@ -440,17 +440,14 @@ namespace Roadie.Api.Services } }; - if (!string.IsNullOrEmpty(request.FilterValue)) + if (!string.IsNullOrEmpty(request.FilterValue) && request.FilterValue.StartsWith("#")) { - if (request.FilterValue.StartsWith("#")) - { - // Find any releases by tags - var tagValue = request.FilterValue.Replace("#", ""); - resultQuery = resultQuery.Where(x => x.ti.Tags != null && x.ti.Tags.Contains(tagValue)); - } + // Find any releases by tags + var tagValue = request.FilterValue.Replace("#", ""); + resultQuery = resultQuery.Where(x => x.ti.Tags != null && x.ti.Tags.Contains(tagValue)); } - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); var result = resultQuery.Select(x => new TrackList { @@ -482,36 +479,33 @@ namespace Roadie.Api.Services }); string sortBy = null; - rowCount = rowCount ?? result.Count(); + rowCount ??= result.Count(); TrackList[] rows = null; if (!doRandomize ?? false) { - if (request.Action == Library.Models.Users.User.ActionKeyUserRated) + if (request.Action == User.ActionKeyUserRated) { sortBy = string.IsNullOrEmpty(request.Sort) ? request.OrderValue(new Dictionary { { "UserTrack.Rating", "DESC" }, { "MediaNumber", "ASC" }, { "TrackNumber", "ASC" } }) : request.OrderValue(); } + else if (request.Sort == "Rating") + { + // The request is to sort tracks by Rating if the artist only has a few tracks rated then order by those then order by played (put most popular after top rated) + sortBy = request.OrderValue(new Dictionary { { "Rating", request.Order }, { "PlayedCount", request.Order } }); + } else { - if (request.Sort == "Rating") - { - // The request is to sort tracks by Rating if the artist only has a few tracks rated then order by those then order by played (put most popular after top rated) - sortBy = request.OrderValue(new Dictionary { { "Rating", request.Order }, { "PlayedCount", request.Order } }); - } - else - { - sortBy = string.IsNullOrEmpty(request.Sort) - ? request.OrderValue(new Dictionary { { "Release.Release.Text", "ASC" }, { "MediaNumber", "ASC" }, { "TrackNumber", "ASC" } }) - : request.OrderValue(); - } + sortBy = string.IsNullOrEmpty(request.Sort) + ? request.OrderValue(new Dictionary { { "Release.Release.Text", "ASC" }, { "MediaNumber", "ASC" }, { "TrackNumber", "ASC" } }) + : request.OrderValue(); } } if (doRandomize ?? false) { - var resultData = await result.ToArrayAsync(); + var resultData = await result.ToArrayAsync().ConfigureAwait(false); rows = (from r in resultData join ra in randomTrackData on r.DatabaseId equals ra.Value orderby ra.Key @@ -524,18 +518,18 @@ namespace Roadie.Api.Services .OrderBy(sortBy) .Skip(request.SkipValue) .Take(request.LimitValue) - .ToArrayAsync(); + .ToArrayAsync().ConfigureAwait(false); } - if (rows.Any() && roadieUser != null) + if (rows.Length > 0 && roadieUser != null) { var rowIds = rows.Select(x => x.DatabaseId).ToArray(); var userTrackRatings = await (from ut in DbContext.UserTracks - where ut.UserId == roadieUser.Id - where rowIds.Contains(ut.TrackId) - select ut).ToArrayAsync(); + where ut.UserId == roadieUser.Id + where rowIds.Contains(ut.TrackId) + select ut).ToArrayAsync().ConfigureAwait(false); foreach (var userTrackRating in userTrackRatings) { - var row = rows.FirstOrDefault(x => x.DatabaseId == userTrackRating.TrackId); + var row = Array.Find(rows, x => x.DatabaseId == userTrackRating.TrackId); if (row != null) { row.UserRating = new UserTrack @@ -551,9 +545,9 @@ namespace Roadie.Api.Services var releaseIds = rows.Select(x => x.Release.DatabaseId).Distinct().ToArray(); var userReleaseRatings = await (from ur in DbContext.UserReleases - where ur.UserId == roadieUser.Id - where releaseIds.Contains(ur.ReleaseId) - select ur).ToArrayAsync(); + where ur.UserId == roadieUser.Id + where releaseIds.Contains(ur.ReleaseId) + select ur).ToArrayAsync().ConfigureAwait(false); foreach (var userReleaseRating in userReleaseRatings) { foreach (var row in rows.Where(x => x.Release.DatabaseId == userReleaseRating.ReleaseId)) @@ -563,12 +557,12 @@ namespace Roadie.Api.Services } var artistIds = rows.Select(x => x.Artist.DatabaseId).ToArray(); - if (artistIds != null && artistIds.Any()) + if (artistIds?.Any() == true) { var userArtistRatings = await (from ua in DbContext.UserArtists - where ua.UserId == roadieUser.Id - where artistIds.Contains(ua.ArtistId) - select ua).ToArrayAsync(); + where ua.UserId == roadieUser.Id + where artistIds.Contains(ua.ArtistId) + select ua).ToArrayAsync().ConfigureAwait(false); foreach (var userArtistRating in userArtistRatings) { foreach (var artistTrack in rows.Where( @@ -580,13 +574,13 @@ namespace Roadie.Api.Services } var trackArtistIds = rows.Where(x => x.TrackArtist != null).Select(x => x.TrackArtist.DatabaseId).ToArray(); - if (trackArtistIds != null && trackArtistIds.Any()) + if (trackArtistIds?.Any() == true) { var userTrackArtistRatings = await (from ua in DbContext.UserArtists - where ua.UserId == roadieUser.Id - where trackArtistIds.Contains(ua.ArtistId) - select ua).ToArrayAsync(); - if (userTrackArtistRatings != null && userTrackArtistRatings.Any()) + where ua.UserId == roadieUser.Id + where trackArtistIds.Contains(ua.ArtistId) + select ua).ToArrayAsync().ConfigureAwait(false); + if (userTrackArtistRatings?.Any() == true) { foreach (var userTrackArtistRating in userTrackArtistRatings) { @@ -601,21 +595,21 @@ namespace Roadie.Api.Services } } - if (rows.Any()) + if (rows.Length > 0) { var rowIds = rows.Select(x => x.DatabaseId).ToArray(); var favoriteUserTrackRatings = await (from ut in DbContext.UserTracks - where ut.IsFavorite ?? false - where rowIds.Contains(ut.TrackId) - select ut).ToArrayAsync(); + where ut.IsFavorite ?? false + where rowIds.Contains(ut.TrackId) + select ut).ToArrayAsync().ConfigureAwait(false); foreach (var row in rows) { - row.FavoriteCount = favoriteUserTrackRatings.Where(x => x.TrackId == row.DatabaseId).Count(); + row.FavoriteCount = favoriteUserTrackRatings.Count(x => x.TrackId == row.DatabaseId); row.TrackNumber = playListTrackPositions.ContainsKey(row.DatabaseId) ? playListTrackPositions[row.DatabaseId] : row.TrackNumber; } } - if(playListTrackPositions.Any()) + if (playListTrackPositions.Count > 0) { rows = rows.OrderBy(x => x.TrackNumber).ToArray(); } @@ -643,7 +637,7 @@ namespace Roadie.Api.Services /// /// Fast as possible check if exists and return minimum information on Track /// - public OperationResult StreamCheckAndInfo(Library.Models.Users.User roadieUser, Guid id) + public OperationResult StreamCheckAndInfo(User roadieUser, Guid id) { var track = DbContext.Tracks.FirstOrDefault(x => x.RoadieId == id); if (track == null) @@ -658,7 +652,7 @@ namespace Roadie.Api.Services }; } - public async Task> TrackStreamInfo(Guid trackId, long beginBytes, long endBytes, Library.Models.Users.User roadieUser) + public async Task> TrackStreamInfo(Guid trackId, long beginBytes, long endBytes, User roadieUser) { var track = DbContext.Tracks.FirstOrDefault(x => x.RoadieId == trackId); if (!(track?.IsValid ?? true)) @@ -673,7 +667,7 @@ namespace Roadie.Api.Services await AdminService.ScanRelease(new Library.Identity.User { Id = roadieUser.Id.Value - }, release.RoadieId, false, true); + }, release.RoadieId, false, true).ConfigureAwait(false); track = DbContext.Tracks.FirstOrDefault(x => x.RoadieId == trackId); } else @@ -712,7 +706,7 @@ namespace Roadie.Api.Services await AdminService.ScanRelease(new Library.Identity.User { Id = roadieUser.Id.Value - }, release.RoadieId, false, true); + }, release.RoadieId, false, true).ConfigureAwait(false); } track = DbContext.Tracks.FirstOrDefault(x => x.RoadieId == trackId); @@ -733,14 +727,12 @@ namespace Roadie.Api.Services if (!trackFileInfo.Exists) { track.UpdateTrackMissingFile(); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); return new OperationResult( $"TrackStreamInfo: TrackId [{trackId}] Unable to Find Track [{trackFileInfo.FullName}]"); } } - var disableCaching = true; - var contentDurationTimeSpan = TimeSpan.FromMilliseconds(track.Duration ?? 0); var info = new TrackStreamInfo { @@ -761,19 +753,11 @@ namespace Roadie.Api.Services info.IsFullRequest = beginBytes == 0 && endBytes == trackFileInfo.Length - 1; info.IsEndRangeRequest = beginBytes > 0 && endBytes != trackFileInfo.Length - 1; info.LastModified = (track.LastUpdated ?? track.CreatedDate).ToString("R"); - if (!disableCaching) - { - var cacheTimeout = 86400; // 24 hours - info.CacheControl = $"public, max-age={cacheTimeout.ToString()} "; - info.Expires = DateTime.UtcNow.AddMinutes(cacheTimeout).ToString("R"); - info.Etag = track.Etag; - } - else - { - info.CacheControl = "no-store, must-revalidate, no-cache, max-age=0"; - info.Pragma = "no-cache"; - info.Expires = "Mon, 01 Jan 1990 00:00:00 GMT"; - } + + info.CacheControl = "no-store, must-revalidate, no-cache, max-age=0"; + info.Pragma = "no-cache"; + info.Expires = "Mon, 01 Jan 1990 00:00:00 GMT"; + var bytesToRead = (int)(endBytes - beginBytes) + 1; var trackBytes = new byte[bytesToRead]; using (var fs = trackFileInfo.OpenRead()) @@ -797,9 +781,8 @@ namespace Roadie.Api.Services }; } - public async Task> UpdateTrack(Library.Models.Users.User user, Track model) + public async Task> UpdateTrack(User user, Track model) { - var didChangeTrack = false; var sw = new Stopwatch(); sw.Start(); var errors = new List(); @@ -829,7 +812,7 @@ namespace Roadie.Api.Services track.MusicBrainzId = model.MusicBrainzId; track.SpotifyId = model.SpotifyId; track.Tags = model.TagsList.ToDelimitedList(); - track.PartTitles = model.PartTitlesList == null || !model.PartTitlesList.Any() + track.PartTitles = model.PartTitlesList?.Any() != true ? null : string.Join("\n", model.PartTitlesList); @@ -839,7 +822,7 @@ namespace Roadie.Api.Services var artistId = SafeParser.ToGuid(model.TrackArtistToken.Value); if (artistId.HasValue) { - trackArtist = await GetArtist(artistId.Value); + trackArtist = await GetArtist(artistId.Value).ConfigureAwait(false); if (trackArtist != null) { track.ArtistId = trackArtist.Id; @@ -851,16 +834,15 @@ namespace Roadie.Api.Services track.ArtistId = null; } - if(model.Credits == null || !model.Credits.Any()) + if (model.Credits?.Any() != true) { // Delete all existing credits for track var trackCreditsToDelete = (from c in DbContext.Credits where c.TrackId == track.Id select c).ToArray(); DbContext.Credits.RemoveRange(trackCreditsToDelete); - } - else if(model.Credits != null && model.Credits.Any()) + else if (model.Credits?.Any() == true) { var trackCreditIds = model.Credits.Select(x => x.Id).ToArray(); // Delete any credits not given in model (removed by edit operation) @@ -870,12 +852,12 @@ namespace Roadie.Api.Services select c).ToArray(); DbContext.Credits.RemoveRange(trackCreditsToDelete); // Update any existing - foreach(var credit in model.Credits) + foreach (var credit in model.Credits) { var trackCredit = DbContext.Credits.FirstOrDefault(x => x.RoadieId == credit.Id); - if(trackCredit == null) + if (trackCredit == null) { - // Add new + // Add new trackCredit = new data.Credit { TrackId = track.Id, @@ -886,7 +868,7 @@ namespace Roadie.Api.Services data.Artist artistForCredit = null; if (credit.Artist != null) { - artistForCredit = await GetArtist(credit.Artist.Id); + artistForCredit = await GetArtist(credit.Artist.Id).ConfigureAwait(false); } var creditCategory = DbContext.CreditCategory.FirstOrDefault(x => x.RoadieId.ToString() == credit.Category.Value); trackCredit.CreditCategoryId = creditCategory.Id; @@ -917,10 +899,10 @@ namespace Roadie.Api.Services File.Move(originalFilename, track.PathToTrack(Configuration)); } track.LastUpdated = now; - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); var trackFileInfo = new FileInfo(track.PathToTrack(Configuration)); - var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo); + var audioMetaData = await AudioMetaDataHelper.GetInfo(trackFileInfo).ConfigureAwait(false); if (audioMetaData != null) { audioMetaData.Title = track.Title; @@ -931,7 +913,7 @@ namespace Roadie.Api.Services AudioMetaDataHelper.WriteTags(audioMetaData, trackFileInfo); } CacheManager.ClearRegion(track.CacheRegion); - Logger.LogInformation($"UpdateTrack `{track}` By User `{user}`: Edited Track [{didChangeTrack}]"); + Logger.LogInformation($"UpdateTrack `{track}` By User `{user}`"); } catch (Exception ex) { @@ -943,8 +925,8 @@ namespace Roadie.Api.Services return new OperationResult { - IsSuccess = !errors.Any(), - Data = !errors.Any(), + IsSuccess = errors.Count == 0, + Data = errors.Count == 0, OperationTime = sw.ElapsedMilliseconds, Errors = errors }; @@ -959,7 +941,7 @@ namespace Roadie.Api.Services sw.Start(); tsw.Restart(); - var track = await GetTrack(id); + var track = await GetTrack(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getTrack", tsw.ElapsedMilliseconds); @@ -1004,24 +986,25 @@ namespace Roadie.Api.Services timings.Add("trackArtist", tsw.ElapsedMilliseconds); } - if (includes != null && includes.Any()) + if (includes?.Any() == true) { if (includes.Contains("credits")) { tsw.Restart(); - result.Credits = (await(from c in DbContext.Credits - join cc in DbContext.CreditCategory on c.CreditCategoryId equals cc.Id - join a in DbContext.Artists on c.ArtistId equals a.Id into agg - from a in agg.DefaultIfEmpty() - where c.TrackId == track.Id - select new { c, cc, a }) - .ToListAsync()) + result.Credits = (await (from c in DbContext.Credits + join cc in DbContext.CreditCategory on c.CreditCategoryId equals cc.Id + join a in DbContext.Artists on c.ArtistId equals a.Id into agg + from a in agg.DefaultIfEmpty() + where c.TrackId == track.Id + select new { c, cc, a }) + .ToListAsync().ConfigureAwait(false)) .Select(x => new CreditList { Id = x.c.RoadieId, Artist = x.a == null ? null : ArtistList.FromDataArtist(x.a, ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, x.a.RoadieId)), - Category = new DataToken { + Category = new DataToken + { Text = x.cc.Name, Value = x.cc.RoadieId.ToString() }, @@ -1045,7 +1028,7 @@ namespace Roadie.Api.Services join ut in DbContext.UserTracks on t.Id equals ut.TrackId where t.Id == track.Id select ut).ToArray(); - if (userTracks != null && userTracks.Any()) + if (userTracks?.Any() == true) { result.Statistics.DislikedCount = userTracks.Count(x => x.IsDisliked ?? false); result.Statistics.FavoriteCount = userTracks.Count(x => x.IsFavorite ?? false); @@ -1059,7 +1042,7 @@ namespace Roadie.Api.Services tsw.Restart(); var trackComments = DbContext.Comments.Include(x => x.User).Where(x => x.TrackId == track.Id) .OrderByDescending(x => x.CreatedDate).ToArray(); - if (trackComments.Any()) + if (trackComments.Length > 0) { var comments = new List(); var commentIds = trackComments.Select(x => x.Id).ToArray(); diff --git a/Roadie.Api.Services/UserService.cs b/Roadie.Api.Services/UserService.cs index 3c4f290..ca4fc37 100644 --- a/Roadie.Api.Services/UserService.cs +++ b/Roadie.Api.Services/UserService.cs @@ -10,7 +10,6 @@ using Roadie.Library.Data.Context; using Roadie.Library.Encoding; using Roadie.Library.Enums; using Roadie.Library.Extensions; -using Roadie.Library.Identity; using Roadie.Library.Imaging; using Roadie.Library.MetaData.LastFm; using Roadie.Library.Models.Pagination; @@ -49,30 +48,21 @@ namespace Roadie.Api.Services { UserManager = userManager; LastFmHelper = lastFmHelper; - ; } - public async Task> ById(models.Users.User user, Guid id, IEnumerable includes, bool isAccountSettingsEdit = false) + public async Task> ById(User user, Guid id, IEnumerable includes, bool isAccountSettingsEdit = false) { - if (isAccountSettingsEdit) + if (isAccountSettingsEdit && user.UserId != id && !user.IsAdmin) { - if (user.UserId != id && !user.IsAdmin) + return new OperationResult("Access Denied") { - var r = new OperationResult("Access Denied") - { - IsAccessDeniedResult = true - }; - return r; - } + IsAccessDeniedResult = true + }; } var sw = Stopwatch.StartNew(); sw.Start(); var cacheKey = string.Format("urn:user_by_id_operation:{0}:{1}", id, isAccountSettingsEdit); - var result = await CacheManager.GetAsync(cacheKey, async () => - { - var rr = await UserByIdAction(id, includes); - return rr; - }, Library.Identity.User.CacheRegionUrn(id)); + var result = await CacheManager.GetAsync(cacheKey, async () => await UserByIdAction(id, includes).ConfigureAwait(false), Library.Identity.User.CacheRegionUrn(id)).ConfigureAwait(false); sw.Stop(); if (result?.Data != null) { @@ -83,7 +73,7 @@ namespace Roadie.Api.Services result.Data.ConcurrencyStamp = null; } } - return new OperationResult(result.Messages) + return new OperationResult(result.Messages) { Data = result?.Data, Errors = result?.Errors, @@ -93,7 +83,7 @@ namespace Roadie.Api.Services }; } - public Task> List(PagedRequest request) + public Task> List(PagedRequest request) { var sw = new Stopwatch(); sw.Start(); @@ -103,7 +93,7 @@ namespace Roadie.Api.Services where ut.UserId == u.Id select ut.LastPlayed).Max() where request.FilterValue.Length == 0 || - request.FilterValue.Length > 0 && u.UserName.Contains(request.FilterValue) + (request.FilterValue.Length > 0 && u.UserName.Contains(request.FilterValue)) select new UserList { DatabaseId = u.Id, @@ -132,7 +122,7 @@ namespace Roadie.Api.Services : request.OrderValue(); rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray(); - if (rows.Any()) + if (rows.Length > 0) { foreach (var row in rows) { @@ -147,22 +137,22 @@ namespace Roadie.Api.Services .ToArray(); row.Statistics = new UserStatistics { - RatedArtists = userArtists.Where(x => x.Rating > 0).Count(), - FavoritedArtists = userArtists.Where(x => x.IsFavorite ?? false).Count(), - DislikedArtists = userArtists.Where(x => x.IsDisliked ?? false).Count(), - RatedReleases = userReleases.Where(x => x.Rating > 0).Count(), - FavoritedReleases = userReleases.Where(x => x.IsFavorite ?? false).Count(), - DislikedReleases = userReleases.Where(x => x.IsDisliked ?? false).Count(), - RatedTracks = userTracks.Where(x => x.Rating > 0).Count(), + RatedArtists = userArtists.Count(x => x.Rating > 0), + FavoritedArtists = userArtists.Count(x => x.IsFavorite ?? false), + DislikedArtists = userArtists.Count(x => x.IsDisliked ?? false), + RatedReleases = userReleases.Count(x => x.Rating > 0), + FavoritedReleases = userReleases.Count(x => x.IsFavorite ?? false), + DislikedReleases = userReleases.Count(x => x.IsDisliked ?? false), + RatedTracks = userTracks.Count(x => x.Rating > 0), PlayedTracks = userTracks.Where(x => x.PlayedCount.HasValue).Select(x => x.PlayedCount).Sum(), - FavoritedTracks = userTracks.Where(x => x.IsFavorite ?? false).Count(), - DislikedTracks = userTracks.Where(x => x.IsDisliked ?? false).Count() + FavoritedTracks = userTracks.Count(x => x.IsFavorite ?? false), + DislikedTracks = userTracks.Count(x => x.IsDisliked ?? false) }; } } sw.Stop(); - return Task.FromResult(new Library.Models.Pagination.PagedResult + return Task.FromResult(new models.Pagination.PagedResult { TotalCount = rowCount, CurrentPage = request.PageValue, @@ -172,16 +162,16 @@ namespace Roadie.Api.Services }); } - public async Task> DeleteAllBookmarks(models.Users.User roadieUser) + public async Task> DeleteAllBookmarks(User roadieUser) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } DbContext.Bookmarks.RemoveRange(DbContext.Bookmarks.Where(x => x.UserId == user.Id)); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); CacheManager.ClearRegion(user.CacheRegion); @@ -192,19 +182,20 @@ namespace Roadie.Api.Services }; } - public async Task> SetArtistBookmark(Guid artistId, models.Users.User roadieUser, bool isBookmarked) + public async Task> SetArtistBookmark(Guid artistId, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var artist = await GetArtist(artistId); + var artist = await GetArtist(artistId).ConfigureAwait(false); if (artist == null) { return new OperationResult(true, $"Invalid Artist [{artistId}]"); } - var result = await SetBookmark(user, BookmarkType.Artist, artist.Id, isBookmarked); + + await SetBookmark(user, BookmarkType.Artist, artist.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(artist.CacheRegion); @@ -215,49 +206,49 @@ namespace Roadie.Api.Services }; } - public async Task> SetArtistDisliked(Guid artistId, models.Users.User roadieUser, bool isDisliked) + public async Task> SetArtistDisliked(Guid artistId, User roadieUser, bool isDisliked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleArtistDisliked(artistId, user, isDisliked); + return await ToggleArtistDisliked(artistId, user, isDisliked).ConfigureAwait(false); } - public async Task> SetArtistFavorite(Guid artistId, models.Users.User roadieUser, bool isFavorite) + public async Task> SetArtistFavorite(Guid artistId, User roadieUser, bool isFavorite) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleArtistFavorite(artistId, user, isFavorite); + return await ToggleArtistFavorite(artistId, user, isFavorite).ConfigureAwait(false); } - public async Task> SetArtistRating(Guid artistId, models.Users.User roadieUser, short rating) + public async Task> SetArtistRating(Guid artistId, User roadieUser, short rating) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await SetArtistRating(artistId, user, rating); + return await SetArtistRating(artistId, user, rating).ConfigureAwait(false); } - public async Task> SetCollectionBookmark(Guid collectionId, models.Users.User roadieUser, bool isBookmarked) + public async Task> SetCollectionBookmark(Guid collectionId, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var collection = await GetCollection(collectionId); + var collection = await GetCollection(collectionId).ConfigureAwait(false); if (collection == null) { return new OperationResult(true, $"Invalid Collection [{collectionId}]"); } - var result = await SetBookmark(user, BookmarkType.Collection, collection.Id, isBookmarked); + await SetBookmark(user, BookmarkType.Collection, collection.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(collection.CacheRegion); @@ -268,19 +259,19 @@ namespace Roadie.Api.Services }; } - public async Task> SetLabelBookmark(Guid labelId, models.Users.User roadieUser, bool isBookmarked) + public async Task> SetLabelBookmark(Guid labelId, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var label = await GetLabel(labelId); + var label = await GetLabel(labelId).ConfigureAwait(false); if (label == null) { return new OperationResult(true, $"Invalid Label [{labelId}]"); } - var result = await SetBookmark(user, BookmarkType.Label, label.Id, isBookmarked); + await SetBookmark(user, BookmarkType.Label, label.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(label.CacheRegion); @@ -291,20 +282,20 @@ namespace Roadie.Api.Services }; } - public async Task> SetPlaylistBookmark(Guid playlistId, models.Users.User roadieUser, + public async Task> SetPlaylistBookmark(Guid playlistId, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var playlist = await GetPlaylist(playlistId); + var playlist = await GetPlaylist(playlistId).ConfigureAwait(false); if (playlist == null) { return new OperationResult(true, $"Invalid Playlist [{playlistId}]"); } - await SetBookmark(user, BookmarkType.Playlist, playlist.Id, isBookmarked); + await SetBookmark(user, BookmarkType.Playlist, playlist.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(playlist.CacheRegion); @@ -315,19 +306,19 @@ namespace Roadie.Api.Services }; } - public async Task> SetReleaseBookmark(Guid releaseid, models.Users.User roadieUser, bool isBookmarked) + public async Task> SetReleaseBookmark(Guid releaseid, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var release = await GetRelease(releaseid); + var release = await GetRelease(releaseid).ConfigureAwait(false); if (release == null) { return new OperationResult(true, $"Invalid Release [{releaseid}]"); } - await SetBookmark(user, BookmarkType.Release, release.Id, isBookmarked); + await SetBookmark(user, BookmarkType.Release, release.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(release.CacheRegion); @@ -338,49 +329,49 @@ namespace Roadie.Api.Services }; } - public async Task> SetReleaseDisliked(Guid releaseId, models.Users.User roadieUser, bool isDisliked) + public async Task> SetReleaseDisliked(Guid releaseId, User roadieUser, bool isDisliked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleReleaseDisliked(releaseId, user, isDisliked); + return await ToggleReleaseDisliked(releaseId, user, isDisliked).ConfigureAwait(false); } - public async Task> SetReleaseFavorite(Guid releaseId, models.Users.User roadieUser, bool isFavorite) + public async Task> SetReleaseFavorite(Guid releaseId, User roadieUser, bool isFavorite) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleReleaseFavorite(releaseId, user, isFavorite); + return await ToggleReleaseFavorite(releaseId, user, isFavorite).ConfigureAwait(false); } - public async Task> SetReleaseRating(Guid releaseId, models.Users.User roadieUser, short rating) + public async Task> SetReleaseRating(Guid releaseId, User roadieUser, short rating) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await base.SetReleaseRating(releaseId, user, rating); + return await SetReleaseRating(releaseId, user, rating).ConfigureAwait(false); } - public async Task> SetTrackBookmark(Guid trackId, models.Users.User roadieUser, bool isBookmarked) + public async Task> SetTrackBookmark(Guid trackId, User roadieUser, bool isBookmarked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - var track = await GetTrack(trackId); + var track = await GetTrack(trackId).ConfigureAwait(false); if (track == null) { return new OperationResult(true, $"Invalid Track [{trackId}]"); } - await SetBookmark(user, BookmarkType.Track, track.Id, isBookmarked); + await SetBookmark(user, BookmarkType.Track, track.Id, isBookmarked).ConfigureAwait(false); CacheManager.ClearRegion(track.CacheRegion); @@ -391,31 +382,31 @@ namespace Roadie.Api.Services }; } - public async Task> SetTrackDisliked(Guid trackId, models.Users.User roadieUser, bool isDisliked) + public async Task> SetTrackDisliked(Guid trackId, User roadieUser, bool isDisliked) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleTrackDisliked(trackId, user, isDisliked); + return await ToggleTrackDisliked(trackId, user, isDisliked).ConfigureAwait(false); } - public async Task> SetTrackFavorite(Guid trackId, models.Users.User roadieUser, bool isFavorite) + public async Task> SetTrackFavorite(Guid trackId, User roadieUser, bool isFavorite) { - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); if (user == null) { return new OperationResult(true, $"Invalid User [{roadieUser}]"); } - return await ToggleTrackFavorite(trackId, user, isFavorite); + return await ToggleTrackFavorite(trackId, user, isFavorite).ConfigureAwait(false); } - public async Task> SetTrackRating(Guid trackId, models.Users.User roadieUser, short rating) + public async Task> SetTrackRating(Guid trackId, User roadieUser, short rating) { var timings = new Dictionary(); var sw = Stopwatch.StartNew(); - var user = await GetUser(roadieUser.UserId); + var user = await GetUser(roadieUser.UserId).ConfigureAwait(false); sw.Stop(); timings.Add("GetUser", sw.ElapsedMilliseconds); @@ -424,7 +415,7 @@ namespace Roadie.Api.Services return new OperationResult(true, $"Invalid User [{roadieUser}]"); } sw.Start(); - var result = await base.SetTrackRating(trackId, user, rating); + var result = await SetTrackRating(trackId, user, rating).ConfigureAwait(false); sw.Stop(); timings.Add("SetTrackRating", sw.ElapsedMilliseconds); @@ -434,19 +425,21 @@ namespace Roadie.Api.Services return result; } - public async Task> UpdateIntegrationGrant(Guid userId, string integrationName, - string token) + public async Task> UpdateIntegrationGrant(Guid userId, string integrationName, string token) { var user = DbContext.Users.FirstOrDefault(x => x.RoadieId == userId); if (user == null) { return new OperationResult(true, $"User Not Found [{userId}]"); } - if (integrationName == "lastfm") return await UpdateLastFMSessionKey(user, token); + if (integrationName == "lastfm") + { + return await UpdateLastFMSessionKey(user, token).ConfigureAwait(false); + } throw new NotImplementedException(); } - public async Task> UpdateProfile(models.Users.User userPerformingUpdate, models.Users.User userBeingUpdatedModel) + public async Task> UpdateProfile(User userPerformingUpdate, User userBeingUpdatedModel) { var user = DbContext.Users.FirstOrDefault(x => x.RoadieId == userBeingUpdatedModel.UserId); if (user == null) @@ -455,11 +448,10 @@ namespace Roadie.Api.Services } if (user.Id != userPerformingUpdate.Id && !userPerformingUpdate.IsAdmin) { - var r = new OperationResult("Access Denied") + return new OperationResult("Access Denied") { IsAccessDeniedResult = true }; - return r; } // Check concurrency stamp if (user.ConcurrencyStamp != userBeingUpdatedModel.ConcurrencyStamp) @@ -509,12 +501,9 @@ namespace Roadie.Api.Services user.ConcurrencyStamp = Guid.NewGuid().ToString(); user.DefaultRowsPerPage = userBeingUpdatedModel.DefaultRowsPerPage; - if (didChangeName) + if (didChangeName && File.Exists(oldPathToImage)) { - if (File.Exists(oldPathToImage)) - { - File.Move(oldPathToImage, user.PathToImage(Configuration)); - } + File.Move(oldPathToImage, user.PathToImage(Configuration)); } if (!string.IsNullOrEmpty(userBeingUpdatedModel.AvatarData)) @@ -529,20 +518,24 @@ namespace Roadie.Api.Services } } - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); if (!string.IsNullOrEmpty(userBeingUpdatedModel.Password) && !string.IsNullOrEmpty(userBeingUpdatedModel.PasswordConfirmation)) { if (userBeingUpdatedModel.Password != userBeingUpdatedModel.PasswordConfirmation) + { return new OperationResult { Errors = new List { new Exception("Password does not match confirmation") } }; - var resetToken = await UserManager.GeneratePasswordResetTokenAsync(user); + } + + var resetToken = await UserManager.GeneratePasswordResetTokenAsync(user).ConfigureAwait(false); var identityResult = - await UserManager.ResetPasswordAsync(user, resetToken, userBeingUpdatedModel.Password); + await UserManager.ResetPasswordAsync(user, resetToken, userBeingUpdatedModel.Password).ConfigureAwait(false); if (!identityResult.Succeeded) + { return new OperationResult { Errors = identityResult.Errors != null @@ -550,6 +543,7 @@ namespace Roadie.Api.Services new Exception($"Code [{x.Code}], Description [{x.Description}]")) : new List { new Exception("Unable to reset password") } }; + } } CacheManager.ClearRegion(Library.Identity.User.CacheRegionUrn(user.RoadieId)); @@ -574,7 +568,7 @@ namespace Roadie.Api.Services if (bookmark != null) { DbContext.Bookmarks.Remove(bookmark); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } } else @@ -589,8 +583,8 @@ namespace Roadie.Api.Services BookmarkType = bookmarktype, CreatedDate = DateTime.UtcNow, Status = Statuses.Ok - }); - await DbContext.SaveChangesAsync(); + }).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -605,19 +599,22 @@ namespace Roadie.Api.Services private async Task> UpdateLastFMSessionKey(Library.Identity.User user, string token) { - var lastFmSessionKeyResult = await LastFmHelper.GetSessionKeyForUserToken(token); + var lastFmSessionKeyResult = await LastFmHelper.GetSessionKeyForUserToken(token).ConfigureAwait(false); if (!lastFmSessionKeyResult.IsSuccess) return new OperationResult(false, $"Unable to Get LastFM Session Key For Token [{token}]"); // Check concurrency stamp if (user.ConcurrencyStamp != user.ConcurrencyStamp) + { return new OperationResult { Errors = new List { new Exception("User data is stale.") } }; + } + user.LastFMSessionKey = lastFmSessionKeyResult.Data; user.LastUpdated = DateTime.UtcNow; user.ConcurrencyStamp = Guid.NewGuid().ToString(); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); CacheManager.ClearRegion(Library.Identity.User.CacheRegionUrn(user.RoadieId)); @@ -630,103 +627,100 @@ namespace Roadie.Api.Services }; } - private async Task> UserByIdAction(Guid id, IEnumerable includes) + private async Task> UserByIdAction(Guid id, IEnumerable includes) { var timings = new Dictionary(); var tsw = new Stopwatch(); tsw.Restart(); - var user = await GetUser(id); + var user = await GetUser(id).ConfigureAwait(false); tsw.Stop(); timings.Add("getUser", tsw.ElapsedMilliseconds); if (user == null) { - return new OperationResult(true, string.Format("User Not Found [{0}]", id)); + return new OperationResult(true, string.Format("User Not Found [{0}]", id)); } tsw.Restart(); - var model = user.Adapt(); + var model = user.Adapt(); model.MediumThumbnail = ImageHelper.MakeThumbnailImage(Configuration, HttpContext, id, "user", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height); model.IsAdmin = user.UserRoles?.Any(x => x.Role?.NormalizedName == "ADMIN") ?? false; - model.IsEditor = model.IsAdmin ? true : user.UserRoles?.Any(x => x.Role?.NormalizedName == "EDITOR") ?? false; + model.IsEditor = model.IsAdmin || (user.UserRoles?.Any(x => x.Role?.NormalizedName == "EDITOR") ?? false); tsw.Stop(); timings.Add("adapt", tsw.ElapsedMilliseconds); - if (includes != null && includes.Any()) + if (includes?.Any() == true && includes.Contains("stats")) { - if (includes.Contains("stats")) - { - tsw.Restart(); - var userArtists = DbContext.UserArtists.Include(x => x.Artist).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserArtist[0]; - var userReleases = DbContext.UserReleases.Include(x => x.Release).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserRelease[0]; - var userTracks = DbContext.UserTracks.Include(x => x.Track).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserTrack[0]; + tsw.Restart(); + var userArtists = DbContext.UserArtists.Include(x => x.Artist).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserArtist[0]; + var userReleases = DbContext.UserReleases.Include(x => x.Release).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserRelease[0]; + var userTracks = DbContext.UserTracks.Include(x => x.Track).Where(x => x.UserId == user.Id).ToArray() ?? new data.UserTrack[0]; - var mostPlayedArtist = await DbContext.MostPlayedArtist(user.Id); - var mostPlayedRelease = await DbContext.MostPlayedRelease(user.Id); - var lastPlayedTrack = await GetTrack((await DbContext.MostPlayedTrack(user.Id))?.RoadieId ?? Guid.Empty); - var mostPlayedTrack = await GetTrack((await DbContext.LastPlayedTrack(user.Id))?.RoadieId ?? Guid.Empty); - model.Statistics = new UserStatistics - { - LastPlayedTrack = lastPlayedTrack == null - ? null - : models.TrackList.FromDataTrack( - MakeTrackPlayUrl(user, HttpContext.BaseUrl, lastPlayedTrack.RoadieId), - lastPlayedTrack, - lastPlayedTrack.ReleaseMedia.MediaNumber, - lastPlayedTrack.ReleaseMedia.Release, - lastPlayedTrack.ReleaseMedia.Release.Artist, - lastPlayedTrack.TrackArtist, - HttpContext.BaseUrl, - ImageHelper.MakeTrackThumbnailImage(Configuration, HttpContext, lastPlayedTrack.RoadieId), - ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, lastPlayedTrack.ReleaseMedia.Release.RoadieId), - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, lastPlayedTrack.ReleaseMedia.Release.Artist.RoadieId), - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, lastPlayedTrack.TrackArtist == null - ? null - : (Guid?)lastPlayedTrack.TrackArtist.RoadieId)), - MostPlayedArtist = mostPlayedArtist == null - ? null - : models.ArtistList.FromDataArtist(mostPlayedArtist, - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedArtist.RoadieId)), - MostPlayedRelease = mostPlayedRelease == null - ? null - : ReleaseList.FromDataRelease(mostPlayedRelease, - mostPlayedRelease.Artist, - HttpContext.BaseUrl, - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedRelease.Artist.RoadieId), - ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, mostPlayedRelease.RoadieId)), - MostPlayedTrack = mostPlayedTrack == null - ? null - : models.TrackList.FromDataTrack( - MakeTrackPlayUrl(user, HttpContext.BaseUrl, mostPlayedTrack.RoadieId), - mostPlayedTrack, - mostPlayedTrack.ReleaseMedia.MediaNumber, - mostPlayedTrack.ReleaseMedia.Release, - mostPlayedTrack.ReleaseMedia.Release.Artist, - mostPlayedTrack.TrackArtist, - HttpContext.BaseUrl, - ImageHelper.MakeTrackThumbnailImage(Configuration, HttpContext, mostPlayedTrack.RoadieId), - ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, mostPlayedTrack.ReleaseMedia.Release.RoadieId), - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedTrack.ReleaseMedia.Release.Artist.RoadieId), - ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedTrack.TrackArtist == null - ? null - : (Guid?)mostPlayedTrack.TrackArtist.RoadieId)), - RatedArtists = userArtists.Where(x => x.Rating > 0).Count(), - FavoritedArtists = userArtists.Where(x => x.IsFavorite ?? false).Count(), - DislikedArtists = userArtists.Where(x => x.IsDisliked ?? false).Count(), - RatedReleases = userReleases.Where(x => x.Rating > 0).Count(), - FavoritedReleases = userReleases.Where(x => x.IsFavorite ?? false).Count(), - DislikedReleases = userReleases.Where(x => x.IsDisliked ?? false).Count(), - RatedTracks = userTracks.Where(x => x.Rating > 0).Count(), - PlayedTracks = userTracks.Where(x => x.PlayedCount.HasValue).Select(x => x.PlayedCount).Sum(), - FavoritedTracks = userTracks.Where(x => x.IsFavorite ?? false).Count(), - DislikedTracks = userTracks.Where(x => x.IsDisliked ?? false).Count() - }; - tsw.Stop(); - timings.Add("stats", tsw.ElapsedMilliseconds); - } + var mostPlayedArtist = await DbContext.MostPlayedArtist(user.Id).ConfigureAwait(false); + var mostPlayedRelease = await DbContext.MostPlayedRelease(user.Id).ConfigureAwait(false); + var lastPlayedTrack = await GetTrack((await DbContext.MostPlayedTrack(user.Id).ConfigureAwait(false))?.RoadieId ?? Guid.Empty).ConfigureAwait(false); + var mostPlayedTrack = await GetTrack((await DbContext.LastPlayedTrack(user.Id).ConfigureAwait(false))?.RoadieId ?? Guid.Empty).ConfigureAwait(false); + model.Statistics = new UserStatistics + { + LastPlayedTrack = lastPlayedTrack == null + ? null + : models.TrackList.FromDataTrack( + MakeTrackPlayUrl(user, HttpContext.BaseUrl, lastPlayedTrack.RoadieId), + lastPlayedTrack, + lastPlayedTrack.ReleaseMedia.MediaNumber, + lastPlayedTrack.ReleaseMedia.Release, + lastPlayedTrack.ReleaseMedia.Release.Artist, + lastPlayedTrack.TrackArtist, + HttpContext.BaseUrl, + ImageHelper.MakeTrackThumbnailImage(Configuration, HttpContext, lastPlayedTrack.RoadieId), + ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, lastPlayedTrack.ReleaseMedia.Release.RoadieId), + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, lastPlayedTrack.ReleaseMedia.Release.Artist.RoadieId), + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, lastPlayedTrack.TrackArtist == null + ? null + : (Guid?)lastPlayedTrack.TrackArtist.RoadieId)), + MostPlayedArtist = mostPlayedArtist == null + ? null + : models.ArtistList.FromDataArtist(mostPlayedArtist, + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedArtist.RoadieId)), + MostPlayedRelease = mostPlayedRelease == null + ? null + : ReleaseList.FromDataRelease(mostPlayedRelease, + mostPlayedRelease.Artist, + HttpContext.BaseUrl, + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedRelease.Artist.RoadieId), + ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, mostPlayedRelease.RoadieId)), + MostPlayedTrack = mostPlayedTrack == null + ? null + : models.TrackList.FromDataTrack( + MakeTrackPlayUrl(user, HttpContext.BaseUrl, mostPlayedTrack.RoadieId), + mostPlayedTrack, + mostPlayedTrack.ReleaseMedia.MediaNumber, + mostPlayedTrack.ReleaseMedia.Release, + mostPlayedTrack.ReleaseMedia.Release.Artist, + mostPlayedTrack.TrackArtist, + HttpContext.BaseUrl, + ImageHelper.MakeTrackThumbnailImage(Configuration, HttpContext, mostPlayedTrack.RoadieId), + ImageHelper.MakeReleaseThumbnailImage(Configuration, HttpContext, mostPlayedTrack.ReleaseMedia.Release.RoadieId), + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedTrack.ReleaseMedia.Release.Artist.RoadieId), + ImageHelper.MakeArtistThumbnailImage(Configuration, HttpContext, mostPlayedTrack.TrackArtist == null + ? null + : (Guid?)mostPlayedTrack.TrackArtist.RoadieId)), + RatedArtists = userArtists.Count(x => x.Rating > 0), + FavoritedArtists = userArtists.Count(x => x.IsFavorite ?? false), + DislikedArtists = userArtists.Count(x => x.IsDisliked ?? false), + RatedReleases = userReleases.Count(x => x.Rating > 0), + FavoritedReleases = userReleases.Count(x => x.IsFavorite ?? false), + DislikedReleases = userReleases.Count(x => x.IsDisliked ?? false), + RatedTracks = userTracks.Count(x => x.Rating > 0), + PlayedTracks = userTracks.Where(x => x.PlayedCount.HasValue).Select(x => x.PlayedCount).Sum(), + FavoritedTracks = userTracks.Count(x => x.IsFavorite ?? false), + DislikedTracks = userTracks.Count(x => x.IsDisliked ?? false) + }; + tsw.Stop(); + timings.Add("stats", tsw.ElapsedMilliseconds); } Logger.LogInformation($"ByIdAction: User `{ user }`: includes [{includes.ToCSV()}], timings: [{ timings.ToTimings() }]"); - return new OperationResult + return new OperationResult { IsSuccess = true, Data = model diff --git a/Roadie.Api/wwwroot/Images/Roadie.png b/Roadie.Api/wwwroot/Images/Roadie.png new file mode 100644 index 0000000000000000000000000000000000000000..beef01c932ecc8dbcf5ba49f84bfc1e8895fda67 GIT binary patch literal 104943 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