This commit is contained in:
Steven Hildreth 2019-11-09 11:43:39 -06:00
parent f385c8f6fc
commit a370108b64
35 changed files with 688 additions and 169 deletions

View file

@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.4.2" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.4.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
</ItemGroup>

View file

@ -0,0 +1,141 @@
using Microsoft.Extensions.Configuration;
using Roadie.Library.Configuration;
using Roadie.Library.Utility;
using System.IO;
using Xunit;
namespace Roadie.Library.Tests
{
public class FolderPathHelperTests
{
private IRoadieSettings Configuration { get; }
public FolderPathHelperTests()
{
var settings = new RoadieSettings();
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("appsettings.test.json");
IConfiguration configuration = configurationBuilder.Build();
configuration.GetSection("RoadieSettings").Bind(settings);
this.Configuration = settings;
}
[Theory]
[InlineData("Bob Jones", 1, @"B\BO\Bob Jones [1]")]
[InlineData("Smith, Angie", 2, @"S\SM\Smith, Angie [2]")]
[InlineData("Blues Traveler", 89, @"B\BL\Blues Traveler [89]")]
[InlineData("Scott, Travis", 3, @"S\SC\Scott, Travis [3]")]
[InlineData("Straight, George", 783, @"S\ST\Straight, George [783]")]
[InlineData("Good! Times?", 213, @"G\GO\Good Times [213]")]
[InlineData("3rd Attempt, The", 56, @"0\3R\3rd Attempt, The [56]")]
[InlineData("3Oh!3", 589, @"0\3O\3oh3 [589]")]
[InlineData(" 3OH!3", 589, @"0\3O\3oh3 [589]")]
[InlineData("3oh!3 ", 589, @"0\3O\3oh3 [589]")]
[InlineData("B", 3423, @"B\B\B [3423]")]
[InlineData("BB", 3423, @"B\BB\Bb [3423]")]
[InlineData("Bad.Boys.Club", 433423, @"B\BA\Bad Boys Club [433423]")]
[InlineData("Bad_Boys_Club", 433423, @"B\BA\Bad Boys Club [433423]")]
[InlineData("Bad.Boys Club", 433423, @"B\BA\Bad Boys Club [433423]")]
[InlineData("Bad~Boys~Club", 433423, @"B\BA\Bad Boys Club [433423]")]
[InlineData("Bad-Boys=Club", 433423, @"B\BA\Bad Boys Club [433423]")]
[InlineData("Bad.Boys~Club_2", 433423, @"B\BA\Bad Boys Club 2 [433423]")]
[InlineData("13", 43234, @"0\13\13 [43234]")]
[InlineData("69 Eyes, The", 123, @"0\69\69 Eyes, The [123]")]
[InlineData("2 Unlimited", 234, @"0\2U\2 Unlimited [234]")]
[InlineData("A Flock Of Seagulls", 567, @"A\AF\A Flock Of Seagulls [567]")]
[InlineData("1975, The", 23, @"0\19\1975, The [23]")]
[InlineData("1914", 89, @"0\19\1914 [89]")]
[InlineData("13th Floor Elevators, The", 24, @"0\13\13th Floor Elevators, The [24]")]
[InlineData("13TH FLOOR ELEVATORS, THE", 24, @"0\13\13th Floor Elevators, The [24]")]
[InlineData("13th floor elevators, the", 24, @"0\13\13th Floor Elevators, The [24]")]
[InlineData("3 Doors Down", 11111111, @"0\3D\3 Doors Down [11111111]")]
[InlineData(" 3 Doors Down", 11111111, @"0\3D\3 Doors Down [11111111]")]
[InlineData("01234567890123456789012345678901234567890123456789", 123, @"0\01\01234567890123456789012345678901234567890123456789 [123]")]
[InlineData("012345678901234567890", 1111111111, @"0\01\012345678901234567890 [1111111111]")]
[InlineData("Royce Da 5'9\"", 876, @"R\RO\Royce Da 59 [876]")]
[InlineData("8", 434, @"0\8\8 [434]")]
[InlineData("Hay, Colin", 888, @"H\HA\Hay, Colin [888]")]
[InlineData("Ty Dolla $Ign", 6542, @"T\TY\Ty Dolla Sign [6542]")] //
[InlineData("Panic! At The Disco", 32, @"P\PA\Panic At The Disco [32]")]
[InlineData("Ke$ha", 98, @"K\KE\Kesha [98]")]
[InlineData("(Whispers)", 432, @"W\WH\Whispers [432]")]
[InlineData("Whiskers, John \"Catfish\"", 32342, @"W\WH\Whiskers, John Catfish [32342]")]
[InlineData("'68", 54321, @"0\68\68 [54321]")]
[InlineData("2Pac", 987654321, @"0\2P\2pac [987654321]")]
[InlineData("007 Superhero", 1, @"0\00\007 Superhero [1]")]
[InlineData("4 Non Blondes", 666, @"0\4N\4 Non Blondes [666]")]
[InlineData("Dream Theater", 99, @"D\DR\Dream Theater [99]")]
[InlineData("A.X.E. Project, The", 100, @"A\AX\A X E Project, The [100]")]
[InlineData("IRON MAIDEN", 9909, @"I\IR\Iron Maiden [9909]")]
[InlineData("iRoN MaiDEn", 9909, @"I\IR\Iron Maiden [9909]")]
[InlineData(" iRoN MaiDEn ", 9909, @"I\IR\Iron Maiden [9909]")]
[InlineData(" i R o N M a i D E n ", 9909, @"I\IR\I R O N M A I D E N [9909]")]
[InlineData("iron maiden", 9909, @"I\IR\Iron Maiden [9909]")]
[InlineData("Dreamside, The", 85, @"D\DR\Dreamside, The [85]")]
[InlineData("Stacy & Tom", 41, @"S\ST\Stacy And Tom [41]")]
[InlineData("A! Whack To The Head", 2111, @"A\AW\A Whack To The Head [2111]")]
[InlineData("!WHACKO!", 2112, @"W\WH\Whacko [2112]")]
[InlineData("Öysterhead", 7653, @"O\OY\Oysterhead [7653]")]
[InlineData("Öystërheäd", 7653, @"O\OY\Oysterhead [7653]")]
[InlineData("2Nu", 7653, @"0\2N\2nu [7653]")]
[InlineData("!SÖmëthing el$e", 73223, @"S\SO\Something Else [73223]")]
[InlineData("McDonald, Audra", 1232, @"M\MC\McDonald, Audra [1232]")]
[InlineData("McKnight, Brian", 1233, @"M\MC\McKnight, Brian [1233]")]
[InlineData("Womack, Lee Ann", 1234, @"W\WO\Womack, Lee Ann [1234]")]
[InlineData("At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non providenta", 123456789, @"A\AT\At Vero Eos Et Accusamus Et Iusto Odio Dignissimos Ducimus Qui Blanditiis Praesent [123456789]")]
public void GenerateArtistFolderNames(string input, int artistId, string shouldBe)
{
var artistFolder = FolderPathHelper.ArtistPath(Configuration, artistId, input);
var t = new DirectoryInfo(Path.Combine(Configuration.LibraryFolder, shouldBe));
Assert.Equal(t.FullName, artistFolder);
}
[Theory]
[InlineData("!SÖmëthing el$e", @"S\SO")]
[InlineData("A&M Records", @"A\AA")]
[InlineData("A!M Records", @"A\AM")]
[InlineData("Aware Records", @"A\AW")]
[InlineData("Jive", @"J\JI")]
[InlineData("Le Plan", @"L\LE")]
[InlineData("42 Records", @"0\42")]
public void GenerateLabelFolderNames(string input, string shouldBe)
{
var artistFolder = FolderPathHelper.LabelPath(Configuration, input);
var t = new DirectoryInfo(Path.Combine(Configuration.LabelImageFolder, 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")]
[InlineData("Original Hits (80S 12'') (72 Original Hits) Cd 5", "01/01/1980", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1980] Original Hits 80s 12 72 Original Hits Cd 5")]
[InlineData("Vices & Virtues", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Vices And Virtues")]
[InlineData("Pretty, Odd.", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Pretty, Odd.")]
[InlineData(" !A Pretty Odd Day", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] A Pretty Odd Day")]
[InlineData("A.Pretty~Odd=Day", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] A Pretty Odd Day")]
[InlineData("A", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] A")]
[InlineData("X", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] X")]
[InlineData("Done-Over", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Done Over")]
[InlineData("%", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Per")]
[InlineData("The Blues Brothers: Music From The Soundtrack", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] The Blues Brothers Music From The Soundtrack")]
[InlineData("14", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] 14")]
[InlineData(@"12\18\2103", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] 12182103")]
[InlineData(@"Insect Warfare / The Kill", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Insect Warfare The Kill")]
[InlineData("Live (1955)", "01/01/1973", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1973] Live 1955")]
[InlineData("We're Only In It For The Money", "01/01/1968", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1968] Were Only In It For The Money")]
[InlineData("Rock \"N Roll High School", "01/01/1968", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1968] Rock N Roll High School")]
[InlineData("1958-1959 Dr. Jekyll (With Cannonball Adderley)", "01/01/1968", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1968] 1958 1959 Dr Jekyll With Cannonball Adderley")]
[InlineData("Hello, Dolly!", "01/01/1968", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1968] Hello, Dolly")]
[InlineData("Deäth", "01/01/1981", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1981] Death")]
[InlineData("Sinatra's Swingin' Session!!! And More", "01/01/1981", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1981] Sinatras Swingin Session And More")]
[InlineData("01234567890123456789012345678901234567890123456789", "01/01/1974", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1974] 01234567890123456789012345678901234567890123456789")]
[InlineData("At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non providenta", "01/01/1975", @"I\IR\Iron Maiden [9909]", @"I\IR\Iron Maiden [9909]\[1975] At Vero Eos Et Accusamus Et Iusto Odio Dignissimos Ducimus Qui Blanditiis Praesentium Volupta")]
public void GenerateReleaseFolderNames(string input, string releaseDate, string artistFolder, string shouldBe)
{
var af = new DirectoryInfo(Path.Combine(Configuration.LibraryFolder, artistFolder));
var releaseFolder = FolderPathHelper.ReleasePath(af.FullName, input, SafeParser.ToDateTime(releaseDate).Value);
var t = new DirectoryInfo(Path.Combine(Configuration.LibraryFolder, shouldBe));
Assert.Equal(t.FullName, releaseFolder);
}
}
}

View file

@ -1,5 +1,6 @@
using Roadie.Library.Imaging;
using System;
using System.IO;
using Xunit;
namespace Roadie.Library.Tests
@ -9,6 +10,10 @@ namespace Roadie.Library.Tests
[Fact]
public void GenerateImageHash()
{
if(!Directory.Exists(@"C:\temp\image_tests"))
{
return;
}
var imageFilename = @"C:\temp\image_tests\1.jpg";
var secondImagFilename = @"C:\temp\image_tests\2.jpg";
var resizedFirstImageFilename = @"C:\temp\image_tests\1-resized.jpg";

View file

@ -413,7 +413,7 @@ namespace Roadie.Library.Tests
}
context.SaveChanges();
foreach (var release in context.Releases.Include(x => x.Artist).Where(x => x.Thumbnail != null).OrderBy(x => x.Title))
foreach (var release in context.Releases.Include(x => x.Artist).Where(x => x.Thumbnail != null).OrderBy(x => x.SortTitle ?? x.Title))
{
var artistFolder = release.Artist.ArtistFileFolder(settings);
var releaseFolder = release.ReleaseFileFolder(artistFolder);

View file

@ -24,8 +24,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View file

@ -244,7 +244,8 @@ namespace Roadie.Library.Tests
[InlineData("Colin Hay!", "colinhay")]
[InlineData("colinhay", "colinhay")]
[InlineData("COLINHAY", "colinhay")]
[InlineData("C.O!L&quot;I$N⌐HƒAY;", "colinhay")]
[InlineData("C.O!L&quot;IN⌐HƒAY;", "colinhay")]
[InlineData("$tacy $exp0t", "stacysexp0t")]
[InlineData(" Leslie &amp; Tom", "leslieandtom")]
[InlineData("<b>Leslie &amp; &#32;&#32; Tom</b>", "leslieandtom")]
[InlineData("Leslie;/&/;Tom", "leslieandtom")]

View file

@ -50,7 +50,7 @@ namespace Roadie.Library.Data
public string ArtistFileFolder(IRoadieSettings configuration)
{
return FolderPathHelper.ArtistPath(configuration, SortNameValue);
return FolderPathHelper.ArtistPath(configuration, Id, SortNameValue);
}
public override string ToString()

View file

@ -23,12 +23,23 @@ namespace Roadie.Library.Data
}
}
public string SortNameValue => string.IsNullOrEmpty(SortName) ? Name : SortName;
/// <summary>
/// Returns a full file path to the Label Image
/// </summary>
public string PathToImage(IRoadieSettings configuration)
{
return Path.Combine(configuration.LabelImageFolder, $"{ (SortName ?? Name).ToFileNameFriendly() } [{ Id }].jpg");
return Path.Combine(FolderPathHelper.LabelPath(configuration, SortNameValue), $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg");
}
/// <summary>
/// Returns a full file path to the Label Image
/// </summary>
[Obsolete("This is only here for migration will be removed in future release.")]
public string OldPathToImage(IRoadieSettings configuration)
{
return Path.Combine(configuration.LabelImageFolder, $"{ SortNameValue.ToFileNameFriendly() } [{ Id }].jpg");
}
public bool IsValid => !string.IsNullOrEmpty(Name);

View file

@ -91,6 +91,12 @@ namespace Roadie.Library.Data
[Required]
public string Title { get; set; }
[MaxLength(250)]
[Column("sortTitle")]
public string SortTitle { get; set; }
public string SortTitleValue => string.IsNullOrEmpty(SortTitle) ? Title : SortTitle;
[Column("trackCount")] public short TrackCount { get; set; }
[Column("urls", TypeName = "text")]

View file

@ -102,14 +102,11 @@ namespace Roadie.Library.Data
/// </summary>
/// <param name="artistFolder"></param>
/// <returns></returns>
public string ReleaseFileFolder(string artistFolder)
{
return FolderPathHelper.ReleasePath(artistFolder, Title, ReleaseDate.Value);
}
public string ReleaseFileFolder(string artistFolder) => FolderPathHelper.ReleasePath(artistFolder, SortTitleValue, ReleaseDate.Value);
public override string ToString()
{
return $"Id [{Id}], Status [{ Status }], LibraryStatus [{ LibraryStatus }], Title [{Title}], Release Date [{ReleaseYear}]";
return $"Id [{Id}], Status [{ Status }], LibraryStatus [{ LibraryStatus }], Title [{Title}], SortTitle [{ SortTitleValue }], Release Date [{ReleaseYear}]";
}
}
}

View file

@ -72,20 +72,6 @@ namespace Roadie.Library.Engines
artist.AlternateNames = artist.AlternateNames.AddToDelimitedList(new[] { artist.Name.ToAlphanumericName() });
artist.Genres = null;
artist.Images = null;
if (artist.Thumbnail == null && ArtistImages != null)
{
// Set the thumbnail to the first image
var firstImageWithNotNullBytes = ArtistImages.Where(x => x.Bytes != null).FirstOrDefault();
if (firstImageWithNotNullBytes != null)
{
artist.Thumbnail = firstImageWithNotNullBytes.Bytes;
if (artist.Thumbnail != null)
{
artist.Thumbnail = ImageHelper.ResizeToThumbnail(artist.Thumbnail, Configuration);
}
}
}
if (!artist.IsValid)
{
return new OperationResult<Artist>
@ -93,10 +79,6 @@ namespace Roadie.Library.Engines
Errors = new Exception[1] { new Exception("Artist is Invalid") }
};
}
if (artist.Thumbnail != null)
{
artist.Thumbnail = ImageHelper.ResizeToThumbnail(artist.Thumbnail, Configuration);
}
var addArtistResult = DbContext.Artists.Add(artist);
var inserted = 0;
inserted = await DbContext.SaveChangesAsync();
@ -378,9 +360,6 @@ namespace Roadie.Library.Engines
BeginDate = i.BeginDate,
Name = result.Name ?? i.ArtistName,
SortName = result.SortName ?? i.ArtistSortName,
Thumbnail = i.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(i.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? i.ArtistType
});
}
@ -443,9 +422,6 @@ namespace Roadie.Library.Engines
BeginDate = mb.BeginDate,
Name = result.Name ?? mb.ArtistName,
SortName = result.SortName ?? mb.ArtistSortName,
Thumbnail = mb.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(mb.ArtistThumbnailUrl)
: null,
ArtistType = mb.ArtistType
});
}
@ -488,9 +464,6 @@ namespace Roadie.Library.Engines
BeginDate = l.BeginDate,
Name = result.Name ?? l.ArtistName,
SortName = result.SortName ?? l.ArtistSortName,
Thumbnail = l.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(l.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? l.ArtistType
});
}
@ -530,9 +503,6 @@ namespace Roadie.Library.Engines
BeginDate = s.BeginDate,
Name = result.Name ?? s.ArtistName,
SortName = result.SortName ?? s.ArtistSortName,
Thumbnail = s.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(s.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? s.ArtistType
});
}
@ -573,9 +543,6 @@ namespace Roadie.Library.Engines
DiscogsId = d.DiscogsId,
Name = result.Name ?? d.ArtistName,
RealName = result.RealName ?? d.ArtistRealName,
Thumbnail = d.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(d.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? d.ArtistType
});
}

View file

@ -4,7 +4,6 @@ using Roadie.Library.Configuration;
using Roadie.Library.Data;
using Roadie.Library.Encoding;
using Roadie.Library.Extensions;
using Roadie.Library.Imaging;
using Roadie.Library.SearchEngines.MetaData;
using Roadie.Library.Utility;
using System;
@ -35,10 +34,6 @@ namespace Roadie.Library.Engines
{
var now = DateTime.UtcNow;
label.AlternateNames = label.AlternateNames.AddToDelimitedList(new[] { label.Name.ToAlphanumericName() });
if (label.Thumbnail != null)
{
label.Thumbnail = ImageHelper.ResizeToThumbnail(label.Thumbnail, Configuration);
}
if (!label.IsValid)
{
return new OperationResult<Label>
@ -182,8 +177,7 @@ namespace Roadie.Library.Engines
{
Profile = HttpEncoder.HtmlEncode(d.Profile),
DiscogsId = d.DiscogsId,
Name = result.Name ?? d.LabelName.ToTitleCase(),
Thumbnail = d.LabelImageUrl != null ? WebHelper.BytesForImageUrl(d.LabelImageUrl) : null
Name = result.Name ?? d.LabelName.ToTitleCase()
});
}

View file

@ -845,8 +845,6 @@ namespace Roadie.Library.Engines
{
var image = metaData.Images.FirstOrDefault(x => x.Type == AudioMetaDataImageType.FrontCover);
if (image == null) image = metaData.Images.FirstOrDefault();
// If there is an image on the metadata file itself then that over-rides metadata providers.
if (image != null) result.Thumbnail = image.Data;
}
if (!string.IsNullOrEmpty(artistFolder))
@ -873,13 +871,6 @@ namespace Roadie.Library.Engines
{
coverFileName = cover.First().FullName;
}
if (!string.IsNullOrEmpty(coverFileName))
{
// Read image and convert to jpeg
result.Thumbnail = File.ReadAllBytes(coverFileName);
Logger.LogDebug("PerformMetaDataProvidersReleaseSearch: Using Release Cover File [{0}]", coverFileName);
}
}
}
sw.Stop();

View file

@ -10,6 +10,8 @@
Wishlist = 5,
AdminRemoved = 6,
Expired = 7,
ReadyToMigrate = 8,
Migrated = 9,
Deleted = 99
}
}

View file

@ -226,15 +226,22 @@ namespace Roadie.Library.Extensions
return input;
}
public static string ToAlphanumericName(this string input)
public static string ToAlphanumericName(this string input, bool stripSpaces = true, bool stripCommas = true)
{
if (string.IsNullOrEmpty(input)) return input;
if (string.IsNullOrEmpty(input))
{
return input;
}
input = input.ToLower()
.Replace("$", "s")
.Replace("%", "per");
input = WebUtility.HtmlDecode(input);
input = input.ScrubHtml().ToLower().Trim().Replace("&", "and");
input = input.ScrubHtml().ToLower()
.Replace("&", "and");
var arr = input.ToCharArray();
arr = Array.FindAll(arr, c => char.IsLetterOrDigit(c));
arr = Array.FindAll(arr, c => (c == ',' && !stripCommas) || (char.IsWhiteSpace(c) && !stripSpaces) || char.IsLetterOrDigit(c));
input = new string(arr).RemoveDiacritics().RemoveUnicodeAccents().Translit();
input = Regex.Replace(input, @"[^A-Za-z0-9]+", "");
input = Regex.Replace(input, $"[^A-Za-z0-9{ ( !stripSpaces ? @"\s" : "") }{ (!stripCommas ? "," : "")}]+", "");
return input;
}
@ -252,7 +259,11 @@ namespace Roadie.Library.Extensions
public static string ToFolderNameFriendly(this string input)
{
if (string.IsNullOrEmpty(input)) return null;
if (string.IsNullOrEmpty(input))
{
return null;
}
input = input.Replace("$", "s");
return Regex.Replace(PathSanitizer.SanitizeFilename(input, ' '), @"\s+", " ").Trim().TrimEnd('.');
}

View file

@ -322,16 +322,22 @@ namespace Roadie.Library.FilePlugins
int? submissionId)
{
var artist = await ArtistLookupEngine.GetByName(metaData, !doJustInfo);
if (!artist.IsSuccess) return null;
if (!artist.IsSuccess)
{
return null;
}
_artistId = artist.Data.RoadieId;
var release =
await ReleaseLookupEngine.GetByName(artist.Data, metaData, !doJustInfo, submissionId: submissionId);
if (!release.IsSuccess) return null;
var release = await ReleaseLookupEngine.GetByName(artist.Data, metaData, !doJustInfo, submissionId: submissionId);
if (!release.IsSuccess)
{
return null;
}
_releaseId = release.Data.RoadieId;
release.Data.ReleaseDate = SafeParser.ToDateTime(release.Data.ReleaseYear ?? metaData.Year);
if (release.Data.ReleaseYear.HasValue && release.Data.ReleaseYear != metaData.Year)
Logger.LogWarning(
$"Found Release `{release.Data}` has different Release Year than MetaData Year `{metaData}`");
{
Logger.LogWarning($"Found Release `{release.Data}` has different Release Year than MetaData Year `{metaData}`");
}
return release.Data.ReleaseFileFolder(artistFolder);
}
}

View file

@ -41,7 +41,7 @@ namespace Roadie.Library.Imaging
IImageFormat imageFormat = null;
using (var image = SixLabors.ImageSharp.Image.Load(imageBytes, out imageFormat))
{
image.Save(outStream, ImageFormats.Jpeg);
image.SaveAsJpeg(outStream);
}
return outStream.ToArray();
}
@ -65,7 +65,7 @@ namespace Roadie.Library.Imaging
IImageFormat imageFormat = null;
using (var image = SixLabors.ImageSharp.Image.Load(imageBytes, out imageFormat))
{
image.Save(outStream, ImageFormats.Gif);
image.SaveAsGif(outStream);
}
return outStream.ToArray();
}
@ -138,6 +138,10 @@ namespace Roadie.Library.Imaging
public static string[] GetFiles(string path, string[] patterns = null, SearchOption options = SearchOption.TopDirectoryOnly)
{
if(!Directory.Exists(path))
{
return new string[0];
}
if (patterns == null || patterns.Length == 0)
{
return Directory.GetFiles(path, "*", options);

View file

@ -477,7 +477,7 @@ namespace Roadie.Library.Inspect
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"❌ Deleting Empty folders in [{delEmptyFolderIn.FullName}]");
Console.ResetColor();
FolderPathHelper.DeleteEmptyDirs(directoryToInspect, false);
FolderPathHelper.DeleteEmptyFolders(delEmptyFolderIn);
}
else
{

View file

@ -77,6 +77,8 @@ namespace Roadie.Library.Models.Releases
public Image Thumbnail { get; set; }
[MaxLength(250)] [Required] public string Title { get; set; }
[MaxLength(250)] public string SortTitle { get; set; }
public short TrackCount { get; set; }
public UserRelease UserRating { get; set; }

View file

@ -9,9 +9,9 @@
<ItemGroup>
<PackageReference Include="AutoCompare.Core" Version="1.0.0" />
<PackageReference Include="CsvHelper" Version="12.1.3" />
<PackageReference Include="CsvHelper" Version="12.2.1" />
<PackageReference Include="EFCore.BulkExtensions" Version="3.0.0" />
<PackageReference Include="FluentFTP" Version="28.0.0" />
<PackageReference Include="FluentFTP" Version="28.0.1" />
<PackageReference Include="Hashids.net" Version="1.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
<PackageReference Include="IdSharp.Common" Version="1.0.1" />
@ -27,14 +27,14 @@
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.0" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" />
<PackageReference Include="MimeMapping" Version="1.0.1.17" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NodaTime" Version="2.4.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.0.0-rc1.final" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.0.0-rc3.final" />
<PackageReference Include="RestSharp" Version="106.6.10" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0006" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0005" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0008" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
<PackageReference Include="System.Drawing.Common" Version="4.6.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" />
<PackageReference Include="System.Runtime.Caching" Version="4.6.0" />

View file

@ -3,9 +3,11 @@ using Roadie.Library.Data;
using Roadie.Library.Extensions;
using Roadie.Library.MetaData.Audio;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace Roadie.Library.Utility
{
@ -14,11 +16,114 @@ namespace Roadie.Library.Utility
/// </summary>
public static class FolderPathHelper
{
public static int MaximumLibraryFolderNameLength = 44;
public static int MaximumArtistFolderNameLength = 100;
public static int MaximumReleaseFolderNameLength = 100;
public static int MaximumLabelFolderNameLength = 100;
public static int MaximumTrackFileNameLength = 500;
public static IEnumerable<string> FolderSpaceReplacements = new List<string> { ".", "~", "_", "=", "-" };
/// <summary>
/// Full path to Artist folder
/// </summary>
/// <param name="artistSortName">Sort name of Artist to use for folder name</param>
public static string ArtistPath(IRoadieSettings configuration, string artistSortName)
public static string ArtistPath(IRoadieSettings configuration, int artistId, string artistSortName)
{
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistSortName), "Invalid Artist Sort Name");
SimpleContract.Requires<ArgumentException>(configuration.LibraryFolder.Length < MaximumLibraryFolderNameLength, $"Library Folder maximum length is [{ MaximumLibraryFolderNameLength }]");
var asn = new StringBuilder(artistSortName);
foreach (var stringReplacement in FolderSpaceReplacements)
{
if (!asn.Equals(stringReplacement))
{
asn.Replace(stringReplacement, " ");
}
}
var artistFolder = asn.ToString().ToAlphanumericName(false, false).ToFolderNameFriendly().ToTitleCase(false);
if (string.IsNullOrEmpty(artistFolder))
{
throw new Exception($"ArtistFolder [{ artistFolder }] is invalid. ArtistId [{ artistId }], ArtistSortName [{ artistSortName }].");
}
var afUpper = artistFolder.ToUpper();
var fnSubPart1 = afUpper.ToUpper().ToCharArray().Take(1).First();
if (!char.IsLetterOrDigit(fnSubPart1))
{
fnSubPart1 = '#';
}
else if (char.IsNumber(fnSubPart1))
{
fnSubPart1 = '0';
}
var fnSubPart2 = afUpper.Length > 2 ? afUpper.Substring(0, 2) : afUpper;
if (fnSubPart2.EndsWith(" "))
{
var pos = 1;
while (fnSubPart2.EndsWith(" "))
{
pos++;
fnSubPart2 = fnSubPart2.Substring(0, 1) + afUpper.Substring(pos, 1);
}
}
var fnSubPart = Path.Combine(fnSubPart1.ToString(), fnSubPart2);
var fnIdPart = $" [{ artistId }]";
var maxFnLength = (MaximumArtistFolderNameLength - (fnSubPart.Length + fnIdPart.Length)) - 2;
if (artistFolder.Length > maxFnLength)
{
artistFolder = artistFolder.Substring(0, maxFnLength);
}
artistFolder = Path.Combine(fnSubPart, $"{ artistFolder }{ fnIdPart }");
var directoryInfo = new DirectoryInfo(Path.Combine(configuration.LibraryFolder, artistFolder));
return directoryInfo.FullName;
}
public static string LabelPath(IRoadieSettings configuration, string labelSortName)
{
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(labelSortName), "Invalid Label Sort Name");
SimpleContract.Requires<ArgumentException>(configuration.LibraryFolder.Length < MaximumLibraryFolderNameLength, $"Library Folder maximum length is [{ MaximumLibraryFolderNameLength }]");
var lsn = new StringBuilder(labelSortName);
foreach (var stringReplacement in FolderSpaceReplacements)
{
if (!lsn.Equals(stringReplacement))
{
lsn.Replace(stringReplacement, " ");
}
}
var labelFolder = lsn.ToString().ToAlphanumericName(false, false).ToFolderNameFriendly().ToTitleCase(false);
if (string.IsNullOrEmpty(labelFolder))
{
throw new Exception($"LabelFolder [{ labelFolder }] is invalid. LabelSortName [{ labelSortName }].");
}
var lfUpper = labelFolder.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.LabelImageFolder, 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)
{
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistSortName),"Invalid Artist Sort Name");
@ -27,33 +132,53 @@ namespace Roadie.Library.Utility
return directoryInfo.FullName;
}
public static void DeleteEmptyDirs(string dir, bool deleteDirIfEmpty = true)
/// <summary>
/// Full path to Release folder using given full Artist folder
/// </summary>
/// <param name="artistFolder">Full path to Artist folder</param>
/// <param name="releaseTitle">Title of Release</param>
/// <param name="releaseDate">Date of Release</param>
public static string ReleasePath(string artistFolder, string releaseTitle, DateTime releaseDate)
{
if (string.IsNullOrEmpty(dir))
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistFolder), "Invalid Artist Folder");
SimpleContract.Requires<ArgumentException>(artistFolder.Length < MaximumArtistFolderNameLength, $"Artist Folder is longer than maximum allowed [{ MaximumArtistFolderNameLength }]");
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(releaseTitle), "Invalid Release Title");
SimpleContract.Requires<ArgumentException>(releaseDate != DateTime.MinValue, "Invalid Release Date");
var rt = new StringBuilder(releaseTitle);
foreach (var stringReplacement in FolderSpaceReplacements)
{
throw new ArgumentException("Starting directory is a null reference or an empty string", "dir");
}
try
{
foreach (var d in Directory.EnumerateDirectories(dir)) DeleteEmptyDirs(d);
var entries = Directory.EnumerateFileSystemEntries(dir);
if (!entries.Any() && deleteDirIfEmpty)
if(!rt.Equals(stringReplacement))
{
try
{
Directory.Delete(dir);
}
catch (UnauthorizedAccessException)
{
}
catch (DirectoryNotFoundException)
{
}
rt.Replace(stringReplacement, " ");
}
}
catch (UnauthorizedAccessException)
var releasePathTitle = rt.ToString().ToAlphanumericName(false, false).ToFolderNameFriendly().ToTitleCase(false);
if(string.IsNullOrEmpty(releasePathTitle))
{
throw new Exception($"ReleaseTitle [{ releaseTitle }] is invalid. ArtistFolder [{ artistFolder }].");
}
var maxFnLength = MaximumReleaseFolderNameLength - 7;
if (releasePathTitle.Length > maxFnLength)
{
releasePathTitle = releasePathTitle.Substring(0, maxFnLength);
}
var releasePath = $"[{ releaseDate.ToString("yyyy")}] {releasePathTitle}";
var directoryInfo = new DirectoryInfo(Path.Combine(artistFolder, releasePath));
return directoryInfo.FullName;
}
[Obsolete("This is only here for migration will be removed in future release.")]
public static string ReleasePathOld(string artistFolder, string releaseTitle, DateTime releaseDate)
{
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistFolder), "Invalid Artist Folder");
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(releaseTitle), "Invalid Release Title");
SimpleContract.Requires<ArgumentException>(releaseDate != DateTime.MinValue, "Invalid Release Date");
var directoryInfo = new DirectoryInfo(Path.Combine(artistFolder, string.Format("{1}{0}", releaseTitle.ToTitleCase(false).ToFolderNameFriendly(), string.Format("[{0}] ", releaseDate.ToString("yyyy")))));
return directoryInfo.FullName;
}
/// <summary>
@ -67,6 +192,7 @@ namespace Roadie.Library.Utility
{
return true;
}
var result = false;
try
{
foreach (var folder in processingFolder.GetDirectories("*.*", SearchOption.AllDirectories))
@ -78,27 +204,34 @@ namespace Roadie.Library.Utility
if (!folder.GetFiles("*.*", SearchOption.AllDirectories).Any())
{
folder.Delete(true);
Trace.WriteLine(string.Format("Deleting Empty Folder [{0}]", folder.FullName), "Debug");
Trace.WriteLine($"Deleting Empty Folder [{folder.FullName}]", "Debug");
result = true;
}
}
}
catch (UnauthorizedAccessException)
{
result = false;
Trace.WriteLine($"UnauthorizedAccessException Deleting Empty Folder [{folder.FullName}]", "Debug");
}
catch (DirectoryNotFoundException)
{
}
catch (Exception)
{
throw;
result = false;
Trace.WriteLine($"DirectoryNotFoundException Deleting Empty Folder [{folder.FullName}]", "Debug");
}
}
}
catch(DirectoryNotFoundException)
catch (UnauthorizedAccessException)
{
result = false;
Trace.WriteLine($"UnauthorizedAccessException Deleting Empty Folder [{processingFolder.FullName}]", "Debug");
}
catch (Exception)
catch (DirectoryNotFoundException)
{
throw;
result = false;
Trace.WriteLine($"DirectoryNotFoundException Deleting Empty Folder [{processingFolder.FullName}]", "Debug");
}
return true;
return result;
}
/// <summary>
@ -143,22 +276,6 @@ namespace Roadie.Library.Utility
return directoryInfo.FullName;
}
/// <summary>
/// Full path to Release folder using given full Artist folder
/// </summary>
/// <param name="artistFolder">Full path to Artist folder</param>
/// <param name="releaseTitle">Title of Release</param>
/// <param name="releaseDate">Date of Release</param>
public static string ReleasePath(string artistFolder, string releaseTitle, DateTime releaseDate)
{
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistFolder), "Invalid Artist Folder");
SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(releaseTitle), "Invalid Release Title");
SimpleContract.Requires<ArgumentException>(releaseDate != DateTime.MinValue, "Invalid Release Date");
var directoryInfo = new DirectoryInfo(Path.Combine(artistFolder, string.Format("{1}{0}", releaseTitle.ToTitleCase(false).ToFolderNameFriendly(), string.Format("[{0}] ", releaseDate.ToString("yyyy")))));
return directoryInfo.FullName;
}
/// <summary>
/// Returns the FileName for given Track details, this is not the Full Path (FQDN) only the FileName
/// </summary>
@ -202,9 +319,12 @@ namespace Roadie.Library.Utility
.ToTitleCase(false);
var trackPathReplace = configuration.TrackPathReplace;
if (trackPathReplace != null)
{
foreach (var kp in trackPathReplace)
{
fileNameFromTitle = fileNameFromTitle.Replace(kp.Key, kp.Value);
}
}
return string.Format("{0}{1} {2}.{3}", disc, track, fileNameFromTitle, fileExtension.ToLower());
}
@ -215,7 +335,7 @@ namespace Roadie.Library.Utility
/// <param name="artistFolder">Optional ArtistFolder default is to get from MetaData artist</param>
public static string TrackFullPath(IRoadieSettings configuration, AudioMetaData metaData, string artistFolder = null, string releaseFolder = null)
{
return TrackFullPath(configuration, metaData.Artist, metaData.Release,
return TrackFullPath(configuration, 0, metaData.Artist, metaData.Release,
SafeParser.ToDateTime(metaData.Year).Value,
metaData.Title, metaData.TrackNumber ?? 0, metaData.Disc ?? 0,
metaData.TotalTrackNumbers ?? 0,
@ -231,7 +351,7 @@ namespace Roadie.Library.Utility
/// <param name="track">Track</param>
/// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param>
/// <returns></returns>
public static string TrackFullPath(IRoadieSettings configuration, Artist artist, Release release, Track track) => TrackFullPath(configuration, artist.SortNameValue, release.Title, release.ReleaseDate.Value, track.Title, track.TrackNumber);
public static string TrackFullPath(IRoadieSettings configuration, Artist artist, Release release, Track track) => TrackFullPath(configuration, artist.Id, artist.SortNameValue, release.SortTitleValue, release.ReleaseDate.Value, track.Title, track.TrackNumber);
/// <summary>
/// Return the full path (FQDN) for given Track details
@ -244,20 +364,16 @@ namespace Roadie.Library.Utility
/// <param name="discNumber">Optional disc number defaults to 0</param>
/// <param name="totalTrackNumber">Optional Total Tracks defaults to TrackNumber</param>
/// <param name="fileExtension">Optional File Extension defaults to mp3</param>
public static string TrackFullPath(IRoadieSettings configuration, string artistSortName, string releaseTitle,
public static string TrackFullPath(IRoadieSettings configuration, int artistId, string artistSortName, string releaseTitle,
DateTime releaseDate, string trackTitle, short trackNumber, int? discNumber = null, int? totalTrackNumber = null, string fileExtension = "mp3",
string artistFolder = null, string releaseFolder = null)
{
artistFolder = artistFolder ?? ArtistPath(configuration, artistSortName);
releaseFolder = releaseFolder ?? ReleasePath(artistFolder, releaseTitle, releaseDate);
artistFolder ??= ArtistPath(configuration, artistId, artistSortName);
releaseFolder ??= ReleasePath(artistFolder, releaseTitle, releaseDate);
var trackFileName = TrackFileName(configuration, trackTitle, trackNumber, discNumber, totalTrackNumber, fileExtension);
var result = Path.Combine(artistFolder, releaseFolder, trackFileName);
var resultInfo = new DirectoryInfo(result);
Trace.WriteLine(string.Format(
"TrackPath [{0}] For ArtistName [{1}], ReleaseTitle [{2}], ReleaseDate [{3}], ReleaseYear [{4}], TrackNumber [{5}]",
resultInfo.FullName, artistSortName, releaseTitle, releaseDate.ToString("s"),
releaseDate.ToString("yyyy"), trackNumber));
return resultInfo.FullName;
}
@ -268,11 +384,15 @@ namespace Roadie.Library.Utility
/// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param>
/// <param name="artistFolder">Optional ArtistFolder default is to get from MetaData artist</param>
/// ///
public static string TrackPath(IRoadieSettings configuration, AudioMetaData metaData,
string destinationFolder = null, string artistFolder = null)
public static string TrackPath(IRoadieSettings configuration, AudioMetaData metaData, string destinationFolder = null, string artistFolder = null)
{
var fileInfo = new FileInfo(TrackFullPath(configuration, metaData, destinationFolder, artistFolder));
return fileInfo.Directory.Name;
var tf = fileInfo.Directory.Parent.FullName.Replace(new DirectoryInfo(configuration.LibraryFolder).FullName, "");
if (tf.StartsWith(Path.DirectorySeparatorChar))
{
tf = tf.RemoveFirst(Path.DirectorySeparatorChar.ToString());
}
return Path.Combine(tf, fileInfo.Directory.Name);
}
/// <summary>
@ -283,8 +403,14 @@ namespace Roadie.Library.Utility
/// <param name="track">Track</param>
public static string TrackPath(IRoadieSettings configuration, Artist artist, Release release, Track track)
{
var fileInfo = new FileInfo(TrackFullPath(configuration, artist.SortNameValue, release.Title, release.ReleaseDate.Value, track.Title, track.TrackNumber));
return fileInfo.Directory.Name;
var fileInfo = new FileInfo(TrackFullPath(configuration, artist.Id, artist.SortNameValue, release.SortTitleValue, release.ReleaseDate.Value, track.Title, track.TrackNumber));
var tf = fileInfo.Directory.Parent.FullName.Replace(new DirectoryInfo(configuration.LibraryFolder).FullName, "");
if (tf.StartsWith(Path.DirectorySeparatorChar))
{
tf = tf.RemoveFirst(Path.DirectorySeparatorChar.ToString());
}
var result = Path.Combine(tf, fileInfo.Directory.Name);
return result;
}
/// <summary>
@ -297,11 +423,17 @@ namespace Roadie.Library.Utility
/// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param>
/// <param name="discNumber">Optional disc number defaults to 0</param>
/// <param name="totalTrackNumber">Optional Total Tracks defaults to TrackNumber</param>
public static string TrackPath(IRoadieSettings configuration, string artistSortName, string releaseTitle,
public static string TrackPath(IRoadieSettings configuration, int artistId, string artistSortName, string releaseTitle,
DateTime releaseDate, string trackTitle, short trackNumber, int? discNumber = null, int? totalTrackNumber = null)
{
var fileInfo = new FileInfo(TrackFullPath(configuration, artistSortName, releaseTitle, releaseDate, trackTitle, trackNumber, discNumber, totalTrackNumber));
return fileInfo.Directory.Name;
var fileInfo = new FileInfo(TrackFullPath(configuration, artistId, artistSortName, releaseTitle, releaseDate, trackTitle, trackNumber, discNumber, totalTrackNumber));
var tf = fileInfo.Directory.Parent.FullName.Replace(new DirectoryInfo(configuration.LibraryFolder).FullName, "");
if (tf.StartsWith(Path.DirectorySeparatorChar))
{
tf = tf.RemoveFirst(Path.DirectorySeparatorChar.ToString());
}
var result = Path.Combine(tf, fileInfo.Directory.Name);
return result;
}
}
}

View file

@ -27,17 +27,25 @@ namespace Roadie.Library.Utility
c.AddRange(Path.GetInvalidFileNameChars());
// Some Roadie instances run in Linux to Windows SMB clients via Samba this helps with Windows clients and invalid characters in Windows
var badWindowsFileAndFoldercharacters = new List<char> { '\\', '/', ':', '*', '?', '\'', '<', '>', '|', '*' };
var badWindowsFileAndFoldercharacters = new List<char> { '\\', '"', '/', ':', '*', '$', '?', '\'', '<', '>', '|', '*' };
foreach (var badWindowsFilecharacter in badWindowsFileAndFoldercharacters)
{
if (!c.Contains(badWindowsFilecharacter))
{
c.Add(badWindowsFilecharacter);
}
}
invalidFilenameChars = c.ToArray();
var f = new List<char>();
f.AddRange(Path.GetInvalidPathChars());
foreach (var badWindowsFilecharacter in badWindowsFileAndFoldercharacters)
{
if (!f.Contains(badWindowsFilecharacter))
{
f.Add(badWindowsFilecharacter);
}
}
invalidFilenameChars = c.ToArray();
invalidPathChars = f.ToArray();
@ -77,17 +85,25 @@ namespace Roadie.Library.Utility
private static string Sanitize(string input, char[] invalidChars, char errorChar)
{
// null always sanitizes to null
if (input == null) return null;
if (input == null)
{
return null;
}
var result = new StringBuilder();
foreach (var characterToTest in input)
{
// we binary search for the character in the invalid set. This should be lightning fast.
if (Array.BinarySearch(invalidChars, characterToTest) >= 0)
{
// we found the character in the array of
result.Append(errorChar);
}
else
{
// the character was not found in invalid, so it is valid.
result.Append(characterToTest);
}
}
// we're done.
return result.ToString();
}

View file

@ -60,7 +60,7 @@ namespace Roadie.Api.Services
FileDirectoryProcessorService = fileDirectoryProcessorService;
}
public async Task<OperationResult<bool>> DeleteArtist(ApplicationUser user, Guid artistId)
public async Task<OperationResult<bool>> DeleteArtist(ApplicationUser user, Guid artistId, bool deleteFolder)
{
var sw = new Stopwatch();
sw.Start();
@ -74,7 +74,7 @@ namespace Roadie.Api.Services
try
{
var result = await ArtistService.Delete(user, artist);
var result = await ArtistService.Delete(user, artist, deleteFolder);
if (!result.IsSuccess)
{
return new OperationResult<bool>
@ -1012,6 +1012,208 @@ namespace Roadie.Api.Services
};
}
/// <summary>
/// Migrate Storage from old folder structure to new folder structure.
/// </summary>
public async Task<OperationResult<bool>> MigrateStorage(ApplicationUser user, bool deleteEmptyFolders = true)
{
var sw = new Stopwatch();
sw.Start();
var errors = new List<Exception>();
var now = DateTime.UtcNow;
var artistsMigrated = 0;
foreach (var artist in DbContext.Artists.Where(x => x.Status == Statuses.ReadyToMigrate).ToArray())
{
var oldArtistPath = FolderPathHelper.ArtistPathOld(Configuration, artist.SortNameValue);
var artistpath = FolderPathHelper.ArtistPath(Configuration, artist.Id, artist.SortNameValue);
if (Directory.Exists(oldArtistPath))
{
var artistInfoFile = new FileInfo(Path.Combine(oldArtistPath, "roadie.artist.json"));
if (artistInfoFile.Exists)
{
artistInfoFile.MoveTo(Path.Combine(artistpath, "roadie.artist.json"));
}
}
var createdDirectory = false;
var filesMoved = 0;
var artistImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(oldArtistPath), ImageType.Artist);
var artistSecondaryImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(oldArtistPath), ImageType.ArtistSecondary).ToList();
if (artistImages.Any())
{
if (!Directory.Exists(artistpath))
{
Directory.CreateDirectory(artistpath);
createdDirectory = true;
}
var artistToMergeIntoPrimaryImage = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(artistpath), ImageType.Artist).FirstOrDefault();
if (artistToMergeIntoPrimaryImage != null)
{
artistSecondaryImages.Add(artistImages.First());
}
else
{
var artistImageFilename = Path.Combine(artistpath, ImageHelper.ArtistImageFilename);
artistImages.First().MoveTo(artistImageFilename, true);
filesMoved++;
}
}
if (artistSecondaryImages.Any())
{
if (!Directory.Exists(artistpath))
{
Directory.CreateDirectory(artistpath);
createdDirectory = true;
}
var looper = 0;
foreach (var artistSecondaryImage in artistSecondaryImages)
{
var artistImageFilename = Path.Combine(artistpath, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00")));
while (File.Exists(artistImageFilename))
{
looper++;
artistImageFilename = Path.Combine(artistpath, string.Format(ImageHelper.ArtistSecondaryImageFilename, looper.ToString("00")));
}
artistSecondaryImage.MoveTo(artistImageFilename, true);
filesMoved++;
}
}
artist.Status = Statuses.Migrated;
artist.LastUpdated = now;
await DbContext.SaveChangesAsync();
Logger.LogInformation($"Migrated Artist Storage `{ artist}` From [{ oldArtistPath }] => [{ artistpath }]");
artistsMigrated++;
}
Logger.LogInformation($"Artist Migration Complete. Migrated [{ artistsMigrated }] Artists.");
var labelsMigrated = 0;
foreach (var label in DbContext.Labels.Where(x => x.Status == Statuses.ReadyToMigrate).ToArray())
{
var oldLabelImageFileName = label.OldPathToImage(Configuration);
var labelImageFileName = label.PathToImage(Configuration);
if(File.Exists(oldLabelImageFileName))
{
var labelFileInfo = new FileInfo(labelImageFileName);
if(!labelFileInfo.Directory.Exists)
{
Directory.CreateDirectory(labelFileInfo.Directory.FullName);
}
File.Move(oldLabelImageFileName, labelImageFileName, true);
label.Status = Statuses.Migrated;
label.LastUpdated = now;
await DbContext.SaveChangesAsync();
Logger.LogInformation($"Migrated Label Storage `{ label}` From [{ oldLabelImageFileName }] => [{ labelImageFileName }]");
labelsMigrated++;
}
}
Logger.LogInformation($"Label Migration Complete. Migrated [{ labelsMigrated }] Labels.");
var releases = DbContext.Releases
.Include(x => x.Artist)
.Include(x => x.Medias)
.Where(x => x.Status == Statuses.ReadyToMigrate)
.ToArray();
var releasesMigrated = 0;
foreach (var release in releases)
{
var oldArtistPath = FolderPathHelper.ArtistPathOld(Configuration, release.Artist.SortNameValue);
var oldReleasePath = FolderPathHelper.ReleasePathOld(oldArtistPath, release.SortTitleValue, release.ReleaseDate.Value);
var artistpath = FolderPathHelper.ArtistPath(Configuration, release.Artist.Id, release.Artist.SortNameValue);
var releasePath = FolderPathHelper.ReleasePath(artistpath, release.SortTitleValue, release.ReleaseDate.Value);
if (!Directory.Exists(artistpath))
{
Directory.CreateDirectory(artistpath);
}
if (!Directory.Exists(releasePath))
{
Directory.CreateDirectory(releasePath);
}
var releaseTracks = (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
where r.Id == release.Id
where t.FileName != null
select t).ToArray();
foreach(var releaseTrack in releaseTracks)
{
var oldTrackFileName = Path.Combine(oldReleasePath, releaseTrack.FileName);
var newTrackFileName = Path.Combine(releasePath, releaseTrack.FileName.ToFileNameFriendly());
if(File.Exists(oldTrackFileName))
{
File.Move(oldTrackFileName, newTrackFileName, true);
releaseTrack.FilePath = FolderPathHelper.TrackPath(Configuration, release.Artist, release, releaseTrack);
releaseTrack.FileName = releaseTrack.FileName.ToFileNameFriendly();
releaseTrack.LastUpdated = now;
}
else
{
Logger.LogWarning($"Migration: Track `{ releaseTrack }` Track File [{ oldTrackFileName }] Not Found");
}
}
var releaseInfoFile = new FileInfo(Path.Combine(oldReleasePath, "roadie.albuminfo.json"));
if(releaseInfoFile.Exists)
{
releaseInfoFile.MoveTo(Path.Combine(releasePath, "roadie.releaseinfo.json"));
}
var releaseToMergeImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(oldReleasePath), ImageType.Release);
var releaseToMergeSecondaryImages = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(oldReleasePath), ImageType.ReleaseSecondary).ToList();
if (releaseToMergeImages.Any())
{
var releaseToMergeIntoPrimaryImage = ImageHelper.FindImageTypeInDirectory(new DirectoryInfo(releasePath), ImageType.Release).FirstOrDefault();
if (releaseToMergeIntoPrimaryImage != null)
{
releaseToMergeSecondaryImages.Add(releaseToMergeImages.First());
}
else
{
var releaseImageFilename = Path.Combine(releasePath, ImageHelper.ReleaseCoverFilename);
releaseToMergeImages.First().MoveTo(releaseImageFilename, true);
}
}
if (releaseToMergeSecondaryImages.Any())
{
var looper = 0;
foreach (var releaseSecondaryImage in releaseToMergeSecondaryImages)
{
var releaseImageFilename = Path.Combine(releasePath, string.Format(ImageHelper.ReleaseSecondaryImageFilename, looper.ToString("00")));
while (File.Exists(releaseImageFilename))
{
looper++;
releaseImageFilename = Path.Combine(releasePath, string.Format(ImageHelper.ReleaseSecondaryImageFilename, looper.ToString("00")));
}
releaseSecondaryImage.MoveTo(releaseImageFilename, true);
}
}
release.Status = Statuses.Migrated;
release.LastUpdated = now;
await DbContext.SaveChangesAsync();
Logger.LogInformation($"Migrated Release `{ release}` From [{ oldReleasePath }] => [{ releasePath }]");
releasesMigrated++;
}
Logger.LogInformation($"Release Migration Complete. Migrated [{ releasesMigrated }] Releases.");
if (deleteEmptyFolders)
{
Logger.LogInformation($"Deleting Empty Folders in Library [{ Configuration.LibraryFolder }] Folder.");
Services.FileDirectoryProcessorService.DeleteEmptyFolders(new DirectoryInfo(Configuration.LibraryFolder), Logger);
}
CacheManager.Clear();
return new OperationResult<bool>
{
IsSuccess = !errors.Any(),
Data = true,
OperationTime = sw.ElapsedMilliseconds,
Errors = errors
};
}
/// <summary>
/// Migrate images from Images table and Thumbnails to file storage.
/// </summary>
@ -1111,7 +1313,7 @@ namespace Roadie.Api.Services
}
await DbContext.SaveChangesAsync();
foreach (var release in DbContext.Releases.Include(x => x.Artist).Where(x => x.Thumbnail != null).OrderBy(x => x.Title))
foreach (var release in DbContext.Releases.Include(x => x.Artist).Where(x => x.Thumbnail != null).OrderBy(x => x.SortTitle ?? x.Title))
{
var artistFolder = release.Artist.ArtistFileFolder(Configuration);
if (!Directory.Exists(artistFolder))

View file

@ -147,7 +147,7 @@ namespace Roadie.Api.Services
};
}
public async Task<OperationResult<bool>> Delete(ApplicationUser user, data.Artist artist)
public async Task<OperationResult<bool>> Delete(ApplicationUser user, data.Artist artist, bool deleteFolder)
{
var isSuccess = false;
try
@ -156,7 +156,11 @@ namespace Roadie.Api.Services
{
DbContext.Artists.Remove(artist);
await DbContext.SaveChangesAsync();
// TODO delete artist folder if empty?
if(deleteFolder)
{
var artistDir = new DirectoryInfo(artist.ArtistFileFolder(Configuration));
FolderPathHelper.DeleteEmptyFolders(artistDir.Parent);
}
CacheManager.ClearRegion(artist.CacheRegion);
Logger.LogWarning("User `{0}` deleted Artist `{1}]`", user, artist);
isSuccess = true;
@ -1410,7 +1414,7 @@ namespace Roadie.Api.Services
}
await DbContext.SaveChangesAsync();
await Delete(user, artistToMerge);
await Delete(user, artistToMerge, true);
result = true;

View file

@ -97,7 +97,7 @@ namespace Roadie.Api.Services
MakeArtistThumbnailImage(Configuration, HttpContext, release.Artist.RoadieId),
MakeReleaseThumbnailImage(Configuration, HttpContext, release.RoadieId));
row.Thumbnail = MakeReleaseThumbnailImage(Configuration, HttpContext, release.RoadieId);
row.SortName = release.Title;
row.SortName = release.SortTitleValue;
break;
case BookmarkType.Track:

