diff --git a/Roadie.Api.Library.Tests/FolderPathHelperTests.cs b/Roadie.Api.Library.Tests/FolderPathHelperTests.cs index 3f48e7c..7eb5042 100644 --- a/Roadie.Api.Library.Tests/FolderPathHelperTests.cs +++ b/Roadie.Api.Library.Tests/FolderPathHelperTests.cs @@ -105,6 +105,22 @@ namespace Roadie.Library.Tests Assert.Equal(t.FullName, artistFolder); } + [Theory] + [InlineData("!SÖmëthing el$e", @"S\SO")] + [InlineData("Alternative Singer/Songwriter", @"A\AL")] + [InlineData("Adult Alternative", @"A\AD")] + [InlineData("Progressive Bluegrass", @"P\PR")] + [InlineData("Western European Traditions", @"W\WE")] + [InlineData("2-Step/British Garage", @"0\2S")] + [InlineData("80's/synthwave/outrun/new retro wave", @"0\80")] + [InlineData("Western Swing Revival", @"W\WE")] + public void GenerateGenreFolderNames(string input, string shouldBe) + { + var artistFolder = FolderPathHelper.GenrePath(Configuration, input); + var t = new DirectoryInfo(Path.Combine(Configuration.GenreImageFolder, shouldBe)); + Assert.Equal(t.FullName, artistFolder); + } + [Theory] [InlineData("What Dreams May Come", "01/15/2004", @"D\DR\Dream Theater [99]", @"D\DR\Dream Theater [99]\[2004] What Dreams May Come")] [InlineData("Killers", "01/01/1980", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1980] Killers")] diff --git a/Roadie.Api.Library/Data/CollectionPartial.cs b/Roadie.Api.Library/Data/CollectionPartial.cs index dca475f..55d762c 100644 --- a/Roadie.Api.Library/Data/CollectionPartial.cs +++ b/Roadie.Api.Library/Data/CollectionPartial.cs @@ -44,9 +44,14 @@ namespace Roadie.Library.Data /// /// Returns a full file path to the Collection Image /// - public string PathToImage(IRoadieSettings configuration) + public string PathToImage(IRoadieSettings configuration, bool makeFolderIfNotExist = false) { - return Path.Combine(configuration.CollectionImageFolder, $"{ (SortName ?? Name).ToFileNameFriendly() } [{ Id }].jpg"); + var folder = configuration.CollectionImageFolder; + if (!Directory.Exists(folder) && makeFolderIfNotExist) + { + Directory.CreateDirectory(folder); + } + return Path.Combine(folder, $"{ (SortName ?? Name).ToFileNameFriendly() } [{ Id }].jpg"); } public int PositionColumn diff --git a/Roadie.Api.Library/Data/Genre.cs b/Roadie.Api.Library/Data/Genre.cs index e277819..6ecd9ec 100644 --- a/Roadie.Api.Library/Data/Genre.cs +++ b/Roadie.Api.Library/Data/Genre.cs @@ -12,7 +12,16 @@ namespace Roadie.Library.Data public ICollection Comments { get; set; } - [Column("name")] [MaxLength(100)] public string Name { get; set; } + [Column("name")] + [MaxLength(100)] + [Required] + public string Name { get; set; } + + [Column("sortName")] + [MaxLength(100)] + public string SortName { get; set; } + + public string SortNameValue => string.IsNullOrEmpty(SortName) ? Name : SortName; [Column("description")] [MaxLength(4000)] diff --git a/Roadie.Api.Library/Data/GenrePartial.cs b/Roadie.Api.Library/Data/GenrePartial.cs index d871acc..a4188f1 100644 --- a/Roadie.Api.Library/Data/GenrePartial.cs +++ b/Roadie.Api.Library/Data/GenrePartial.cs @@ -1,5 +1,6 @@ using Roadie.Library.Configuration; using Roadie.Library.Extensions; +using Roadie.Library.Utility; using System; using System.IO; @@ -11,14 +12,37 @@ 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"); + //} + /// /// Returns a full file path to the Genre Image /// - public string PathToImage(IRoadieSettings configuration) + public string PathToImage(IRoadieSettings configuration, bool makeFolderIfNotExist = false) { - return Path.Combine(configuration.GenreImageFolder, $"{ Name.ToFileNameFriendly() } [{ Id }].jpg"); + var folder = FolderPathHelper.GenrePath(configuration, SortNameValue); + if(!Directory.Exists(folder) && makeFolderIfNotExist) + { + Directory.CreateDirectory(folder); + } + return Path.Combine(folder, $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg"); } + /// + /// Returns a full file path to the Label Image + /// + [Obsolete("This is only here for migration will be removed in future release.")] + public string OldPathToImage(IRoadieSettings configuration) + { + return Path.Combine(configuration.GenreImageFolder, $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg"); + } + + public static string CacheRegionUrn(Guid Id) { return string.Format("urn:genre:{0}", Id); diff --git a/Roadie.Api.Library/Data/LabelPartial.cs b/Roadie.Api.Library/Data/LabelPartial.cs index 5bcf372..252ff4f 100644 --- a/Roadie.Api.Library/Data/LabelPartial.cs +++ b/Roadie.Api.Library/Data/LabelPartial.cs @@ -28,9 +28,14 @@ namespace Roadie.Library.Data /// /// Returns a full file path to the Label Image /// - public string PathToImage(IRoadieSettings configuration) + public string PathToImage(IRoadieSettings configuration, bool makeFolderIfNotExist = false) { - return Path.Combine(FolderPathHelper.LabelPath(configuration, SortNameValue), $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg"); + var folder = FolderPathHelper.LabelPath(configuration, SortNameValue); + if (!Directory.Exists(folder) && makeFolderIfNotExist) + { + Directory.CreateDirectory(folder); + } + return Path.Combine(folder, $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg"); } /// diff --git a/Roadie.Api.Library/Data/PlaylistPartial.cs b/Roadie.Api.Library/Data/PlaylistPartial.cs index 6631ede..7c79135 100644 --- a/Roadie.Api.Library/Data/PlaylistPartial.cs +++ b/Roadie.Api.Library/Data/PlaylistPartial.cs @@ -25,9 +25,14 @@ namespace Roadie.Library.Data /// /// Returns a full file path to the Playlist Image /// - public string PathToImage(IRoadieSettings configuration) + public string PathToImage(IRoadieSettings configuration, bool makeFolderIfNotExist = false) { - return Path.Combine(configuration.PlaylistImageFolder, $"{ (SortName ?? Name).ToFileNameFriendly() } [{ Id }].jpg"); + var folder = configuration.PlaylistImageFolder; + if (!Directory.Exists(folder) && makeFolderIfNotExist) + { + Directory.CreateDirectory(folder); + } + return Path.Combine(folder, $"{ (SortName ?? Name).ToFileNameFriendly() } [{ Id }].jpg"); } public override string ToString() diff --git a/Roadie.Api.Library/Identity/ApplicationUserPartial.cs b/Roadie.Api.Library/Identity/ApplicationUserPartial.cs index a644ec2..e86780b 100644 --- a/Roadie.Api.Library/Identity/ApplicationUserPartial.cs +++ b/Roadie.Api.Library/Identity/ApplicationUserPartial.cs @@ -20,9 +20,14 @@ namespace Roadie.Library.Identity /// /// Returns a full file path to the User Image /// - public string PathToImage(IRoadieSettings configuration) + public string PathToImage(IRoadieSettings configuration, bool makeFolderIfNotExist = false) { - return Path.Combine(configuration.UserImageFolder, $"{ UserName.ToFileNameFriendly() } [{ Id }].gif"); + var folder = configuration.UserImageFolder; + if (!Directory.Exists(folder) && makeFolderIfNotExist) + { + Directory.CreateDirectory(folder); + } + return Path.Combine(folder, $"{ UserName.ToFileNameFriendly() } [{ Id }].gif"); } public ApplicationUser() diff --git a/Roadie.Api.Library/Utility/FolderPathHelper.cs b/Roadie.Api.Library/Utility/FolderPathHelper.cs index 5431df9..6ab3078 100644 --- a/Roadie.Api.Library/Utility/FolderPathHelper.cs +++ b/Roadie.Api.Library/Utility/FolderPathHelper.cs @@ -19,7 +19,8 @@ namespace Roadie.Library.Utility public static int MaximumLibraryFolderNameLength = 44; public static int MaximumArtistFolderNameLength = 100; public static int MaximumReleaseFolderNameLength = 100; - public static int MaximumLabelFolderNameLength = 100; + public static int MaximumLabelFolderNameLength = 80; + public static int MaximumGenreFolderNameLength = 80; public static int MaximumTrackFileNameLength = 500; public static IEnumerable FolderSpaceReplacements = new List { ".", "~", "_", "=", "-" }; @@ -78,11 +79,10 @@ namespace Roadie.Library.Utility return directoryInfo.FullName; } - public static string LabelPath(IRoadieSettings configuration, string labelSortName) { SimpleContract.Requires(!string.IsNullOrEmpty(labelSortName), "Invalid Label Sort Name"); - SimpleContract.Requires(configuration.LibraryFolder.Length < MaximumLibraryFolderNameLength, $"Library Folder maximum length is [{ MaximumLibraryFolderNameLength }]"); + SimpleContract.Requires(configuration.LabelImageFolder.Length < MaximumLabelFolderNameLength, $"Label Image Folder maximum length is [{ MaximumLibraryFolderNameLength }]"); var lsn = new StringBuilder(labelSortName); foreach (var stringReplacement in FolderSpaceReplacements) @@ -122,6 +122,50 @@ namespace Roadie.Library.Utility return directoryInfo.FullName; } + public static string GenrePath(IRoadieSettings configuration, string genreSortName) + { + SimpleContract.Requires(!string.IsNullOrEmpty(genreSortName), "Invalid Genre Sort Name"); + SimpleContract.Requires(configuration.GenreImageFolder.Length < MaximumGenreFolderNameLength, $"Genre Image Folder maximum length is [{ MaximumLibraryFolderNameLength }]"); + + var lsn = new StringBuilder(genreSortName); + foreach (var stringReplacement in FolderSpaceReplacements) + { + if (!lsn.Equals(stringReplacement)) + { + lsn.Replace(stringReplacement, " "); + } + } + var genreFolder = lsn.ToString().ToAlphanumericName(false, false).ToFolderNameFriendly().ToTitleCase(false); + if (string.IsNullOrEmpty(genreFolder)) + { + throw new Exception($"GenreFolder [{ genreFolder }] is invalid. GenreSortName [{ genreSortName }]."); + } + var lfUpper = genreFolder.ToUpper(); + var fnSubPart1 = lfUpper.ToUpper().ToCharArray().Take(1).First(); + if (!char.IsLetterOrDigit(fnSubPart1)) + { + fnSubPart1 = '#'; + } + else if (char.IsNumber(fnSubPart1)) + { + fnSubPart1 = '0'; + } + var fnSubPart2 = lfUpper.Length > 2 ? lfUpper.Substring(0, 2) : lfUpper; + if (fnSubPart2.EndsWith(" ")) + { + var pos = 1; + while (fnSubPart2.EndsWith(" ")) + { + pos++; + fnSubPart2 = fnSubPart2.Substring(0, 1) + lfUpper.Substring(pos, 1); + } + } + var fnSubPart = Path.Combine(fnSubPart1.ToString(), fnSubPart2); + var directoryInfo = new DirectoryInfo(Path.Combine(configuration.GenreImageFolder, fnSubPart)); + return directoryInfo.FullName; + } + + [Obsolete("This is only here for migration will be removed in future release.")] public static string ArtistPathOld(IRoadieSettings configuration, string artistSortName) { diff --git a/Roadie.Api.Services/AdminService.cs b/Roadie.Api.Services/AdminService.cs index 84b77b8..d571fac 100644 --- a/Roadie.Api.Services/AdminService.cs +++ b/Roadie.Api.Services/AdminService.cs @@ -1110,6 +1110,28 @@ namespace Roadie.Api.Services } Logger.LogInformation($"Label Migration Complete. Migrated [{ labelsMigrated }] Labels."); + var genresMigrated = 0; + foreach (var genre in DbContext.Genres.Where(x => x.Status == Statuses.ReadyToMigrate).ToArray()) + { + var oldGenreImageFileName = genre.OldPathToImage(Configuration); + var genreImageFileName = genre.PathToImage(Configuration); + if (File.Exists(oldGenreImageFileName)) + { + var genreFileInfo = new FileInfo(genreImageFileName); + if (!genreFileInfo.Directory.Exists) + { + Directory.CreateDirectory(genreFileInfo.Directory.FullName); + } + File.Move(oldGenreImageFileName, genreImageFileName, true); + genre.Status = Statuses.Migrated; + genre.LastUpdated = now; + await DbContext.SaveChangesAsync(); + Logger.LogInformation($"Migrated Genre Storage `{ genre}` From [{ oldGenreImageFileName }] => [{ genreImageFileName }]"); + genresMigrated++; + } + } + Logger.LogInformation($"Genre Migration Complete. Migrated [{ genresMigrated }] Genres."); + var releases = DbContext.Releases .Include(x => x.Artist) .Include(x => x.Medias) diff --git a/Roadie.Api.Services/CollectionService.cs b/Roadie.Api.Services/CollectionService.cs index 0dc198d..01d18f6 100644 --- a/Roadie.Api.Services/CollectionService.cs +++ b/Roadie.Api.Services/CollectionService.cs @@ -301,7 +301,7 @@ namespace Roadie.Api.Services if (collectionImage != null) { // Save unaltered collection image - File.WriteAllBytes(collection.PathToImage(Configuration), ImageHelper.ConvertToJpegFormat(collectionImage)); + File.WriteAllBytes(collection.PathToImage(Configuration, true), ImageHelper.ConvertToJpegFormat(collectionImage)); } if (model.Maintainer?.Value != null) { diff --git a/Roadie.Api.Services/GenreService.cs b/Roadie.Api.Services/GenreService.cs index bc86b23..3a712ac 100644 --- a/Roadie.Api.Services/GenreService.cs +++ b/Roadie.Api.Services/GenreService.cs @@ -236,14 +236,17 @@ namespace Roadie.Api.Services 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)); + if (genre == null) + { + return new OperationResult(true, string.Format("Genre Not Found [{0}]", id)); + } try { var now = DateTime.UtcNow; if (imageBytes != null) { - // Save unaltered label image - File.WriteAllBytes(genre.PathToImage(Configuration), ImageHelper.ConvertToJpegFormat(imageBytes)); + // Save unaltered genre image + File.WriteAllBytes(genre.PathToImage(Configuration, true), ImageHelper.ConvertToJpegFormat(imageBytes)); } genre.LastUpdated = now; await DbContext.SaveChangesAsync(); diff --git a/Roadie.Api.Services/LabelService.cs b/Roadie.Api.Services/LabelService.cs index 0173bd2..489bbe1 100644 --- a/Roadie.Api.Services/LabelService.cs +++ b/Roadie.Api.Services/LabelService.cs @@ -461,7 +461,7 @@ namespace Roadie.Api.Services if (imageBytes != null) { // Save unaltered label image - File.WriteAllBytes(label.PathToImage(Configuration), ImageHelper.ConvertToJpegFormat(imageBytes)); + File.WriteAllBytes(label.PathToImage(Configuration, true), ImageHelper.ConvertToJpegFormat(imageBytes)); } label.LastUpdated = now; await DbContext.SaveChangesAsync(); diff --git a/Roadie.Api.Services/PlaylistService.cs b/Roadie.Api.Services/PlaylistService.cs index e801752..9ecc3f3 100644 --- a/Roadie.Api.Services/PlaylistService.cs +++ b/Roadie.Api.Services/PlaylistService.cs @@ -352,7 +352,7 @@ namespace Roadie.Api.Services if (playlistImage != null) { // Save unaltered playlist image - File.WriteAllBytes(playlist.PathToImage(Configuration), ImageHelper.ConvertToJpegFormat(playlistImage)); + File.WriteAllBytes(playlist.PathToImage(Configuration, true), ImageHelper.ConvertToJpegFormat(playlistImage)); } playlist.LastUpdated = now; await DbContext.SaveChangesAsync(); diff --git a/Roadie.Api.Services/UserService.cs b/Roadie.Api.Services/UserService.cs index bdfa99b..5836c3e 100644 --- a/Roadie.Api.Services/UserService.cs +++ b/Roadie.Api.Services/UserService.cs @@ -503,7 +503,7 @@ namespace Roadie.Api.Services imageData = ImageHelper.ConvertToGifFormat(imageData); // Save unaltered user image - File.WriteAllBytes(user.PathToImage(Configuration), imageData); + File.WriteAllBytes(user.PathToImage(Configuration, true), imageData); } } diff --git a/Roadie.sln b/Roadie.sln index 04ed91b..6a14477 100644 --- a/Roadie.sln +++ b/Roadie.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{1BA7 Upgrade0005.sql = Upgrade0005.sql Upgrade0006.sql = Upgrade0006.sql Upgrade0007.sql = Upgrade0007.sql + Upgrade0008.sql = Upgrade0008.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/Upgrade0008.sql b/Upgrade0008.sql new file mode 100644 index 0000000..ff3a5b5 --- /dev/null +++ b/Upgrade0008.sql @@ -0,0 +1,2 @@ +-- New SortName columns +ALTER TABLE `genre` ADD sortName varchar(100) NULL; diff --git a/roadie.sql b/roadie.sql index 4025008..e8b9d38 100644 --- a/roadie.sql +++ b/roadie.sql @@ -337,6 +337,7 @@ CREATE TABLE `genre` ( `createdDate` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `sortName` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `normalizedName` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `thumbnail` blob DEFAULT NULL, `alternateNames` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, @@ -511,6 +512,7 @@ CREATE TABLE `release` ( `lastUpdated` datetime DEFAULT NULL, `isVirtual` tinyint(1) DEFAULT NULL, `title` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, + `sortTitle` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `alternateNames` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, `releaseDate` date DEFAULT NULL, `rating` smallint(6) NOT NULL, @@ -534,7 +536,6 @@ CREATE TABLE `release` ( `playedCount` int(11) DEFAULT NULL, `duration` int(11) DEFAULT NULL, `rank` decimal(9,2) DEFAULT NULL, - `sortTitle` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_releaseArtistAndTitle` (`artistId`,`title`), KEY `ix_release_roadieId` (`roadieId`),