diff --git a/Roadie.Api.Library/Configuration/IRoadieSettings.cs b/Roadie.Api.Library/Configuration/IRoadieSettings.cs index 76b0bc1..b01d890 100644 --- a/Roadie.Api.Library/Configuration/IRoadieSettings.cs +++ b/Roadie.Api.Library/Configuration/IRoadieSettings.cs @@ -21,6 +21,7 @@ namespace Roadie.Library.Configuration string ImageFolder { get; set; } string LabelImageFolder { get; } string CollectionImageFolder { get; } + string GenreImageFolder { get; } string PlaylistImageFolder { get; } string UserImageFolder { get; } string ListenAddress { get; set; } @@ -47,5 +48,6 @@ namespace Roadie.Library.Configuration bool IsRegistrationClosed { get; set; } bool UseRegistrationTokens { get; set; } string SearchEngineReposFolder { get; set; } + short DefaultRowsPerPage { get; set; } } } \ No newline at end of file diff --git a/Roadie.Api.Library/Configuration/RoadieSettings.cs b/Roadie.Api.Library/Configuration/RoadieSettings.cs index f28ebf4..70cc500 100644 --- a/Roadie.Api.Library/Configuration/RoadieSettings.cs +++ b/Roadie.Api.Library/Configuration/RoadieSettings.cs @@ -63,6 +63,14 @@ namespace Roadie.Library.Configuration } } + public string GenreImageFolder + { + get + { + return Path.Combine(ImageFolder ?? LibraryFolder, "__roadie_images", "genres"); + } + } + public string PlaylistImageFolder { get @@ -133,6 +141,8 @@ namespace Roadie.Library.Configuration /// public string SearchEngineReposFolder { get; set; } + public short DefaultRowsPerPage { get; set; } + public RoadieSettings() { ArtistNameReplace = new Dictionary> @@ -152,6 +162,7 @@ namespace Roadie.Library.Configuration SiteName = "Roadie"; SmallImageSize = new ImageSize { Width = 160, Height = 160 }; ThumbnailImageSize = new ImageSize { Width = 80, Height = 80 }; + DefaultRowsPerPage = 12; SmtpFromAddress = "noreply@roadie.rocks"; SmtpPort = 587; diff --git a/Roadie.Api.Library/Data/Genre.cs b/Roadie.Api.Library/Data/Genre.cs index 142fdb7..92a6997 100644 --- a/Roadie.Api.Library/Data/Genre.cs +++ b/Roadie.Api.Library/Data/Genre.cs @@ -13,6 +13,22 @@ namespace Roadie.Library.Data [Column("name")] [MaxLength(100)] public string Name { get; set; } + [Column("description")] + [MaxLength(4000)] + public string Description { get; set; } + + [Column("alternateNames", TypeName = "text")] + [MaxLength(65535)] + public string AlternateNames { get; set; } + + [Column("tags", TypeName = "text")] + [MaxLength(65535)] + public string Tags { get; set; } + + [Column("thumbnail", TypeName = "blob")] + [MaxLength(65535)] + public byte[] Thumbnail { get; set; } + [Column("normalizedName")] [MaxLength(100)] public string NormalizedName { get; set; } public ICollection Releases { get; set; } @@ -22,6 +38,7 @@ namespace Roadie.Library.Data Releases = new HashSet(); Artists = new HashSet(); Comments = new HashSet(); + Status = Enums.Statuses.Ok; } } } \ No newline at end of file diff --git a/Roadie.Api.Library/Data/GenrePartial.cs b/Roadie.Api.Library/Data/GenrePartial.cs index 9ced395..020d392 100644 --- a/Roadie.Api.Library/Data/GenrePartial.cs +++ b/Roadie.Api.Library/Data/GenrePartial.cs @@ -1,4 +1,7 @@ -using System; +using Roadie.Library.Configuration; +using Roadie.Library.Extensions; +using System; +using System.IO; namespace Roadie.Library.Data { @@ -8,6 +11,14 @@ namespace Roadie.Library.Data public string CacheRegion => CacheRegionUrn(RoadieId); + /// + /// Returns a full file path to the Genre Image + /// + public string PathToImage(IRoadieSettings configuration) + { + return Path.Combine(configuration.GenreImageFolder, $"{ Name.ToFileNameFriendly() } [{ Id }].jpg"); + } + public static string CacheRegionUrn(Guid Id) { return string.Format("urn:genre:{0}", Id); diff --git a/Roadie.Api.Library/Identity/ApplicationUser.cs b/Roadie.Api.Library/Identity/ApplicationUser.cs index 426a14c..96646bb 100644 --- a/Roadie.Api.Library/Identity/ApplicationUser.cs +++ b/Roadie.Api.Library/Identity/ApplicationUser.cs @@ -118,6 +118,9 @@ namespace Roadie.Library.Identity [StringLength(50)] public string Timezone { get; set; } + [Column("defaultRowsPerPage")] + public short? DefaultRowsPerPage { get; set; } + public ICollection TrackRatings { get; set; } public ICollection UserQues { get; set; } diff --git a/Roadie.Api.Library/Imaging/DefaultNotFoundImages.cs b/Roadie.Api.Library/Imaging/DefaultNotFoundImages.cs index d794492..6f0a4ae 100644 --- a/Roadie.Api.Library/Imaging/DefaultNotFoundImages.cs +++ b/Roadie.Api.Library/Imaging/DefaultNotFoundImages.cs @@ -11,6 +11,7 @@ namespace Roadie.Library.Imaging private Image _artist; private Image _collection; private Image _label; + private Image _genre; private Image _playlist; private Image _release; private Image _track; @@ -23,6 +24,8 @@ namespace Roadie.Library.Imaging public Image Label => _label ?? (_label = MakeImageFromFile(MakeImagePath(@"images/label.jpg"))); + public Image Genre => _genre ?? (_genre = MakeImageFromFile(MakeImagePath(@"images/genre.jpg"))); + public Image Playlist => _playlist ?? (_playlist = MakeImageFromFile(MakeImagePath(@"images/playlist.jpg"))); public Image Release => _release ?? (_release = MakeImageFromFile(MakeImagePath(@"images/release.jpg"))); diff --git a/Roadie.Api.Library/Imaging/IDefaultNotFoundImages.cs b/Roadie.Api.Library/Imaging/IDefaultNotFoundImages.cs index 2d3a3bd..d1f60eb 100644 --- a/Roadie.Api.Library/Imaging/IDefaultNotFoundImages.cs +++ b/Roadie.Api.Library/Imaging/IDefaultNotFoundImages.cs @@ -7,6 +7,7 @@ namespace Roadie.Library.Imaging Image Artist { get; } Image Collection { get; } Image Label { get; } + Image Genre { get; } Image Playlist { get; } Image Release { get; } Image Track { get; } diff --git a/Roadie.Api.Library/Models/EntityModelBase.cs b/Roadie.Api.Library/Models/EntityModelBase.cs index 390cc86..5ab2990 100644 --- a/Roadie.Api.Library/Models/EntityModelBase.cs +++ b/Roadie.Api.Library/Models/EntityModelBase.cs @@ -93,7 +93,7 @@ namespace Roadie.Library.Models set => _urlsList = value; } - public bool UserBookmarked { get; set; } + public bool? UserBookmarked { get; set; } public EntityModelBase() { diff --git a/Roadie.Api.Library/Models/Genre.cs b/Roadie.Api.Library/Models/Genre.cs index ed5b806..60b41ba 100644 --- a/Roadie.Api.Library/Models/Genre.cs +++ b/Roadie.Api.Library/Models/Genre.cs @@ -1,4 +1,5 @@ -using System; +using Roadie.Library.Models.Statistics; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -7,7 +8,26 @@ namespace Roadie.Library.Models [Serializable] public class Genre : EntityModelBase { + public const string DefaultIncludes = "stats"; + + public Image Thumbnail { get; set; } + + [MaxLength(4000)] public string Description { get; set; } + public IEnumerable Comments { get; set; } [MaxLength(100)] public string Name { get; set; } + [MaxLength(100)] public string NormalizedName { get; set; } + public ReleaseGroupingStatistics Statistics { get; set; } + + public static string CacheRegionUrn(Guid Id) + { + return string.Format("urn:genre:{0}", Id); + } + + public static string CacheUrn(Guid Id) + { + return $"urn:genre_by_id:{Id}"; + } + public Image MediumThumbnail { get; set; } } } \ No newline at end of file diff --git a/Roadie.Api.Library/Models/GenreList.cs b/Roadie.Api.Library/Models/GenreList.cs index 56d2c7d..9588c1d 100644 --- a/Roadie.Api.Library/Models/GenreList.cs +++ b/Roadie.Api.Library/Models/GenreList.cs @@ -8,5 +8,6 @@ namespace Roadie.Library.Models public int? ArtistCount { get; set; } public DataToken Genre { get; set; } public int? ReleaseCount { get; set; } + public Image Thumbnail { get; set; } } } \ No newline at end of file diff --git a/Roadie.Api.Library/Models/Pagination/PagedRequest.cs b/Roadie.Api.Library/Models/Pagination/PagedRequest.cs index c04be27..fc67314 100644 --- a/Roadie.Api.Library/Models/Pagination/PagedRequest.cs +++ b/Roadie.Api.Library/Models/Pagination/PagedRequest.cs @@ -34,6 +34,7 @@ namespace Roadie.Library.Models.Pagination public Statuses FilterToStatusValue => SafeParser.ToEnum(FilterToStatus); public Guid? FilterToTrackId { get; set; } public Guid?[] FilterToTrackIds { get; set; } + public Guid? FilterToGenreId { get; set; } public int? FilterToYear { get; set; } public string FilterValue => Filter ?? string.Empty; public bool IsHistoryRequest { get; set; } diff --git a/Roadie.Api.Library/Models/Statistics/UserStatistics.cs b/Roadie.Api.Library/Models/Statistics/UserStatistics.cs index afd72c8..367f3e6 100644 --- a/Roadie.Api.Library/Models/Statistics/UserStatistics.cs +++ b/Roadie.Api.Library/Models/Statistics/UserStatistics.cs @@ -15,6 +15,8 @@ namespace Roadie.Library.Models.Statistics public ArtistList MostPlayedArtist { get; set; } public ReleaseList MostPlayedRelease { get; set; } public TrackList MostPlayedTrack { get; set; } + public TrackList LastPlayedTrack { get; set; } + public int? PlayedTracks { get; set; } public int? RatedArtists { get; set; } public int? RatedReleases { get; set; } diff --git a/Roadie.Api.Library/Models/Users/User.cs b/Roadie.Api.Library/Models/Users/User.cs index 6703e0d..fd1f655 100644 --- a/Roadie.Api.Library/Models/Users/User.cs +++ b/Roadie.Api.Library/Models/Users/User.cs @@ -7,7 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Roadie.Library.Models.Users { [Serializable] - public class User + public class User : EntityModelBase { public const string ActionKeyUserRated = "__userrated__"; public const string DefaultIncludes = "stats"; @@ -69,6 +69,10 @@ namespace Roadie.Library.Models.Users [Required] [MaxLength(20)] public string UserName { get; set; } public Image MediumThumbnail { get; set; } + public DateTime LastLogin { get; set; } + public DateTime LastApiAccess { get; set; } + public short? DefaultRowsPerPage { get; set; } + public override string ToString() { return $"Id [{Id}], RoadieId [{UserId}], UserName [{UserName}]"; diff --git a/Roadie.Api.Library/Models/Users/UserList.cs b/Roadie.Api.Library/Models/Users/UserList.cs index 454a46d..1e3d7c9 100644 --- a/Roadie.Api.Library/Models/Users/UserList.cs +++ b/Roadie.Api.Library/Models/Users/UserList.cs @@ -9,6 +9,7 @@ namespace Roadie.Library.Models.Users public class UserList : EntityInfoModelBase { public bool IsEditor { get; set; } + public bool IsAdmin { get; set; } public bool? IsPrivate { get; set; } public DateTime? LastActivity { get; set; } public DateTime? LastApiAccessDate { get; set; } @@ -31,13 +32,15 @@ namespace Roadie.Library.Models.Users Value = user.RoadieId.ToString() }, IsEditor = user.UserRoles.Any(x => x.Role.Name == "Editor"), + IsAdmin = user.UserRoles.Any(x => x.Role.Name == "Admin"), IsPrivate = user.IsPrivate, Thumbnail = thumbnail, CreatedDate = user.CreatedDate, LastUpdated = user.LastUpdated, RegisteredDate = user.RegisteredOn, LastLoginDate = user.LastLogin, - LastApiAccessDate = user.LastApiAccess + LastApiAccessDate = user.LastApiAccess, + Statistics = new UserStatistics() }; } } diff --git a/Roadie.Api.Services/AdminService.cs b/Roadie.Api.Services/AdminService.cs index 1628532..d4f8344 100644 --- a/Roadie.Api.Services/AdminService.cs +++ b/Roadie.Api.Services/AdminService.cs @@ -42,11 +42,12 @@ namespace Roadie.Api.Services private IReleaseService ReleaseService { get; } private ILabelService LabelService { get; } + private IGenreService GenreService { get; } public AdminService(IRoadieSettings configuration, IHttpEncoder httpEncoder, IHttpContext httpContext, data.IRoadieDbContext context, ICacheManager cacheManager, ILogger logger, IHubContext scanActivityHub, IFileDirectoryProcessorService fileDirectoryProcessorService, IArtistService artistService, - IReleaseService releaseService, IReleaseLookupEngine releaseLookupEngine, ILabelService labelService + IReleaseService releaseService, IReleaseLookupEngine releaseLookupEngine, ILabelService labelService, IGenreService genreService ) : base(configuration, httpEncoder, context, cacheManager, logger, httpContext) { @@ -57,6 +58,7 @@ namespace Roadie.Api.Services ArtistService = artistService; ReleaseService = releaseService; LabelService = labelService; + GenreService = genreService; ReleaseLookupEngine = releaseLookupEngine; FileDirectoryProcessorService = fileDirectoryProcessorService; } @@ -158,6 +160,11 @@ namespace Roadie.Api.Services Directory.CreateDirectory(Configuration.UserImageFolder); Logger.LogInformation($"Created User Image Folder [{Configuration.UserImageFolder }]"); } + if (!Directory.Exists(Configuration.GenreImageFolder)) + { + Directory.CreateDirectory(Configuration.GenreImageFolder); + Logger.LogInformation($"Created Genre Image Folder [{Configuration.GenreImageFolder }]"); + } if (!Directory.Exists(Configuration.PlaylistImageFolder)) { Directory.CreateDirectory(Configuration.PlaylistImageFolder); @@ -264,7 +271,7 @@ namespace Roadie.Api.Services catch (Exception ex) { Logger.LogError(ex); - await LogAndPublish("Error deleting artist secondary image."); + await LogAndPublish("Error deleting Label."); errors.Add(ex); } @@ -279,6 +286,42 @@ namespace Roadie.Api.Services }; } + public async Task> DeleteGenre(ApplicationUser user, Guid genreId) + { + var sw = new Stopwatch(); + sw.Start(); + var errors = new List(); + var genre = DbContext.Genres.FirstOrDefault(x => x.RoadieId == genreId); + if (genre == null) + { + await LogAndPublish($"DeleteLabel Unknown Genre [{genreId}]", LogLevel.Warning); + return new OperationResult(true, $"Genre Not Found [{genreId}]"); + } + + try + { + await GenreService.Delete(user, genreId); + CacheManager.ClearRegion(genre.CacheRegion); + } + catch (Exception ex) + { + Logger.LogError(ex); + await LogAndPublish("Error deleting Genre."); + errors.Add(ex); + } + + sw.Stop(); + await LogAndPublish($"DeleteGenre `{genre}`, By User `{user}`", LogLevel.Information); + return new OperationResult + { + IsSuccess = !errors.Any(), + Data = true, + OperationTime = sw.ElapsedMilliseconds, + Errors = errors + }; + } + + public async Task> DeleteArtistSecondaryImage(ApplicationUser user, Guid artistId, int index) { var sw = new Stopwatch(); diff --git a/Roadie.Api.Services/ArtistService.cs b/Roadie.Api.Services/ArtistService.cs index 4545b63..7d8884b 100644 --- a/Roadie.Api.Services/ArtistService.cs +++ b/Roadie.Api.Services/ArtistService.cs @@ -21,7 +21,6 @@ using Roadie.Library.Models.Pagination; using Roadie.Library.Models.Releases; using Roadie.Library.Models.Statistics; using Roadie.Library.Models.Users; -using Roadie.Library.Processors; using Roadie.Library.Utility; using System; using System.Collections.Generic; @@ -45,6 +44,7 @@ namespace Roadie.Api.Services private ICollectionService CollectionService { get; } + private IFileDirectoryProcessorService FileDirectoryProcessorService { get; } private IFileNameHelper FileNameHelper { get; } private IID3TagsHelper ID3TagsHelper { get; } @@ -61,9 +61,7 @@ namespace Roadie.Api.Services private IReleaseService ReleaseService { get; } - private IFileDirectoryProcessorService FileDirectoryProcessorService { get; } - - public ArtistService(IRoadieSettings configuration, + public ArtistService(IRoadieSettings configuration, IHttpEncoder httpEncoder, IHttpContext httpContext, data.IRoadieDbContext dbContext, @@ -214,65 +212,52 @@ 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 - }; - } - - public async Task> List(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(); IQueryable favoriteArtistIds = null; if (request.FilterFavoriteOnly) + { favoriteArtistIds = from a in DbContext.Artists join ua in DbContext.UserArtists on a.Id equals ua.ArtistId where ua.IsFavorite ?? false where roadieUser == null || ua.UserId == roadieUser.Id select a.Id; + } IQueryable labelArtistIds = null; if (request.FilterToLabelId.HasValue) + { labelArtistIds = (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 l.RoadieId == request.FilterToLabelId select r.ArtistId) - .Distinct(); + .Distinct(); + } IQueryable genreArtistIds = null; var isFilteredToGenre = false; - if (!string.IsNullOrEmpty(request.Filter) && - request.Filter.StartsWith(":genre", StringComparison.OrdinalIgnoreCase)) + if(request.FilterToGenreId.HasValue) + { + genreArtistIds = (from ag in DbContext.ArtistGenres + join g in DbContext.Genres on ag.GenreId equals g.Id + where g.RoadieId == request.FilterToGenreId + select ag.ArtistId) + .Distinct(); + isFilteredToGenre = true; + } + else if (!string.IsNullOrEmpty(request.Filter) && request.Filter.StartsWith(":genre", StringComparison.OrdinalIgnoreCase)) { var genreFilter = request.Filter.Replace(":genre ", ""); genreArtistIds = (from ag in DbContext.ArtistGenres join g in DbContext.Genres on ag.GenreId equals g.Id where g.Name.Contains(genreFilter) select ag.ArtistId) - .Distinct(); + .Distinct(); isFilteredToGenre = true; request.Filter = null; } - var onlyWithReleases = onlyIncludeWithReleases ?? true; var isEqualFilter = false; if (!string.IsNullOrEmpty(request.FilterValue)) @@ -285,7 +270,6 @@ namespace Roadie.Api.Services request.Filter = filter.Substring(1, filter.Length - 2); } } - var normalizedFilterValue = !string.IsNullOrEmpty(request.FilterValue) ? request.FilterValue.ToAlphanumericName() : null; @@ -293,8 +277,18 @@ namespace Roadie.Api.Services where !onlyWithReleases || a.ReleaseCount > 0 where request.FilterToArtistId == null || a.RoadieId == request.FilterToArtistId where request.FilterMinimumRating == null || a.Rating >= request.FilterMinimumRating.Value - where request.FilterValue == "" || a.Name.Contains(request.FilterValue) || a.SortName.Contains(request.FilterValue) || a.AlternateNames.Contains(request.FilterValue) || a.AlternateNames.Contains(normalizedFilterValue) - where !isEqualFilter || a.Name.Equals(request.FilterValue) || a.SortName.Equals(request.FilterValue) || a.AlternateNames.Equals(request.FilterValue) || a.AlternateNames.Equals(normalizedFilterValue) + where request.FilterValue == "" || + a.Name.Contains(request.FilterValue) || + a.SortName.Contains(request.FilterValue) || + a.RealName.Contains(request.FilterValue) || + a.AlternateNames.Contains(request.FilterValue) || + a.AlternateNames.Contains(normalizedFilterValue) + where !isEqualFilter || + a.Name.Equals(request.FilterValue) || + a.SortName.Equals(request.FilterValue) || + a.RealName.Equals(request.FilterValue) || + a.AlternateNames.Equals(request.FilterValue) || + a.AlternateNames.Equals(normalizedFilterValue) where !request.FilterFavoriteOnly || favoriteArtistIds.Contains(a.Id) where request.FilterToLabelId == null || labelArtistIds.Contains(a.Id) where !isFilteredToGenre || genreArtistIds.Contains(a.Id) @@ -323,22 +317,24 @@ namespace Roadie.Api.Services var rowCount = result.Count(); if (doRandomize ?? false) { - var randomLimit = roadieUser?.RandomReleaseLimit ?? 100; + var randomLimit = roadieUser?.RandomReleaseLimit ?? request.Limit; request.Limit = request.LimitValue > randomLimit ? randomLimit : request.LimitValue; - rows = result.OrderBy(x => x.RandomSortId).Skip(request.SkipValue).Take(request.LimitValue).ToArray(); + rows = result.OrderBy(x => x.RandomSortId) + .Take(request.LimitValue) + .ToArray(); } else { string sortBy; if (request.ActionValue == User.ActionKeyUserRated) { - sortBy = string.IsNullOrEmpty(request.Sort) + sortBy = string.IsNullOrEmpty(request.Sort) ? request.OrderValue(new Dictionary { { "Rating", "DESC" }, { "Artist.Text", "ASC" } }) : request.OrderValue(); } else { - sortBy = request.OrderValue(new Dictionary {{"SortName", "ASC"}, {"Artist.Text", "ASC"}}); + sortBy = request.OrderValue(new Dictionary { { "SortName", "ASC" }, { "Artist.Text", "ASC" } }); } rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray(); } @@ -351,11 +347,11 @@ namespace Roadie.Api.Services where rowIds.Contains(ua.ArtistId) select ua).ToArray(); - foreach (var userArtistRating in userArtistRatings.Where(x => - rows.Select(r => r.DatabaseId).Contains(x.ArtistId))) + foreach (var userArtistRating in userArtistRatings.Where(x => rows.Select(r => r.DatabaseId).Contains(x.ArtistId))) { var row = rows.FirstOrDefault(x => x.DatabaseId == userArtistRating.ArtistId); if (row != null) + { row.UserRating = new UserArtist { IsDisliked = userArtistRating.IsDisliked ?? false, @@ -363,11 +359,11 @@ namespace Roadie.Api.Services Rating = userArtistRating.Rating, RatedDate = userArtistRating.LastUpdated ?? userArtistRating.CreatedDate }; + } } } - - sw.Stop(); if (!string.IsNullOrEmpty(request.Filter) && rowCount == 0) + { if (Configuration.RecordNoResultSearches) { // Create request for no artist found @@ -379,7 +375,8 @@ namespace Roadie.Api.Services DbContext.Requests.Add(req); await DbContext.SaveChangesAsync(); } - + } + sw.Stop(); return new Library.Models.Pagination.PagedResult { TotalCount = rowCount, @@ -450,179 +447,6 @@ namespace Roadie.Api.Services }; } - - async Task> MergeArtists(ApplicationUser user, data.Artist artistToMerge, data.Artist artistToMergeInto) - { - SimpleContract.Requires(artistToMerge != null, "Invalid Artist"); - SimpleContract.Requires(artistToMergeInto != null, "Invalid Artist"); - - var result = false; - var now = DateTime.UtcNow; - - var sw = new Stopwatch(); - sw.Start(); - - 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.Thumbnail = artistToMergeInto.Thumbnail ?? artistToMerge.Thumbnail; - artistToMergeInto.Profile = artistToMergeInto.Profile ?? artistToMerge.Profile; - artistToMergeInto.BirthDate = artistToMergeInto.BirthDate ?? artistToMerge.BirthDate; - artistToMergeInto.BeginDate = artistToMergeInto.BeginDate ?? artistToMerge.BeginDate; - artistToMergeInto.EndDate = artistToMergeInto.EndDate ?? artistToMerge.EndDate; - if (!string.IsNullOrEmpty(artistToMerge.ArtistType) && !artistToMerge.ArtistType.Equals("Other", StringComparison.OrdinalIgnoreCase)) - { - artistToMergeInto.ArtistType = artistToMergeInto.ArtistType ?? artistToMerge.ArtistType; - } - artistToMergeInto.BioContext = artistToMergeInto.BioContext ?? artistToMerge.BioContext; - artistToMergeInto.DiscogsId = artistToMergeInto.DiscogsId ?? artistToMerge.DiscogsId; - artistToMergeInto.Tags = artistToMergeInto.Tags.AddToDelimitedList(artistToMerge.Tags.ToListFromDelimited()); - var altNames = artistToMerge.AlternateNames.ToListFromDelimited().ToList(); - altNames.Add(artistToMerge.Name); - altNames.Add(artistToMerge.SortName); - artistToMergeInto.AlternateNames = artistToMergeInto.AlternateNames.AddToDelimitedList(altNames); - artistToMergeInto.URLs = artistToMergeInto.URLs.AddToDelimitedList(artistToMerge.URLs.ToListFromDelimited()); - artistToMergeInto.ISNI = artistToMergeInto.ISNI.AddToDelimitedList(artistToMerge.ISNI.ToListFromDelimited()); - artistToMergeInto.LastUpdated = now; - - try - { - var artistGenres = DbContext.ArtistGenres.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); - if (artistGenres != null) - { - 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); - // If not exist then add new for artist to merge into - if (existing == null) - { - DbContext.ArtistGenres.Add(new data.ArtistGenre - { - ArtistId = artistToMergeInto.Id, - GenreId = artistGenre.GenreId - }); - } - } - } - var artistImages = DbContext.Images.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); - if (artistImages != null) - { - foreach (var artistImage in artistImages) - { - artistImage.ArtistId = artistToMergeInto.Id; - } - } - - try - { - // Move any Artist and Artist Secondary images from ArtistToMerge into ArtistToMergeInto folder - if (Directory.Exists(artistToMergeFolder)) - { - var artistToMergeImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeFolder), ImageType.Artist); - var artistToMergeSecondaryImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeFolder), ImageType.ArtistSecondary).ToList(); - // Primary Artist image - if (artistToMergeImages.Any()) - { - // If the ArtistToMergeInto already has a primary image then the ArtistToMerge primary image becomes a secondary image - var artistToMergeIntoPrimaryImage = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeIntoFolder), ImageType.Artist).FirstOrDefault(); - if (artistToMergeIntoPrimaryImage != null) - { - artistToMergeSecondaryImages.Add(artistToMergeImages.First()); - } - else - { - var artistImageFilename = Path.Combine(artistToMergeIntoFolder, ImageHelper.ArtistImageFilename); - artistToMergeImages.First().MoveTo(artistImageFilename); - } - } - // Secondary Artist images - if (artistToMergeSecondaryImages.Any()) - { - var looper = 0; - foreach (var artistSecondaryImage in artistToMergeSecondaryImages) - { - var artistImageFilename = Path.Combine(artistToMergeIntoFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); - while (File.Exists(artistImageFilename)) - { - looper++; - artistImageFilename = Path.Combine(artistToMergeIntoFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); - } - artistSecondaryImage.MoveTo(artistImageFilename); - } - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "MergeArtists: Error Moving Artist Primary and Secondary Images"); - } - - var userArtists = DbContext.UserArtists.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); - if (artistImages != null) - { - foreach (var userArtist in userArtists) - { - userArtist.ArtistId = artistToMergeInto.Id; - } - } - var artistTracks = DbContext.Tracks.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); - if (artistTracks != null) - { - foreach (var artistTrack in artistTracks) - { - artistTrack.ArtistId = artistToMergeInto.Id; - } - } - var artistReleases = DbContext.Releases.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); - if (artistReleases != null) - { - foreach (var artistRelease in artistReleases) - { - // See if there is already a release by the same name for the artist to merge into, if so then merge releases - var artistToMergeHasRelease = DbContext.Releases.FirstOrDefault(x => x.ArtistId == artistToMerge.Id && x.Title == artistRelease.Title); - if (artistToMergeHasRelease != null) - { - await ReleaseService.MergeReleases(user, artistRelease, artistToMergeHasRelease, false); - } - else - { - artistRelease.ArtistId = artistToMerge.Id; - } - } - } - } - catch (Exception ex) - { - Logger.LogWarning(ex.ToString()); - } - - 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 DbContext.SaveChangesAsync(); - - await Delete(user, artistToMerge); - - - result = true; - - sw.Stop(); - return new OperationResult - { - Data = artistToMergeInto, - IsSuccess = result, - OperationTime = sw.ElapsedMilliseconds - }; - } - public async Task> RefreshArtistMetadata(ApplicationUser user, Guid artistId) { SimpleContract.Requires(artistId != Guid.Empty, "Invalid ArtistId"); @@ -687,6 +511,97 @@ namespace Roadie.Api.Services }; } + public async Task> ScanArtistReleasesFolders(ApplicationUser user, Guid artistId, string destinationFolder, bool doJustInfo) + { + SimpleContract.Requires(artistId != Guid.Empty, "Invalid ArtistId"); + + var result = true; + var resultErrors = new List(); + var sw = new Stopwatch(); + sw.Start(); + try + { + var artist = DbContext.Artists + .Include("Releases") + .Include("Releases.Labels") + .FirstOrDefault(x => x.RoadieId == artistId); + if (artist == null) + { + Logger.LogWarning("Unable To Find Artist [{0}]", artistId); + return new OperationResult(); + } + var releaseScannedCount = 0; + var artistFolder = artist.ArtistFileFolder(Configuration); + if (!Directory.Exists(artistFolder)) + { + Logger.LogDebug($"ScanArtistReleasesFolders: ArtistFolder Not Found [{ artistFolder }] For Artist `{ artist }`"); + return new OperationResult(); + } + var scannedArtistFolders = new List(); + // Scan known releases for changes + if (artist.Releases != null) + { + foreach (var release in artist.Releases) + try + { + result = result && (await ReleaseService.ScanReleaseFolder(user, Guid.Empty, doJustInfo, release)).Data; + releaseScannedCount++; + scannedArtistFolders.Add(release.ReleaseFileFolder(artistFolder)); + } + catch (Exception ex) + { + Logger.LogError(ex, ex.Serialize()); + } + } + // Any folder found in Artist folder not already scanned scan + var nonReleaseFolders = from d in Directory.EnumerateDirectories(artistFolder) + where !(from r in scannedArtistFolders select r).Contains(d) + orderby d + select d; + foreach (var folder in nonReleaseFolders) + { + await FileDirectoryProcessorService.Process(user, new DirectoryInfo(folder), doJustInfo); + } + if (!doJustInfo) + { + Services.FileDirectoryProcessorService.DeleteEmptyFolders(new DirectoryInfo(artistFolder), Logger); + } + // Always update artist image if artist image is found on an artist rescan + var imageFiles = ImageHelper.ImageFilesInFolder(artistFolder, SearchOption.AllDirectories); + if (imageFiles != null && imageFiles.Any()) + { + var i = new FileInfo(imageFiles.First()); + var iName = i.Name.ToLower().Trim(); + if (ImageHelper.IsArtistImage(i)) + { + // Read image and convert to jpeg + artist.Thumbnail = ImageHelper.ResizeToThumbnail(File.ReadAllBytes(i.FullName), Configuration); + artist.LastUpdated = DateTime.UtcNow; + await DbContext.SaveChangesAsync(); + CacheManager.ClearRegion(artist.CacheRegion); + Logger.LogInformation("Update Thumbnail using Artist File [{0}]", iName); + } + } + + sw.Stop(); + CacheManager.ClearRegion(artist.CacheRegion); + Logger.LogInformation("Scanned Artist [{0}], Releases Scanned [{1}], OperationTime [{2}]", artist.ToString(), releaseScannedCount, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + Logger.LogError(ex, ex.Serialize()); + resultErrors.Add(ex); + } + + return new OperationResult + { + Data = result, + IsSuccess = result, + Errors = resultErrors, + OperationTime = sw.ElapsedMilliseconds + }; + } + public async Task> SetReleaseImageByUrl(ApplicationUser user, Guid id, string imageUrl) { return await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)); @@ -773,11 +688,11 @@ namespace Roadie.Api.Services // Ensure is jpeg first artistSecondaryImage = ImageHelper.ConvertToJpegFormat(artistSecondaryImage); - var artistImageFilename = Path.Combine(newArtistFolder,string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); + var artistImageFilename = Path.Combine(newArtistFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); while (File.Exists(artistImageFilename)) { looper++; - artistImageFilename = Path.Combine(newArtistFolder,string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); + artistImageFilename = Path.Combine(newArtistFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); } File.WriteAllBytes(artistImageFilename, artistSecondaryImage); @@ -1317,6 +1232,200 @@ 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(ApplicationUser user, data.Artist artistToMerge, data.Artist artistToMergeInto) + { + SimpleContract.Requires(artistToMerge != null, "Invalid Artist"); + SimpleContract.Requires(artistToMergeInto != null, "Invalid Artist"); + + var result = false; + var now = DateTime.UtcNow; + + var sw = new Stopwatch(); + sw.Start(); + + 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.Thumbnail = artistToMergeInto.Thumbnail ?? artistToMerge.Thumbnail; + artistToMergeInto.Profile = artistToMergeInto.Profile ?? artistToMerge.Profile; + artistToMergeInto.BirthDate = artistToMergeInto.BirthDate ?? artistToMerge.BirthDate; + artistToMergeInto.BeginDate = artistToMergeInto.BeginDate ?? artistToMerge.BeginDate; + artistToMergeInto.EndDate = artistToMergeInto.EndDate ?? artistToMerge.EndDate; + if (!string.IsNullOrEmpty(artistToMerge.ArtistType) && !artistToMerge.ArtistType.Equals("Other", StringComparison.OrdinalIgnoreCase)) + { + artistToMergeInto.ArtistType = artistToMergeInto.ArtistType ?? artistToMerge.ArtistType; + } + artistToMergeInto.BioContext = artistToMergeInto.BioContext ?? artistToMerge.BioContext; + artistToMergeInto.DiscogsId = artistToMergeInto.DiscogsId ?? artistToMerge.DiscogsId; + artistToMergeInto.Tags = artistToMergeInto.Tags.AddToDelimitedList(artistToMerge.Tags.ToListFromDelimited()); + var altNames = artistToMerge.AlternateNames.ToListFromDelimited().ToList(); + altNames.Add(artistToMerge.Name); + altNames.Add(artistToMerge.SortName); + artistToMergeInto.AlternateNames = artistToMergeInto.AlternateNames.AddToDelimitedList(altNames); + artistToMergeInto.URLs = artistToMergeInto.URLs.AddToDelimitedList(artistToMerge.URLs.ToListFromDelimited()); + artistToMergeInto.ISNI = artistToMergeInto.ISNI.AddToDelimitedList(artistToMerge.ISNI.ToListFromDelimited()); + artistToMergeInto.LastUpdated = now; + + try + { + var artistGenres = DbContext.ArtistGenres.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); + if (artistGenres != null) + { + 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); + // If not exist then add new for artist to merge into + if (existing == null) + { + DbContext.ArtistGenres.Add(new data.ArtistGenre + { + ArtistId = artistToMergeInto.Id, + GenreId = artistGenre.GenreId + }); + } + } + } + var artistImages = DbContext.Images.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); + if (artistImages != null) + { + foreach (var artistImage in artistImages) + { + artistImage.ArtistId = artistToMergeInto.Id; + } + } + + try + { + // Move any Artist and Artist Secondary images from ArtistToMerge into ArtistToMergeInto folder + if (Directory.Exists(artistToMergeFolder)) + { + var artistToMergeImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeFolder), ImageType.Artist); + var artistToMergeSecondaryImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeFolder), ImageType.ArtistSecondary).ToList(); + // Primary Artist image + if (artistToMergeImages.Any()) + { + // If the ArtistToMergeInto already has a primary image then the ArtistToMerge primary image becomes a secondary image + var artistToMergeIntoPrimaryImage = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistToMergeIntoFolder), ImageType.Artist).FirstOrDefault(); + if (artistToMergeIntoPrimaryImage != null) + { + artistToMergeSecondaryImages.Add(artistToMergeImages.First()); + } + else + { + var artistImageFilename = Path.Combine(artistToMergeIntoFolder, ImageHelper.ArtistImageFilename); + artistToMergeImages.First().MoveTo(artistImageFilename); + } + } + // Secondary Artist images + if (artistToMergeSecondaryImages.Any()) + { + var looper = 0; + foreach (var artistSecondaryImage in artistToMergeSecondaryImages) + { + var artistImageFilename = Path.Combine(artistToMergeIntoFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); + while (File.Exists(artistImageFilename)) + { + looper++; + artistImageFilename = Path.Combine(artistToMergeIntoFolder, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00"))); + } + artistSecondaryImage.MoveTo(artistImageFilename); + } + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "MergeArtists: Error Moving Artist Primary and Secondary Images"); + } + + var userArtists = DbContext.UserArtists.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); + if (artistImages != null) + { + foreach (var userArtist in userArtists) + { + userArtist.ArtistId = artistToMergeInto.Id; + } + } + var artistTracks = DbContext.Tracks.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); + if (artistTracks != null) + { + foreach (var artistTrack in artistTracks) + { + artistTrack.ArtistId = artistToMergeInto.Id; + } + } + var artistReleases = DbContext.Releases.Where(x => x.ArtistId == artistToMerge.Id).ToArray(); + if (artistReleases != null) + { + foreach (var artistRelease in artistReleases) + { + // See if there is already a release by the same name for the artist to merge into, if so then merge releases + var artistToMergeHasRelease = DbContext.Releases.FirstOrDefault(x => x.ArtistId == artistToMerge.Id && x.Title == artistRelease.Title); + if (artistToMergeHasRelease != null) + { + await ReleaseService.MergeReleases(user, artistRelease, artistToMergeHasRelease, false); + } + else + { + artistRelease.ArtistId = artistToMerge.Id; + } + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex.ToString()); + } + + 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 DbContext.SaveChangesAsync(); + + await Delete(user, artistToMerge); + + result = true; + + sw.Stop(); + return new OperationResult + { + Data = artistToMergeInto, + IsSuccess = result, + OperationTime = sw.ElapsedMilliseconds + }; + } + private async Task> SaveImageBytes(ApplicationUser user, Guid id, byte[] imageBytes) { var sw = new Stopwatch(); @@ -1368,96 +1477,5 @@ namespace Roadie.Api.Services Errors = errors }; } - - public async Task> ScanArtistReleasesFolders(ApplicationUser user, Guid artistId, string destinationFolder, bool doJustInfo) - { - SimpleContract.Requires(artistId != Guid.Empty, "Invalid ArtistId"); - - var result = true; - var resultErrors = new List(); - var sw = new Stopwatch(); - sw.Start(); - try - { - var artist = DbContext.Artists - .Include("Releases") - .Include("Releases.Labels") - .FirstOrDefault(x => x.RoadieId == artistId); - if (artist == null) - { - Logger.LogWarning("Unable To Find Artist [{0}]", artistId); - return new OperationResult(); - } - var releaseScannedCount = 0; - var artistFolder = artist.ArtistFileFolder(Configuration); - if (!Directory.Exists(artistFolder)) - { - Logger.LogDebug($"ScanArtistReleasesFolders: ArtistFolder Not Found [{ artistFolder }] For Artist `{ artist }`"); - return new OperationResult(); - } - var scannedArtistFolders = new List(); - // Scan known releases for changes - if (artist.Releases != null) - { - foreach (var release in artist.Releases) - try - { - result = result && (await ReleaseService.ScanReleaseFolder(user, Guid.Empty, doJustInfo, release)).Data; - releaseScannedCount++; - scannedArtistFolders.Add(release.ReleaseFileFolder(artistFolder)); - } - catch (Exception ex) - { - Logger.LogError(ex, ex.Serialize()); - } - } - // Any folder found in Artist folder not already scanned scan - var nonReleaseFolders = from d in Directory.EnumerateDirectories(artistFolder) - where !(from r in scannedArtistFolders select r).Contains(d) - orderby d - select d; - foreach (var folder in nonReleaseFolders) - { - await FileDirectoryProcessorService.Process(user, new DirectoryInfo(folder), doJustInfo); - } - if (!doJustInfo) - { - Services.FileDirectoryProcessorService.DeleteEmptyFolders(new DirectoryInfo(artistFolder), Logger); - } - // Always update artist image if artist image is found on an artist rescan - var imageFiles = ImageHelper.ImageFilesInFolder(artistFolder, SearchOption.AllDirectories); - if (imageFiles != null && imageFiles.Any()) - { - var i = new FileInfo(imageFiles.First()); - var iName = i.Name.ToLower().Trim(); - if (ImageHelper.IsArtistImage(i)) - { - // Read image and convert to jpeg - artist.Thumbnail = ImageHelper.ResizeToThumbnail(File.ReadAllBytes(i.FullName), Configuration); - artist.LastUpdated = DateTime.UtcNow; - await DbContext.SaveChangesAsync(); - CacheManager.ClearRegion(artist.CacheRegion); - Logger.LogInformation("Update Thumbnail using Artist File [{0}]", iName); - } - } - - sw.Stop(); - CacheManager.ClearRegion(artist.CacheRegion); - Logger.LogInformation("Scanned Artist [{0}], Releases Scanned [{1}], OperationTime [{2}]", artist.ToString(), releaseScannedCount, sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - Logger.LogError(ex, ex.Serialize()); - resultErrors.Add(ex); - } - - return new OperationResult - { - Data = result, - IsSuccess = result, - Errors = resultErrors, - OperationTime = sw.ElapsedMilliseconds - }; - } } } \ No newline at end of file diff --git a/Roadie.Api.Services/GenreService.cs b/Roadie.Api.Services/GenreService.cs index 785bc97..6d08f5c 100644 --- a/Roadie.Api.Services/GenreService.cs +++ b/Roadie.Api.Services/GenreService.cs @@ -1,7 +1,12 @@ -using Microsoft.Extensions.Logging; +using Mapster; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Roadie.Library; using Roadie.Library.Caching; using Roadie.Library.Configuration; using Roadie.Library.Encoding; +using Roadie.Library.Identity; +using Roadie.Library.Imaging; using Roadie.Library.Models; using Roadie.Library.Models.Pagination; using Roadie.Library.Models.Users; @@ -9,6 +14,7 @@ using Roadie.Library.Utility; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Linq.Dynamic.Core; using System.Threading.Tasks; @@ -28,8 +34,155 @@ namespace Roadie.Api.Services { } - public Task> List(User roadieUser, PagedRequest request, - bool? doRandomize = false) + public async Task> ById(User roadieUser, Guid id, IEnumerable includes = null) + { + var sw = Stopwatch.StartNew(); + sw.Start(); + var cacheKey = string.Format("urn:genre_by_id_operation:{0}:{1}", id, includes == null ? "0" : string.Join("|", includes)); + var result = await CacheManager.GetAsync(cacheKey, async () => + { + return await GenreByIdAction(id, includes); + }, data.Genre.CacheRegionUrn(id)); + sw.Stop(); + return new OperationResult(result.Messages) + { + Data = result?.Data, + IsNotFoundResult = result?.IsNotFoundResult ?? false, + Errors = result?.Errors, + IsSuccess = result?.IsSuccess ?? false, + OperationTime = sw.ElapsedMilliseconds + }; + } + + public async Task> Delete(ApplicationUser user, Guid id) + { + var sw = new Stopwatch(); + sw.Start(); + var genre = DbContext.Genres.FirstOrDefault(x => x.RoadieId == id); + if (genre == null) return new OperationResult(true, string.Format("Genre Not Found [{0}]", id)); + DbContext.Genres.Remove(genre); + await DbContext.SaveChangesAsync(); + + var genreImageFilename = genre.PathToImage(Configuration); + if (File.Exists(genreImageFilename)) + { + File.Delete(genreImageFilename); + } + + Logger.LogInformation("User `{0}` deleted Genre `{1}]`", user, genre); + CacheManager.ClearRegion(genre.CacheRegion); + sw.Stop(); + return new OperationResult + { + IsSuccess = true, + Data = true, + OperationTime = sw.ElapsedMilliseconds + }; + } + + + + private Task> GenreByIdAction(Guid id, IEnumerable includes = null) + { + var sw = Stopwatch.StartNew(); + sw.Start(); + + var genre = DbContext.Genres.FirstOrDefault(x => x.RoadieId == id); + if (genre == null) + { + return Task.FromResult(new OperationResult(true, string.Format("Genre Not Found [{0}]", id))); + } + var result = genre.Adapt(); + result.AlternateNames = genre.AlternateNames; + result.Tags = genre.Tags; + result.Thumbnail = MakeLabelThumbnailImage(genre.RoadieId); + result.MediumThumbnail = MakeThumbnailImage(id, "genre", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height); + if (includes != null && includes.Any()) + { + if (includes.Contains("stats")) + { + var releaseCount = (from rg in DbContext.ReleaseGenres + where rg.GenreId == genre.Id + select rg.Id).Count(); + var artistCount = (from rg in DbContext.ArtistGenres + where rg.GenreId == genre.Id + select rg.Id).Count(); + result.Statistics = new Library.Models.Statistics.ReleaseGroupingStatistics + { + ArtistCount = artistCount, + ReleaseCount = releaseCount + }; + } + } + sw.Stop(); + return Task.FromResult(new OperationResult + { + Data = result, + IsSuccess = result != null, + OperationTime = sw.ElapsedMilliseconds + }); + } + + public async Task> UploadGenreImage(User user, Guid id, IFormFile file) + { + var bytes = new byte[0]; + using (var ms = new MemoryStream()) + { + file.CopyTo(ms); + bytes = ms.ToArray(); + } + + return await SaveImageBytes(user, id, bytes); + } + + public async Task> SetGenreImageByUrl(User user, Guid id, string imageUrl) + { + return await SaveImageBytes(user, id, WebHelper.BytesForImageUrl(imageUrl)); + } + + + private async Task> SaveImageBytes(User user, Guid id, byte[] imageBytes) + { + var sw = new Stopwatch(); + sw.Start(); + var errors = new List(); + var genre = DbContext.Genres.FirstOrDefault(x => x.RoadieId == id); + if (genre == null) return new OperationResult(true, string.Format("Genre Not Found [{0}]", id)); + try + { + var now = DateTime.UtcNow; + genre.Thumbnail = imageBytes; + if (genre.Thumbnail != null) + { + // Save unaltered label image + File.WriteAllBytes(genre.PathToImage(Configuration), ImageHelper.ConvertToJpegFormat(imageBytes)); + genre.Thumbnail = ImageHelper.ResizeToThumbnail(genre.Thumbnail, Configuration); + } + + genre.LastUpdated = now; + await DbContext.SaveChangesAsync(); + CacheManager.ClearRegion(genre.CacheRegion); + Logger.LogInformation($"UploadGenreImage `{genre}` By User `{user}`"); + } + catch (Exception ex) + { + Logger.LogError(ex); + errors.Add(ex); + } + + sw.Stop(); + + return new OperationResult + { + IsSuccess = !errors.Any(), + Data = MakeThumbnailImage(id, "genre", Configuration.MediumImageSize.Width, Configuration.MediumImageSize.Height, true), + OperationTime = sw.ElapsedMilliseconds, + Errors = errors + }; + } + + + public Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false) { var sw = new Stopwatch(); sw.Start(); @@ -60,7 +213,8 @@ namespace Roadie.Api.Services ReleaseCount = releaseCount, ArtistCount = artistCount, CreatedDate = g.CreatedDate, - LastUpdated = g.LastUpdated + LastUpdated = g.LastUpdated, + Thumbnail = MakeGenreThumbnailImage(g.RoadieId) }; GenreList[] rows; diff --git a/Roadie.Api.Services/IAdminService.cs b/Roadie.Api.Services/IAdminService.cs index d5c279b..5078ee0 100644 --- a/Roadie.Api.Services/IAdminService.cs +++ b/Roadie.Api.Services/IAdminService.cs @@ -19,6 +19,8 @@ namespace Roadie.Api.Services Task> DeleteLabel(ApplicationUser user, Guid labelId); + Task> DeleteGenre(ApplicationUser user, Guid genreId); + Task> DeleteRelease(ApplicationUser user, Guid releaseId, bool? doDeleteFiles); Task> DeleteReleaseSecondaryImage(ApplicationUser user, Guid releaseId, int index); diff --git a/Roadie.Api.Services/IGenreService.cs b/Roadie.Api.Services/IGenreService.cs index 896e125..07987af 100644 --- a/Roadie.Api.Services/IGenreService.cs +++ b/Roadie.Api.Services/IGenreService.cs @@ -1,12 +1,22 @@ -using Roadie.Library.Models; +using Microsoft.AspNetCore.Http; +using Roadie.Library; +using Roadie.Library.Identity; +using Roadie.Library.Models; using Roadie.Library.Models.Pagination; using Roadie.Library.Models.Users; +using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Roadie.Api.Services { public interface IGenreService { + Task> ById(User roadieUser, Guid id, IEnumerable includes = null); + Task> Delete(ApplicationUser user, Guid id); Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false); + Task> SetGenreImageByUrl(User user, Guid id, string imageUrl); + Task> UploadGenreImage(User user, Guid id, IFormFile file); + } } \ No newline at end of file diff --git a/Roadie.Api.Services/IImageService.cs b/Roadie.Api.Services/IImageService.cs index 9ad4d5d..448fced 100644 --- a/Roadie.Api.Services/IImageService.cs +++ b/Roadie.Api.Services/IImageService.cs @@ -28,6 +28,8 @@ namespace Roadie.Api.Services Task> LabelImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null); + Task> GenreImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null); + Task> PlaylistImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null); Task> ReleaseImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null); diff --git a/Roadie.Api.Services/IUserService.cs b/Roadie.Api.Services/IUserService.cs index ec34609..c20ad91 100644 --- a/Roadie.Api.Services/IUserService.cs +++ b/Roadie.Api.Services/IUserService.cs @@ -9,7 +9,7 @@ namespace Roadie.Api.Services { public interface IUserService { - Task> ById(User user, Guid id, IEnumerable includes); + Task> ById(User user, Guid id, IEnumerable includes, bool isAccountSettingsEdit = false); Task> List(PagedRequest request); diff --git a/Roadie.Api.Services/ImageService.cs b/Roadie.Api.Services/ImageService.cs index d1e38cf..3d58a3a 100644 --- a/Roadie.Api.Services/ImageService.cs +++ b/Roadie.Api.Services/ImageService.cs @@ -128,6 +128,17 @@ namespace Roadie.Api.Services etag); } + public async Task> GenreImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) + { + return await GetImageFileOperation("GenreThumbnail", + data.Label.CacheRegionUrn(id), + id, + width, + height, + async () => { return await GenreImageAction(id, etag); }, + etag); + } + public async Task> PlaylistImage(Guid id, int? width, int? height, EntityTagHeaderValue etag = null) { return await GetImageFileOperation("PlaylistThumbnail", @@ -480,6 +491,47 @@ namespace Roadie.Api.Services return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); } + private Task> GenreImageAction(Guid id, EntityTagHeaderValue etag = null) + { + try + { + var genre = GetGenre(id); + if (genre == null) + { + return Task.FromResult(new FileOperationResult(true, string.Format("Genre Not Found [{0}]", id))); + } + var image = new data.Image + { + Bytes = genre.Thumbnail, + CreatedDate = genre.CreatedDate, + LastUpdated = genre.LastUpdated + }; + var genreImageFilename = genre.PathToImage(Configuration); + try + { + if (File.Exists(genreImageFilename)) + { + image.Bytes = File.ReadAllBytes(genreImageFilename); + } + } + catch (Exception ex) + { + Logger.LogError(ex, $"Error Reading Image File [{genreImageFilename}]"); + } + if (genre.Thumbnail == null || !genre.Thumbnail.Any()) + { + image = DefaultNotFoundImages.Genre; + } + return Task.FromResult(GenerateFileOperationResult(id, image, etag)); + } + catch (Exception ex) + { + Logger.LogError($"Error fetching Label Thumbnail [{id}]", ex); + } + + return Task.FromResult(new FileOperationResult(OperationMessages.ErrorOccured)); + } + private Task> PlaylistImageAction(Guid id, EntityTagHeaderValue etag = null) { try diff --git a/Roadie.Api.Services/LabelService.cs b/Roadie.Api.Services/LabelService.cs index 105969e..0aff883 100644 --- a/Roadie.Api.Services/LabelService.cs +++ b/Roadie.Api.Services/LabelService.cs @@ -77,7 +77,7 @@ namespace Roadie.Api.Services var cacheKey = string.Format("urn:label_by_id_operation:{0}:{1}", id, includes == null ? "0" : string.Join("|", includes)); var result = await CacheManager.GetAsync(cacheKey, - async () => { return await LabelByIdAction(id, includes); }, data.Artist.CacheRegionUrn(id)); + async () => { return await LabelByIdAction(id, includes); }, data.Label.CacheRegionUrn(id)); sw.Stop(); if (result?.Data != null && roadieUser != null) { @@ -157,9 +157,11 @@ namespace Roadie.Api.Services var rowCount = result.Count(); if (doRandomize ?? false) { - var randomLimit = roadieUser?.RandomReleaseLimit ?? 100; + var randomLimit = roadieUser?.RandomReleaseLimit ?? request.Limit; request.Limit = request.LimitValue > randomLimit ? randomLimit : request.LimitValue; - rows = result.OrderBy(x => x.RandomSortId).Take(request.LimitValue).ToArray(); + rows = result.OrderBy(x => x.RandomSortId) + .Take(request.LimitValue) + .ToArray(); } else { @@ -341,8 +343,7 @@ namespace Roadie.Api.Services result.Tags = label.Tags; result.URLs = label.URLs; result.Thumbnail = MakeLabelThumbnailImage(label.RoadieId); - result.MediumThumbnail = MakeThumbnailImage(id, "label", Configuration.MediumImageSize.Width, - Configuration.MediumImageSize.Height); + result.MediumThumbnail = MakeThumbnailImage(id, "label", Configuration.MediumImageSize.Width,Configuration.MediumImageSize.Height); if (includes != null && includes.Any()) { if (includes.Contains("stats")) diff --git a/Roadie.Api.Services/PlaylistService.cs b/Roadie.Api.Services/PlaylistService.cs index 655618f..747c1b0 100644 --- a/Roadie.Api.Services/PlaylistService.cs +++ b/Roadie.Api.Services/PlaylistService.cs @@ -113,8 +113,10 @@ namespace Roadie.Api.Services sw.Start(); var cacheKey = string.Format("urn:playlist_by_id_operation:{0}:{1}", id, includes == null ? "0" : string.Join("|", includes)); - var result = await CacheManager.GetAsync(cacheKey, - async () => { return await PlaylistByIdAction(id, includes); }, data.Artist.CacheRegionUrn(id)); + var result = await CacheManager.GetAsync(cacheKey, async () => + { + return await PlaylistByIdAction(id, includes); + }, data.Artist.CacheRegionUrn(id)); sw.Stop(); if (result?.Data != null && roadieUser != null) { @@ -122,17 +124,17 @@ namespace Roadie.Api.Services { var user = GetUser(roadieUser.UserId); foreach (var track in result.Data.Tracks) + { track.Track.TrackPlayUrl = MakeTrackPlayUrl(user, track.Track.DatabaseId, track.Track.Id); + } } result.Data.UserCanEdit = result.Data.Maintainer.Id == roadieUser.UserId || roadieUser.IsAdmin; - var userBookmarkResult = - await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Playlist); + var userBookmarkResult = await BookmarkService.List(roadieUser, new PagedRequest(), false, BookmarkType.Playlist); if (userBookmarkResult.IsSuccess) - result.Data.UserBookmarked = - userBookmarkResult?.Rows?.FirstOrDefault(x => x.Bookmark.Text == result.Data.Id.ToString()) != - null; - + { + result.Data.UserBookmarked = userBookmarkResult?.Rows?.FirstOrDefault(x => x.Bookmark.Text == result.Data.Id.ToString()) != null; + } if (result.Data.Comments.Any()) { var commentIds = result.Data.Comments.Select(x => x.DatabaseId).ToArray(); @@ -142,8 +144,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 = userCommentReactions.FirstOrDefault(x => x.CommentId == comment.DatabaseId); comment.IsDisliked = userCommentReaction?.ReactionValue == CommentReaction.Dislike; comment.IsLiked = userCommentReaction?.ReactionValue == CommentReaction.Like; } @@ -402,15 +403,14 @@ namespace Roadie.Api.Services var playlist = GetPlaylist(id); if (playlist == null) - return Task.FromResult(new OperationResult(true, - string.Format("Playlist Not Found [{0}]", id))); - + { + return Task.FromResult(new OperationResult(true, string.Format("Playlist Not Found [{0}]", id))); + } var result = playlist.Adapt(); result.AlternateNames = playlist.AlternateNames; result.Tags = playlist.Tags; result.URLs = playlist.URLs; - var maintainer = DbContext.Users.Include(x => x.UserRoles).Include("UserRoles.Role") - .FirstOrDefault(x => x.Id == playlist.UserId); + var maintainer = DbContext.Users.Include(x => x.UserRoles).Include("UserRoles.Role").FirstOrDefault(x => x.Id == playlist.UserId); result.Maintainer = UserList.FromDataUser(maintainer, MakeUserThumbnailImage(maintainer.RoadieId)); result.Thumbnail = MakePlaylistThumbnailImage(playlist.RoadieId); result.MediumThumbnail = MakeThumbnailImage(id, "playlist", Configuration.MediumImageSize.Width, @@ -424,6 +424,7 @@ namespace Roadie.Api.Services select new { t, pltr }).ToArray(); if (includes.Contains("stats")) + { result.Statistics = new ReleaseGroupingStatistics { ReleaseCount = result.ReleaseCount, @@ -431,7 +432,9 @@ namespace Roadie.Api.Services TrackSize = result.DurationTime, FileSize = playlistTracks.Sum(x => (long?)x.t.FileSize).ToFileSize() }; + } if (includes.Contains("tracks")) + { result.Tracks = (from plt in playlistTracks join rm in DbContext.ReleaseMedias on plt.t.ReleaseMediaId equals rm.Id join r in DbContext.Releases on rm.ReleaseId equals r.Id @@ -453,10 +456,13 @@ namespace Roadie.Api.Services MakeArtistThumbnailImage(releaseArtist.RoadieId), MakeArtistThumbnailImage(trackArtist == null ? null : (Guid?)trackArtist.RoadieId)) }).ToArray(); + } if (includes.Contains("comments")) { var playlistComments = DbContext.Comments.Include(x => x.User) - .Where(x => x.PlaylistId == playlist.Id).OrderByDescending(x => x.CreatedDate).ToArray(); + .Where(x => x.PlaylistId == playlist.Id) + .OrderByDescending(x => x.CreatedDate) + .ToArray(); if (playlistComments.Any()) { var comments = new List(); @@ -468,15 +474,11 @@ namespace Roadie.Api.Services { var comment = playlistComment.Adapt(); comment.DatabaseId = playlistComment.Id; - comment.User = UserList.FromDataUser(playlistComment.User, - MakeUserThumbnailImage(playlistComment.User.RoadieId)); - comment.DislikedCount = userCommentReactions.Count(x => - x.CommentId == playlistComment.Id && x.ReactionValue == CommentReaction.Dislike); - comment.LikedCount = userCommentReactions.Count(x => - x.CommentId == playlistComment.Id && x.ReactionValue == CommentReaction.Like); + comment.User = UserList.FromDataUser(playlistComment.User, MakeUserThumbnailImage(playlistComment.User.RoadieId)); + comment.DislikedCount = userCommentReactions.Count(x => x.CommentId == playlistComment.Id && x.ReactionValue == CommentReaction.Dislike); + comment.LikedCount = userCommentReactions.Count(x => x.CommentId == playlistComment.Id && x.ReactionValue == CommentReaction.Like); comments.Add(comment); } - result.Comments = comments; } } diff --git a/Roadie.Api.Services/ReleaseService.cs b/Roadie.Api.Services/ReleaseService.cs index f112260..d426407 100644 --- a/Roadie.Api.Services/ReleaseService.cs +++ b/Roadie.Api.Services/ReleaseService.cs @@ -211,8 +211,16 @@ namespace Roadie.Api.Services select a.Id; IQueryable genreReleaseIds = null; var isFilteredToGenre = false; - if (!string.IsNullOrEmpty(request.FilterByGenre) || !string.IsNullOrEmpty(request.Filter) && - request.Filter.StartsWith(":genre", StringComparison.OrdinalIgnoreCase)) + if(request.FilterToGenreId.HasValue) + { + genreReleaseIds = (from rg in DbContext.ReleaseGenres + join g in DbContext.Genres on rg.GenreId equals g.Id + where g.RoadieId == request.FilterToGenreId + select rg.ReleaseId) + .Distinct(); + isFilteredToGenre = true; + } + 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 ", "", @@ -221,7 +229,7 @@ namespace Roadie.Api.Services join g in DbContext.Genres on rg.GenreId equals g.Id where g.Name.Contains(genreFilter) select rg.ReleaseId) - .Distinct(); + .Distinct(); request.Filter = null; isFilteredToGenre = true; } @@ -314,9 +322,11 @@ namespace Roadie.Api.Services if (doRandomize ?? false) { - var randomLimit = roadieUser?.RandomReleaseLimit ?? 100; + var randomLimit = roadieUser?.RandomReleaseLimit ?? request.Limit; request.Limit = request.LimitValue > randomLimit ? randomLimit : request.LimitValue; - rows = result.OrderBy(x => x.RandomSortId).Skip(request.SkipValue).Take(request.LimitValue).ToArray(); + rows = result.OrderBy(x => x.RandomSortId) + .Take(request.LimitValue) + .ToArray(); } else { diff --git a/Roadie.Api.Services/ServiceBase.cs b/Roadie.Api.Services/ServiceBase.cs index f113ad1..8b801c6 100644 --- a/Roadie.Api.Services/ServiceBase.cs +++ b/Roadie.Api.Services/ServiceBase.cs @@ -125,6 +125,15 @@ namespace Roadie.Api.Services }, data.Label.CacheRegionUrn(id)); } + protected data.Genre GetGenre(Guid id) + { + return CacheManager.Get(data.Genre.CacheUrn(id), () => + { + return DbContext.Genres + .FirstOrDefault(x => x.RoadieId == id); + }, data.Genre.CacheRegionUrn(id)); + } + protected data.Playlist GetPlaylist(Guid id) { return CacheManager.Get(data.Playlist.CacheUrn(id), () => @@ -245,6 +254,11 @@ namespace Roadie.Api.Services return MakeThumbnailImage(id, "label"); } + protected Image MakeGenreThumbnailImage(Guid id) + { + return MakeThumbnailImage(id, "genre"); + } + protected string MakeLastFmUrl(string artistName, string releaseTitle) { return "http://www.last.fm/music/" + HttpEncoder.UrlEncode($"{artistName}/{releaseTitle}"); diff --git a/Roadie.Api.Services/TrackService.cs b/Roadie.Api.Services/TrackService.cs index 86bac0f..ed6f707 100644 --- a/Roadie.Api.Services/TrackService.cs +++ b/Roadie.Api.Services/TrackService.cs @@ -518,8 +518,12 @@ namespace Roadie.Api.Services if (doRandomize ?? false) { + var randomLimit = roadieUser?.RandomReleaseLimit ?? request.Limit; + request.Limit = request.LimitValue > randomLimit ? randomLimit : request.LimitValue; + rows = result.OrderBy(x => x.Artist.RandomSortId) .ThenBy(x => x.RandomSortId) + .Take(request.LimitValue) .ToArray(); } else diff --git a/Roadie.Api.Services/UserService.cs b/Roadie.Api.Services/UserService.cs index eabdc43..2460f10 100644 --- a/Roadie.Api.Services/UserService.cs +++ b/Roadie.Api.Services/UserService.cs @@ -50,14 +50,20 @@ namespace Roadie.Api.Services ; } - public async Task> ById(User user, Guid id, IEnumerable includes) + public async Task> ById(User user, Guid id, IEnumerable includes, bool isAccountSettingsEdit = false) { var timings = new Dictionary(); var tsw = new Stopwatch(); - + if(isAccountSettingsEdit) + { + if(user.UserId != id && !user.IsAdmin) + { + return new OperationResult(new Exception("Access Denied")); + } + } var sw = Stopwatch.StartNew(); sw.Start(); - var cacheKey = string.Format("urn:user_by_id_operation:{0}", id); + var cacheKey = string.Format("urn:user_by_id_operation:{0}:{1}", id, isAccountSettingsEdit); var result = await CacheManager.GetAsync(cacheKey, async () => { tsw.Restart(); @@ -67,7 +73,15 @@ namespace Roadie.Api.Services return rr; }, ApplicationUser.CacheRegionUrn(id)); sw.Stop(); - if (result?.Data != null) result.Data.Avatar = MakeUserThumbnailImage(id); + if (result?.Data != null) + { + result.Data.Avatar = MakeUserThumbnailImage(id); + if(!isAccountSettingsEdit) + { + result.Data.ApiToken = null; + result.Data.ConcurrencyStamp = null; + } + } timings.Add("operation", sw.ElapsedMilliseconds); Logger.LogDebug("ById Timings: id [{0}]", id); return new OperationResult(result.Messages) @@ -101,6 +115,7 @@ namespace Roadie.Api.Services Value = u.RoadieId.ToString() }, IsEditor = u.UserRoles.Any(x => x.Role.Name == "Editor"), + IsAdmin = u.UserRoles.Any(x => x.Role.Name == "Admin"), IsPrivate = u.IsPrivate, Thumbnail = MakeUserThumbnailImage(u.RoadieId), CreatedDate = u.CreatedDate, @@ -119,15 +134,18 @@ namespace Roadie.Api.Services rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray(); if (rows.Any()) + { foreach (var row in rows) { var userArtists = DbContext.UserArtists.Include(x => x.Artist) - .Where(x => x.UserId == row.DatabaseId).ToArray(); + .Where(x => x.UserId == row.DatabaseId) + .ToArray(); var userReleases = DbContext.UserReleases.Include(x => x.Release) - .Where(x => x.UserId == row.DatabaseId).ToArray(); - var userTracks = DbContext.UserTracks.Include(x => x.Track).Where(x => x.UserId == row.DatabaseId) - .ToArray(); - + .Where(x => x.UserId == row.DatabaseId) + .ToArray(); + var userTracks = DbContext.UserTracks.Include(x => x.Track) + .Where(x => x.UserId == row.DatabaseId) + .ToArray(); row.Statistics = new UserStatistics { RatedArtists = userArtists.Where(x => x.Rating > 0).Count(), @@ -142,6 +160,7 @@ namespace Roadie.Api.Services DislikedTracks = userTracks.Where(x => x.IsDisliked ?? false).Count() }; } + } sw.Stop(); return Task.FromResult(new Library.Models.Pagination.PagedResult @@ -347,41 +366,48 @@ namespace Roadie.Api.Services { var user = DbContext.Users.FirstOrDefault(x => x.RoadieId == userBeingUpdatedModel.UserId); if (user == null) - return new OperationResult(true, - string.Format("User Not Found [{0}]", userBeingUpdatedModel.UserId)); + { + return new OperationResult(true, string.Format("User Not Found [{0}]", userBeingUpdatedModel.UserId)); + } if (user.Id != userPerformingUpdate.Id && !userPerformingUpdate.IsAdmin) + { return new OperationResult { Errors = new List { new Exception("Access Denied") } }; + } // Check concurrency stamp if (user.ConcurrencyStamp != userBeingUpdatedModel.ConcurrencyStamp) + { return new OperationResult { Errors = new List { new Exception("User data is stale.") } }; + } // Check that username (if changed) doesn't already exist if (user.UserName != userBeingUpdatedModel.UserName) { - var userByUsername = DbContext.Users.FirstOrDefault(x => - x.NormalizedUserName == userBeingUpdatedModel.UserName.ToUpper()); + var userByUsername = DbContext.Users.FirstOrDefault(x => x.NormalizedUserName == userBeingUpdatedModel.UserName.ToUpper()); if (userByUsername != null) + { return new OperationResult { Errors = new List { new Exception("Username already in use") } }; + } } // Check that email (if changed) doesn't already exist if (user.Email != userBeingUpdatedModel.Email) { - var userByEmail = - DbContext.Users.FirstOrDefault(x => x.NormalizedEmail == userBeingUpdatedModel.Email.ToUpper()); + var userByEmail = DbContext.Users.FirstOrDefault(x => x.NormalizedEmail == userBeingUpdatedModel.Email.ToUpper()); if (userByEmail != null) + { return new OperationResult { Errors = new List { new Exception("Email already in use") } }; + } } var oldPathToImage = user.PathToImage(Configuration); var didChangeName = user.UserName != userBeingUpdatedModel.UserName; @@ -405,6 +431,7 @@ namespace Roadie.Api.Services user.FtpUsername = userBeingUpdatedModel.FtpUsername; user.FtpPassword = EncryptionHelper.Encrypt(userBeingUpdatedModel.FtpPassword, user.RoadieId.ToString()); user.ConcurrencyStamp = Guid.NewGuid().ToString(); + user.DefaultRowsPerPage = userBeingUpdatedModel.DefaultRowsPerPage; if (didChangeName) { @@ -414,7 +441,6 @@ namespace Roadie.Api.Services } } - if (!string.IsNullOrEmpty(userBeingUpdatedModel.AvatarData)) { var imageData = ImageHelper.ImageDataFromUrl(userBeingUpdatedModel.AvatarData); @@ -560,33 +586,43 @@ namespace Roadie.Api.Services join ut in DbContext.UserTracks on t.Id equals ut.TrackId where ut.UserId == user.Id select new { a, ut.PlayedCount }) - .GroupBy(a => a.a) - .Select(x => new - { - Artist = x.Key, - Played = x.Sum(t => t.PlayedCount) - }) - .OrderByDescending(x => x.Played) - .FirstOrDefault(); + .GroupBy(a => a.a) + .Select(x => new + { + Artist = x.Key, + Played = x.Sum(t => t.PlayedCount) + }) + .OrderByDescending(x => x.Played) + .FirstOrDefault(); var mostPlayedReleaseId = (from r in DbContext.Releases join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId join t in DbContext.Tracks on rm.Id equals t.ReleaseMediaId join ut in DbContext.UserTracks on t.Id equals ut.TrackId where ut.UserId == user.Id select new { r, ut.PlayedCount }) - .GroupBy(r => r.r) - .Select(x => new - { - Release = x.Key, - Played = x.Sum(t => t.PlayedCount) - }) - .OrderByDescending(x => x.Played) - .Select(x => x.Release.RoadieId) - .FirstOrDefault(); + .GroupBy(r => r.r) + .Select(x => new + { + Release = x.Key, + Played = x.Sum(t => t.PlayedCount) + }) + .OrderByDescending(x => x.Played) + .Select(x => x.Release.RoadieId) + .FirstOrDefault(); var mostPlayedRelease = GetRelease(mostPlayedReleaseId); - var mostPlayedTrackUserTrack = userTracks - .OrderByDescending(x => x.PlayedCount) - .FirstOrDefault(); + var mostPlayedTrackUserTrack = userTracks.OrderByDescending(x => x.PlayedCount) + .FirstOrDefault(); + var lastPlayedTrackUserTrack = userTracks.OrderByDescending(x => x.LastPlayed) + .FirstOrDefault(); + + var lastPlayedTrack = lastPlayedTrackUserTrack == null + ? null + : DbContext.Tracks + .Include(x => x.TrackArtist) + .Include(x => x.ReleaseMedia) + .Include("ReleaseMedia.Release") + .Include("ReleaseMedia.Release.Artist") + .FirstOrDefault(x => x.Id == lastPlayedTrackUserTrack.TrackId); var mostPlayedTrack = mostPlayedTrackUserTrack == null ? null : DbContext.Tracks @@ -598,6 +634,22 @@ namespace Roadie.Api.Services model.Statistics = new UserStatistics { + LastPlayedTrack = lastPlayedTrack == null + ? null + : models.TrackList.FromDataTrack( + MakeTrackPlayUrl(user, lastPlayedTrack.Id, lastPlayedTrack.RoadieId), + lastPlayedTrack, + lastPlayedTrack.ReleaseMedia.MediaNumber, + lastPlayedTrack.ReleaseMedia.Release, + lastPlayedTrack.ReleaseMedia.Release.Artist, + lastPlayedTrack.TrackArtist, + HttpContext.BaseUrl, + MakeTrackThumbnailImage(lastPlayedTrack.RoadieId), + MakeReleaseThumbnailImage(lastPlayedTrack.ReleaseMedia.Release.RoadieId), + MakeArtistThumbnailImage(lastPlayedTrack.ReleaseMedia.Release.Artist.RoadieId), + MakeArtistThumbnailImage(lastPlayedTrack.TrackArtist == null + ? null + : (Guid?)lastPlayedTrack.TrackArtist.RoadieId)), MostPlayedArtist = mostPlayedArtist == null ? null : models.ArtistList.FromDataArtist(mostPlayedArtist.Artist, @@ -634,7 +686,7 @@ namespace Roadie.Api.Services 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() + DislikedTracks = userTracks.Where(x => x.IsDisliked ?? false).Count() }; } } diff --git a/Roadie.Api/Controllers/AccountController.cs b/Roadie.Api/Controllers/AccountController.cs index a5473be..f753dea 100644 --- a/Roadie.Api/Controllers/AccountController.cs +++ b/Roadie.Api/Controllers/AccountController.cs @@ -102,9 +102,11 @@ namespace Roadie.Api.Controllers try { // Login user - var loginResult = - await SignInManager.PasswordSignInAsync(model.Username, model.Password, false, false); - if (!loginResult.Succeeded) return BadRequest(); + var loginResult = await SignInManager.PasswordSignInAsync(model.Username, model.Password, false, false); + if (!loginResult.Succeeded) + { + return BadRequest(); + } var user = await UserManager.FindByNameAsync(model.Username); var now = DateTime.UtcNow; user.LastLogin = now; @@ -127,8 +129,7 @@ namespace Roadie.Api.Controllers } CacheManager.ClearRegion(EntityControllerBase.ControllerCacheRegionUrn); - var avatarUrl = - $"{RoadieHttpContext.ImageBaseUrl}/user/{user.RoadieId}/{RoadieSettings.ThumbnailImageSize.Width}/{RoadieSettings.ThumbnailImageSize.Height}"; + var avatarUrl = $"{RoadieHttpContext.ImageBaseUrl}/user/{user.RoadieId}/{RoadieSettings.ThumbnailImageSize.Width}/{RoadieSettings.ThumbnailImageSize.Height}"; return Ok(new { Username = user.UserName, @@ -139,7 +140,8 @@ namespace Roadie.Api.Controllers avatarUrl, Token = t, user.Timeformat, - user.Timezone + user.Timezone, + DefaultRowsPerPage = user.DefaultRowsPerPage ?? RoadieSettings.DefaultRowsPerPage }); } catch (Exception ex) diff --git a/Roadie.Api/Controllers/AdminController.cs b/Roadie.Api/Controllers/AdminController.cs index 1d76b47..9bd0cff 100644 --- a/Roadie.Api/Controllers/AdminController.cs +++ b/Roadie.Api/Controllers/AdminController.cs @@ -82,6 +82,15 @@ namespace Roadie.Api.Controllers return Ok(result); } + [HttpPost("delete/genre/{id}")] + [ProducesResponseType(200)] + public async Task DeleteGenre(Guid id) + { + var result = await AdminService.DeleteGenre(await UserManager.GetUserAsync(User), id); + if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); + return Ok(result); + } + [HttpPost("delete/releasesecondaryimage/{id}/{index}")] [ProducesResponseType(200)] public async Task DeleteReleaseSecondaryImage(Guid id, int index) diff --git a/Roadie.Api/Controllers/GenreController.cs b/Roadie.Api/Controllers/GenreController.cs index e5cd062..a5a71a7 100644 --- a/Roadie.Api/Controllers/GenreController.cs +++ b/Roadie.Api/Controllers/GenreController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -10,6 +11,8 @@ using Roadie.Library.Models.Pagination; using System; using System.Net; using System.Threading.Tasks; +using System.Web; +using models = Roadie.Library.Models; namespace Roadie.Api.Controllers { @@ -29,33 +32,16 @@ namespace Roadie.Api.Controllers GenreService = genreService; } - //[EnableQuery] - //public IActionResult Get() - //{ - // return Ok(this._RoadieDbContext.Labels.ProjectToType()); - //} - - //[HttpGet("{id}")] - //[ProducesResponseType(200)] - //[ProducesResponseType(404)] - //public IActionResult Get(Guid id) - //{ - // var key = id.ToString(); - // var result = this._cacheManager.Get(key, () => - // { - // var d = this._RoadieDbContext.Labels.FirstOrDefault(x => x.RoadieId == id); - // if (d != null) - // { - // return d.Adapt(); - // } - // return null; - // }, key); - // if (result == null) - // { - // return NotFound(); - // } - // return Ok(result); - //} + [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task Get(Guid id, string inc = null) + { + var result = await GenreService.ById(await CurrentUserModel(), id,(inc ?? models.Genre.DefaultIncludes).ToLower().Split(",")); + if (result == null || result.IsNotFoundResult) return NotFound(); + if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); + return Ok(result); + } [HttpGet] [ProducesResponseType(200)] @@ -80,5 +66,35 @@ namespace Roadie.Api.Controllers return StatusCode((int)HttpStatusCode.InternalServerError); } + + [HttpPost("setImageByUrl/{id}/{imageUrl}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [Authorize(Policy = "Editor")] + public async Task SetGenreImageByUrl(Guid id, string imageUrl) + { + var result = await GenreService.SetGenreImageByUrl(await CurrentUserModel(), id, HttpUtility.UrlDecode(imageUrl)); + if (result == null || result.IsNotFoundResult) + { + return NotFound(); + } + if (!result.IsSuccess) + { + return StatusCode((int)HttpStatusCode.InternalServerError); + } + return Ok(result); + } + + [HttpPost("uploadImage/{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [Authorize(Policy = "Editor")] + public async Task UploadImage(Guid id, IFormFile file) + { + var result = await GenreService.UploadGenreImage(await CurrentUserModel(), id, file); + if (result == null || result.IsNotFoundResult) return NotFound(); + if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); + return Ok(result); + } } } \ No newline at end of file diff --git a/Roadie.Api/Controllers/ImageController.cs b/Roadie.Api/Controllers/ImageController.cs index 0b24cbc..6b6ad46 100644 --- a/Roadie.Api/Controllers/ImageController.cs +++ b/Roadie.Api/Controllers/ImageController.cs @@ -117,6 +117,21 @@ namespace Roadie.Api.Controllers result.ETag); } + [HttpGet("genre/{id}/{width:int?}/{height:int?}/{cacheBuster?}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task GenreImage(Guid id, int? width, int? height) + { + var result = await ImageService.GenreImage(id, width ?? RoadieSettings.ThumbnailImageSize.Width, height ?? RoadieSettings.ThumbnailImageSize.Height); + if (result == null || result.IsNotFoundResult) return NotFound(); + if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); + return File(result.Data.Bytes, + result.ContentType, + $"{result.Data.Caption ?? id.ToString()}.jpg", + result.LastModified, + result.ETag); + } + [HttpGet("playlist/{id}/{width:int?}/{height:int?}/{cacheBuster?}")] [ProducesResponseType(200)] [ProducesResponseType(404)] @@ -186,6 +201,17 @@ namespace Roadie.Api.Controllers return Ok(result); } + [HttpPost("search/genre/{query}/{resultsCount:int?}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task SearchForGenreImage(string query, int? resultsCount) + { + var result = await ImageService.Search(query, resultsCount ?? 10); + if (result == null || result.IsNotFoundResult) return NotFound(); + if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); + return Ok(result); + } + [HttpPost("search/release/{query}/{resultsCount:int?}")] [ProducesResponseType(200)] [ProducesResponseType(404)] diff --git a/Roadie.Api/Controllers/UserController.cs b/Roadie.Api/Controllers/UserController.cs index 115132d..fbae03c 100644 --- a/Roadie.Api/Controllers/UserController.cs +++ b/Roadie.Api/Controllers/UserController.cs @@ -40,6 +40,36 @@ namespace Roadie.Api.Controllers RoadieHttpContext = httpContext; } + + [HttpGet("accountsettings/{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(404)] + public async Task GetAccountSettings(Guid id, string inc = null) + { + var user = await CurrentUserModel(); + var result = await CacheManager.GetAsync($"urn:user_edit_model_by_id:{id}", async () => + { + return await UserService.ById(user, id, (inc ?? Library.Models.Users.User.DefaultIncludes).ToLower().Split(","), true); + }, ControllerCacheRegionUrn); + if (result == null || result.IsNotFoundResult) + { + return NotFound(); + } + if (!result.IsSuccess) + { + return StatusCode((int)HttpStatusCode.InternalServerError); + } + result.AdditionalClientData = new Dictionary(); + if (RoadieSettings.Integrations.LastFmProviderEnabled) + { + var lastFmCallBackUrl = $"{RoadieHttpContext.BaseUrl}/users/integration/grant?userId={user.UserId}&iname=lastfm"; + result.AdditionalClientData.Add("lastFMIntegrationUrl", $"http://www.last.fm/api/auth/?api_key={RoadieSettings.Integrations.LastFMApiKey}&cb={WebUtility.UrlEncode(lastFmCallBackUrl)}"); + } + return Ok(result); + } + + [HttpGet("{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] @@ -52,17 +82,14 @@ namespace Roadie.Api.Controllers { return await UserService.ById(user, id, (inc ?? Library.Models.Users.User.DefaultIncludes).ToLower().Split(",")); }, ControllerCacheRegionUrn); - if (result == null || result.IsNotFoundResult) return NotFound(); - if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError); - result.AdditionalClientData = new Dictionary(); - if (RoadieSettings.Integrations.LastFmProviderEnabled) + if (result == null || result.IsNotFoundResult) { - var lastFmCallBackUrl = - $"{RoadieHttpContext.BaseUrl}/users/integration/grant?userId={user.UserId}&iname=lastfm"; - result.AdditionalClientData.Add("lastFMIntegrationUrl", - $"http://www.last.fm/api/auth/?api_key={RoadieSettings.Integrations.LastFMApiKey}&cb={WebUtility.UrlEncode(lastFmCallBackUrl)}"); + return NotFound(); + } + if (!result.IsSuccess) + { + return StatusCode((int)HttpStatusCode.InternalServerError); } - return Ok(result); } @@ -239,7 +266,10 @@ namespace Roadie.Api.Controllers [ProducesResponseType(200)] public async Task UpdateProfile(User model) { - if (!ModelState.IsValid) return BadRequest(ModelState); + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } var user = await CurrentUserModel(); var result = await UserService.UpdateProfile(user, model); if (result == null || result.IsNotFoundResult) return NotFound(); @@ -260,7 +290,8 @@ namespace Roadie.Api.Controllers avatarUrl, Token = t, modelUser.Timeformat, - modelUser.Timezone + modelUser.Timezone, + DefaultRowsPerPage = modelUser.DefaultRowsPerPage ?? RoadieSettings.DefaultRowsPerPage }); } diff --git a/Roadie.Api/wwwroot/Images/genre.jpg b/Roadie.Api/wwwroot/Images/genre.jpg new file mode 100644 index 0000000..a97f548 Binary files /dev/null and b/Roadie.Api/wwwroot/Images/genre.jpg differ diff --git a/Roadie.sln b/Roadie.sln index af5d8f4..c13fa31 100644 --- a/Roadie.sln +++ b/Roadie.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{1BA7 Upgrade0003.sql = Upgrade0003.sql Upgrade0004.sql = Upgrade0004.sql Upgrade0005.sql = Upgrade0005.sql + Upgrade0006.sql = Upgrade0006.sql EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Api.Services", "Roadie.Api.Services\Roadie.Api.Services.csproj", "{7B37031E-F2AE-4BE2-9F6F-005CA7A6FDF1}" diff --git a/Upgrade0006.sql b/Upgrade0006.sql new file mode 100644 index 0000000..164169b --- /dev/null +++ b/Upgrade0006.sql @@ -0,0 +1,6 @@ +-- v1.0.3.1 -- Genre tables modifications for https://github.com/sphildreth/roadie-vuejs/issues/63 +ALTER TABLE `genre` ADD `thumbnail` BLOB NULL; +ALTER TABLE `genre` ADD `alternateNames` MEDIUMTEXT NULL; +ALTER TABLE `genre` ADD `description` varchar(4000) NULL; +ALTER TABLE `genre` ADD `tags` MEDIUMTEXT NULL; +ALTER TABLE `user` ADD `defaultRowsPerPage` SMALLINT NULL; \ No newline at end of file diff --git a/roadie.sql b/roadie.sql index 666037c..f4f354e 100644 --- a/roadie.sql +++ b/roadie.sql @@ -1,5 +1,5 @@ -- /// --- Roadie version 1.0.2.3 new database script, if upgrading skip this and run Upgrade*.sql scripts from your version to current. +-- Roadie version 1.0.3.1 new database script, if upgrading skip this and run Upgrade*.sql scripts from your version to current. -- /// -- MySQL dump 10.17 Distrib 10.3.16-MariaDB, for Linux (x86_64) -- @@ -338,6 +338,10 @@ CREATE TABLE `genre` ( `lastUpdated` datetime DEFAULT NULL, `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `normalizedName` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `thumbnail` blob DEFAULT NULL, + `alternateNames` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` varchar(4000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `tags` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ix_genre_name` (`name`), KEY `ix_genre_roadieId` (`roadieId`), @@ -629,7 +633,7 @@ CREATE TABLE `request` ( KEY `ix_request_roadieId` (`roadieId`), KEY `requestartist_ibfk_1` (`userId`), CONSTRAINT `requestartist_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=148 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -655,7 +659,7 @@ CREATE TABLE `scanHistory` ( PRIMARY KEY (`id`), KEY `ix_scanHistory_roadieId` (`roadieId`), KEY `rscanHistoryt_ibfk_1` (`userId`) -) ENGINE=InnoDB AUTO_INCREMENT=97 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=107 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -723,7 +727,7 @@ CREATE TABLE `track` ( KEY `track_artistId_IDX` (`artistId`) USING BTREE, KEY `track_releaseMediaId_IDX` (`releaseMediaId`) USING BTREE, CONSTRAINT `track_ibfk_1` FOREIGN KEY (`releaseMediaId`) REFERENCES `releasemedia` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=615 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=623 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -959,7 +963,7 @@ CREATE TABLE `usersInRoles` ( KEY `ix_usersInRoles_userId` (`userId`), CONSTRAINT `usersInRoles_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE, CONSTRAINT `usersInRoles_ibfk_2` FOREIGN KEY (`userRoleId`) REFERENCES `userrole` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -989,7 +993,7 @@ CREATE TABLE `usertrack` ( KEY `ix_usertrack_roadieId` (`roadieId`), CONSTRAINT `usertrack_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE, CONSTRAINT `usertrack_ibfk_2` FOREIGN KEY (`trackId`) REFERENCES `track` (`id`) ON DELETE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=237 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=238 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1075,4 +1079,4 @@ SET character_set_client = @saved_cs_client; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2019-07-18 17:31:08 +-- Dump completed on 2019-08-02 11:14:18