View file

@ -9,7 +9,7 @@ namespace Roadie.Api.Services
{
public interface IAdminService
{
Task<OperationResult<bool>> DeleteArtist(ApplicationUser user, Guid artistId);
Task<OperationResult<bool>> DeleteArtist(ApplicationUser user, Guid artistId, bool deleteFolder);
Task<OperationResult<bool>> DeleteArtistReleases(ApplicationUser user, Guid artistId, bool doDeleteFiles = false);
@ -54,5 +54,7 @@ namespace Roadie.Api.Services
Task<OperationResult<bool>> ValidateInviteToken(Guid? tokenId);
Task<OperationResult<bool>> MigrateImages(ApplicationUser user);
Task<OperationResult<bool>> MigrateStorage(ApplicationUser user, bool deleteEmptyFolders);
}
}

View file

@ -14,7 +14,7 @@ namespace Roadie.Api.Services
{
Task<OperationResult<Artist>> ById(User user, Guid id, IEnumerable<string> includes);
Task<OperationResult<bool>> Delete(ApplicationUser user, Library.Data.Artist Artist);
Task<OperationResult<bool>> Delete(ApplicationUser user, Library.Data.Artist Artist, bool deleteFolder);
Task<PagedResult<ArtistList>> List(User user, PagedRequest request, bool? doRandomize = false, bool? onlyIncludeWithReleases = true);

View file

@ -225,7 +225,7 @@ namespace Roadie.Api.Services
// Update the track path to have the new album title. This is needed because future scans might not work properly without updating track title.
foreach (var track in DbContext.Tracks.Where(x => x.ReleaseMediaId == releaseMedia.Id).ToArray())
{
track.FilePath = Path.Combine(releaseDirectoryInfo.Parent.Name, releaseDirectoryInfo.Name);
track.FilePath = FolderPathHelper.TrackPath(Configuration, release.Artist, release, track);
var trackPath = track.PathToTrack(Configuration);
var trackFileInfo = new FileInfo(trackPath);
if (trackFileInfo.Exists)
@ -344,8 +344,12 @@ namespace Roadie.Api.Services
var now = DateTime.UtcNow;
if (doUpdateArtistCounts) await UpdateArtistCounts(release.Artist.Id, now);
if (releaseLabelIds != null && releaseLabelIds.Any())
{
foreach (var releaseLabelId in releaseLabelIds)
{
await UpdateLabelCounts(releaseLabelId, now);
}
}
sw.Stop();
Logger.LogWarning("User `{0}` deleted Release `{1}]`", user, release);
return new OperationResult<bool>
@ -1109,7 +1113,7 @@ namespace Roadie.Api.Services
zipBytes = zipStream.ToArray();
}
zipFileName = $"{release.Artist.Name}_{release.Title}.zip".ToFileNameFriendly();
zipFileName = $"{release.Artist.Name}_{release.SortTitleValue}.zip".ToFileNameFriendly();
Logger.LogTrace(
$"User `{roadieUser}` downloaded Release `{release}` ZipFileName [{zipFileName}], Zip Size [{zipBytes?.Length}]");
}
@ -1255,7 +1259,7 @@ namespace Roadie.Api.Services
string partTitles = null;
var audioMetaData = await AudioMetaDataHelper.GetInfo(file, doJustInfo);
// This is the path for the new track not in the database but the found MP3 file to be added to library
var trackPath = Path.Combine(releaseDirectory.Parent.Name, releaseDirectory.Name);
var trackPath = FolderPathHelper.TrackPath(Configuration, release.Artist.Id, release.Artist.SortNameValue, release.SortTitleValue, release.ReleaseDate.Value, audioMetaData.Title, audioMetaData.TrackNumber.Value);
if (audioMetaData.IsValid)
{
@ -1550,6 +1554,7 @@ namespace Roadie.Api.Services
release.IsVirtual = model.IsVirtual;
release.Status = SafeParser.ToEnum<Statuses>(model.Status);
release.Title = model.Title;
release.SortTitle = model.SortTitle;
var specialReleaseTitle = model.Title.ToAlphanumericName();
var alt = new List<string>(model.AlternateNamesList);
if (!model.AlternateNamesList.Contains(specialReleaseTitle, StringComparer.OrdinalIgnoreCase))

View file

@ -9,7 +9,7 @@
<PackageReference Include="Hashids.net" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.19" />
</ItemGroup>

View file

@ -40,9 +40,9 @@ namespace Roadie.Api.Controllers
[HttpPost("delete/artist/{id}")]
[ProducesResponseType(200)]
public async Task<IActionResult> DeleteArtist(Guid id)
public async Task<IActionResult> DeleteArtist(Guid id, bool? doDeleteFile)
{
var result = await AdminService.DeleteArtist(await UserManager.GetUserAsync(User), id);
var result = await AdminService.DeleteArtist(await UserManager.GetUserAsync(User), id, doDeleteFile ?? true);
if (!result.IsSuccess)
{
if (result.Messages?.Any() ?? false)
@ -354,5 +354,21 @@ namespace Roadie.Api.Controllers
return Ok(result);
}
[HttpPost("migratestorage")]
[ProducesResponseType(200)]
public async Task<IActionResult> MigrateStorage(bool? deleteEmptyFolders)
{
var result = await AdminService.MigrateStorage(await UserManager.GetUserAsync(User), deleteEmptyFolders ?? true);
if (!result.IsSuccess)
{
if (result.Messages?.Any() ?? false)
{
return StatusCode((int)HttpStatusCode.BadRequest, result.Messages);
}
return StatusCode((int)HttpStatusCode.InternalServerError);
}
return Ok(result);
}
}
}

View file

@ -32,7 +32,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="3.1.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />

View file

@ -438,7 +438,7 @@ namespace Roadie.Dlna.Services
{
return (from r in DbContext.Releases
where r.ArtistId == artistId
orderby r.ReleaseYear, r.Title
orderby r.ReleaseYear, r.SortTitleValue
select r).ToArray();
}, "urn:DlnaServiceRegion");
}
@ -481,7 +481,7 @@ namespace Roadie.Dlna.Services
join cr in DbContext.CollectionReleases on c.Id equals cr.CollectionId
join r in DbContext.Releases on cr.ReleaseId equals r.Id
where c.Id == collectionId
orderby cr.ListNumber, r.Title
orderby cr.ListNumber, r.SortTitleValue
select r).ToArray();
}, "urn:DlnaServiceRegion");
}

View file

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{1BA7
Upgrade0004.sql = Upgrade0004.sql
Upgrade0005.sql = Upgrade0005.sql
Upgrade0006.sql = Upgrade0006.sql
Upgrade0007.sql = Upgrade0007.sql
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Api.Services", "Roadie.Api.Services\Roadie.Api.Services.csproj", "{7B37031E-F2AE-4BE2-9F6F-005CA7A6FDF1}"

2
Upgrade0007.sql Normal file
View file

@ -0,0 +1,2 @@
-- New Release SortTitle for file friendly names
ALTER TABLE `release` ADD sortTitle varchar(250) NULL;

View file

@ -534,6 +534,7 @@ 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`),