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>
<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" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
</ItemGroup> </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 Roadie.Library.Imaging;
using System; using System;
using System.IO;
using Xunit; using Xunit;
namespace Roadie.Library.Tests namespace Roadie.Library.Tests
@ -9,6 +10,10 @@ namespace Roadie.Library.Tests
[Fact] [Fact]
public void GenerateImageHash() public void GenerateImageHash()
{ {
if(!Directory.Exists(@"C:\temp\image_tests"))
{
return;
}
var imageFilename = @"C:\temp\image_tests\1.jpg"; var imageFilename = @"C:\temp\image_tests\1.jpg";
var secondImagFilename = @"C:\temp\image_tests\2.jpg"; var secondImagFilename = @"C:\temp\image_tests\2.jpg";
var resizedFirstImageFilename = @"C:\temp\image_tests\1-resized.jpg"; var resizedFirstImageFilename = @"C:\temp\image_tests\1-resized.jpg";

View file

@ -413,7 +413,7 @@ namespace Roadie.Library.Tests
} }
context.SaveChanges(); 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 artistFolder = release.Artist.ArtistFileFolder(settings);
var releaseFolder = release.ReleaseFileFolder(artistFolder); var releaseFolder = release.ReleaseFileFolder(artistFolder);

View file

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

View file

@ -244,7 +244,8 @@ namespace Roadie.Library.Tests
[InlineData("Colin Hay!", "colinhay")] [InlineData("Colin Hay!", "colinhay")]
[InlineData("colinhay", "colinhay")] [InlineData("colinhay", "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(" Leslie &amp; Tom", "leslieandtom")]
[InlineData("<b>Leslie &amp; &#32;&#32; Tom</b>", "leslieandtom")] [InlineData("<b>Leslie &amp; &#32;&#32; Tom</b>", "leslieandtom")]
[InlineData("Leslie;/&/;Tom", "leslieandtom")] [InlineData("Leslie;/&/;Tom", "leslieandtom")]

View file

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

View file

@ -23,12 +23,23 @@ namespace Roadie.Library.Data
} }
} }
public string SortNameValue => string.IsNullOrEmpty(SortName) ? Name : SortName;
/// <summary> /// <summary>
/// Returns a full file path to the Label Image /// Returns a full file path to the Label Image
/// </summary> /// </summary>
public string PathToImage(IRoadieSettings configuration) 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); public bool IsValid => !string.IsNullOrEmpty(Name);

View file

@ -91,6 +91,12 @@ namespace Roadie.Library.Data
[Required] [Required]
public string Title { get; set; } 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("trackCount")] public short TrackCount { get; set; }
[Column("urls", TypeName = "text")] [Column("urls", TypeName = "text")]

View file

@ -102,14 +102,11 @@ namespace Roadie.Library.Data
/// </summary> /// </summary>
/// <param name="artistFolder"></param> /// <param name="artistFolder"></param>
/// <returns></returns> /// <returns></returns>
public string ReleaseFileFolder(string artistFolder) public string ReleaseFileFolder(string artistFolder) => FolderPathHelper.ReleasePath(artistFolder, SortTitleValue, ReleaseDate.Value);
{
return FolderPathHelper.ReleasePath(artistFolder, Title, ReleaseDate.Value);
}
public override string ToString() 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.AlternateNames = artist.AlternateNames.AddToDelimitedList(new[] { artist.Name.ToAlphanumericName() });
artist.Genres = null; artist.Genres = null;
artist.Images = 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) if (!artist.IsValid)
{ {
return new OperationResult<Artist> return new OperationResult<Artist>
@ -93,10 +79,6 @@ namespace Roadie.Library.Engines
Errors = new Exception[1] { new Exception("Artist is Invalid") } 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 addArtistResult = DbContext.Artists.Add(artist);
var inserted = 0; var inserted = 0;
inserted = await DbContext.SaveChangesAsync(); inserted = await DbContext.SaveChangesAsync();
@ -378,9 +360,6 @@ namespace Roadie.Library.Engines
BeginDate = i.BeginDate, BeginDate = i.BeginDate,
Name = result.Name ?? i.ArtistName, Name = result.Name ?? i.ArtistName,
SortName = result.SortName ?? i.ArtistSortName, SortName = result.SortName ?? i.ArtistSortName,
Thumbnail = i.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(i.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? i.ArtistType ArtistType = result.ArtistType ?? i.ArtistType
}); });
} }
@ -443,9 +422,6 @@ namespace Roadie.Library.Engines
BeginDate = mb.BeginDate, BeginDate = mb.BeginDate,
Name = result.Name ?? mb.ArtistName, Name = result.Name ?? mb.ArtistName,
SortName = result.SortName ?? mb.ArtistSortName, SortName = result.SortName ?? mb.ArtistSortName,
Thumbnail = mb.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(mb.ArtistThumbnailUrl)
: null,
ArtistType = mb.ArtistType ArtistType = mb.ArtistType
}); });
} }
@ -488,9 +464,6 @@ namespace Roadie.Library.Engines
BeginDate = l.BeginDate, BeginDate = l.BeginDate,
Name = result.Name ?? l.ArtistName, Name = result.Name ?? l.ArtistName,
SortName = result.SortName ?? l.ArtistSortName, SortName = result.SortName ?? l.ArtistSortName,
Thumbnail = l.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(l.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? l.ArtistType ArtistType = result.ArtistType ?? l.ArtistType
}); });
} }
@ -530,9 +503,6 @@ namespace Roadie.Library.Engines
BeginDate = s.BeginDate, BeginDate = s.BeginDate,
Name = result.Name ?? s.ArtistName, Name = result.Name ?? s.ArtistName,
SortName = result.SortName ?? s.ArtistSortName, SortName = result.SortName ?? s.ArtistSortName,
Thumbnail = s.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(s.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? s.ArtistType ArtistType = result.ArtistType ?? s.ArtistType
}); });
} }
@ -573,9 +543,6 @@ namespace Roadie.Library.Engines
DiscogsId = d.DiscogsId, DiscogsId = d.DiscogsId,
Name = result.Name ?? d.ArtistName, Name = result.Name ?? d.ArtistName,
RealName = result.RealName ?? d.ArtistRealName, RealName = result.RealName ?? d.ArtistRealName,
Thumbnail = d.ArtistThumbnailUrl != null
? WebHelper.BytesForImageUrl(d.ArtistThumbnailUrl)
: null,
ArtistType = result.ArtistType ?? d.ArtistType ArtistType = result.ArtistType ?? d.ArtistType
}); });
} }

View file

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

View file

@ -845,8 +845,6 @@ namespace Roadie.Library.Engines
{ {
var image = metaData.Images.FirstOrDefault(x => x.Type == AudioMetaDataImageType.FrontCover); var image = metaData.Images.FirstOrDefault(x => x.Type == AudioMetaDataImageType.FrontCover);
if (image == null) image = metaData.Images.FirstOrDefault(); 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)) if (!string.IsNullOrEmpty(artistFolder))
@ -873,13 +871,6 @@ namespace Roadie.Library.Engines
{ {
coverFileName = cover.First().FullName; 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(); sw.Stop();

View file

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

View file

@ -226,15 +226,22 @@ namespace Roadie.Library.Extensions
return input; 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 = WebUtility.HtmlDecode(input);
input = input.ScrubHtml().ToLower().Trim().Replace("&", "and"); input = input.ScrubHtml().ToLower()
.Replace("&", "and");
var arr = input.ToCharArray(); 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 = 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; return input;
} }
@ -252,7 +259,11 @@ namespace Roadie.Library.Extensions
public static string ToFolderNameFriendly(this string input) 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('.'); return Regex.Replace(PathSanitizer.SanitizeFilename(input, ' '), @"\s+", " ").Trim().TrimEnd('.');
} }

View file

@ -322,16 +322,22 @@ namespace Roadie.Library.FilePlugins
int? submissionId) int? submissionId)
{ {
var artist = await ArtistLookupEngine.GetByName(metaData, !doJustInfo); var artist = await ArtistLookupEngine.GetByName(metaData, !doJustInfo);
if (!artist.IsSuccess) return null; if (!artist.IsSuccess)
{
return null;
}
_artistId = artist.Data.RoadieId; _artistId = artist.Data.RoadieId;
var release = var release = await ReleaseLookupEngine.GetByName(artist.Data, metaData, !doJustInfo, submissionId: submissionId);
await ReleaseLookupEngine.GetByName(artist.Data, metaData, !doJustInfo, submissionId: submissionId); if (!release.IsSuccess)
if (!release.IsSuccess) return null; {
return null;
}
_releaseId = release.Data.RoadieId; _releaseId = release.Data.RoadieId;
release.Data.ReleaseDate = SafeParser.ToDateTime(release.Data.ReleaseYear ?? metaData.Year); release.Data.ReleaseDate = SafeParser.ToDateTime(release.Data.ReleaseYear ?? metaData.Year);
if (release.Data.ReleaseYear.HasValue && 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); return release.Data.ReleaseFileFolder(artistFolder);
} }
} }

View file

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

View file

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

View file

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

View file

@ -9,9 +9,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoCompare.Core" Version="1.0.0" /> <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="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="Hashids.net" Version="1.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.16" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
<PackageReference Include="IdSharp.Common" Version="1.0.1" /> <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.Net.Http.Headers" Version="2.2.0" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" /> <PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" />
<PackageReference Include="MimeMapping" Version="1.0.1.17" /> <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="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="RestSharp" Version="106.6.10" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0006" /> <PackageReference Include="SixLabors.Core" Version="1.0.0-beta0008" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0005" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.Shapes" 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.Drawing.Common" Version="4.6.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" />
<PackageReference Include="System.Runtime.Caching" 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.Extensions;
using Roadie.Library.MetaData.Audio; using Roadie.Library.MetaData.Audio;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
namespace Roadie.Library.Utility namespace Roadie.Library.Utility
{ {
@ -14,11 +16,114 @@ namespace Roadie.Library.Utility
/// </summary> /// </summary>
public static class FolderPathHelper 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> /// <summary>
/// Full path to Artist folder /// Full path to Artist folder
/// </summary> /// </summary>
/// <param name="artistSortName">Sort name of Artist to use for folder name</param> /// <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"); SimpleContract.Requires<ArgumentException>(!string.IsNullOrEmpty(artistSortName),"Invalid Artist Sort Name");
@ -27,33 +132,53 @@ namespace Roadie.Library.Utility
return directoryInfo.FullName; 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"); if(!rt.Equals(stringReplacement))
}
try
{
foreach (var d in Directory.EnumerateDirectories(dir)) DeleteEmptyDirs(d);
var entries = Directory.EnumerateFileSystemEntries(dir);
if (!entries.Any() && deleteDirIfEmpty)
{ {
try rt.Replace(stringReplacement, " ");
{
Directory.Delete(dir);
}
catch (UnauthorizedAccessException)
{
}
catch (DirectoryNotFoundException)
{
}
} }
} }
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> /// <summary>
@ -67,6 +192,7 @@ namespace Roadie.Library.Utility
{ {
return true; return true;
} }
var result = false;
try try
{ {
foreach (var folder in processingFolder.GetDirectories("*.*", SearchOption.AllDirectories)) foreach (var folder in processingFolder.GetDirectories("*.*", SearchOption.AllDirectories))
@ -78,27 +204,34 @@ namespace Roadie.Library.Utility
if (!folder.GetFiles("*.*", SearchOption.AllDirectories).Any()) if (!folder.GetFiles("*.*", SearchOption.AllDirectories).Any())
{ {
folder.Delete(true); 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 (DirectoryNotFoundException)
{ {
} result = false;
catch (Exception) Trace.WriteLine($"DirectoryNotFoundException Deleting Empty Folder [{folder.FullName}]", "Debug");
{
throw;
} }
} }
} }
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> /// <summary>
@ -143,22 +276,6 @@ namespace Roadie.Library.Utility
return directoryInfo.FullName; 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> /// <summary>
/// Returns the FileName for given Track details, this is not the Full Path (FQDN) only the FileName /// Returns the FileName for given Track details, this is not the Full Path (FQDN) only the FileName
/// </summary> /// </summary>
@ -202,9 +319,12 @@ namespace Roadie.Library.Utility
.ToTitleCase(false); .ToTitleCase(false);
var trackPathReplace = configuration.TrackPathReplace; var trackPathReplace = configuration.TrackPathReplace;
if (trackPathReplace != null) if (trackPathReplace != null)
{
foreach (var kp in trackPathReplace) foreach (var kp in trackPathReplace)
{
fileNameFromTitle = fileNameFromTitle.Replace(kp.Key, kp.Value); fileNameFromTitle = fileNameFromTitle.Replace(kp.Key, kp.Value);
}
}
return string.Format("{0}{1} {2}.{3}", disc, track, fileNameFromTitle, fileExtension.ToLower()); 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> /// <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) 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, SafeParser.ToDateTime(metaData.Year).Value,
metaData.Title, metaData.TrackNumber ?? 0, metaData.Disc ?? 0, metaData.Title, metaData.TrackNumber ?? 0, metaData.Disc ?? 0,
metaData.TotalTrackNumbers ?? 0, metaData.TotalTrackNumbers ?? 0,
@ -231,7 +351,7 @@ namespace Roadie.Library.Utility
/// <param name="track">Track</param> /// <param name="track">Track</param>
/// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param> /// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param>
/// <returns></returns> /// <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> /// <summary>
/// Return the full path (FQDN) for given Track details /// 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="discNumber">Optional disc number defaults to 0</param>
/// <param name="totalTrackNumber">Optional Total Tracks defaults to TrackNumber</param> /// <param name="totalTrackNumber">Optional Total Tracks defaults to TrackNumber</param>
/// <param name="fileExtension">Optional File Extension defaults to mp3</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", DateTime releaseDate, string trackTitle, short trackNumber, int? discNumber = null, int? totalTrackNumber = null, string fileExtension = "mp3",
string artistFolder = null, string releaseFolder = null) string artistFolder = null, string releaseFolder = null)
{ {
artistFolder = artistFolder ?? ArtistPath(configuration, artistSortName); artistFolder ??= ArtistPath(configuration, artistId, artistSortName);
releaseFolder = releaseFolder ?? ReleasePath(artistFolder, releaseTitle, releaseDate); releaseFolder ??= ReleasePath(artistFolder, releaseTitle, releaseDate);
var trackFileName = TrackFileName(configuration, trackTitle, trackNumber, discNumber, totalTrackNumber, fileExtension); var trackFileName = TrackFileName(configuration, trackTitle, trackNumber, discNumber, totalTrackNumber, fileExtension);
var result = Path.Combine(artistFolder, releaseFolder, trackFileName); var result = Path.Combine(artistFolder, releaseFolder, trackFileName);
var resultInfo = new DirectoryInfo(result); 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; 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="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param>
/// <param name="artistFolder">Optional ArtistFolder default is to get from MetaData artist</param> /// <param name="artistFolder">Optional ArtistFolder default is to get from MetaData artist</param>
/// /// /// ///
public static string TrackPath(IRoadieSettings configuration, AudioMetaData metaData, public static string TrackPath(IRoadieSettings configuration, AudioMetaData metaData, string destinationFolder = null, string artistFolder = null)
string destinationFolder = null, string artistFolder = null)
{ {
var fileInfo = new FileInfo(TrackFullPath(configuration, metaData, destinationFolder, artistFolder)); 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> /// <summary>
@ -283,8 +403,14 @@ namespace Roadie.Library.Utility
/// <param name="track">Track</param> /// <param name="track">Track</param>
public static string TrackPath(IRoadieSettings configuration, Artist artist, Release release, Track track) 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)); var fileInfo = new FileInfo(TrackFullPath(configuration, artist.Id, artist.SortNameValue, release.SortTitleValue, release.ReleaseDate.Value, track.Title, track.TrackNumber));
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());
}
var result = Path.Combine(tf, fileInfo.Directory.Name);
return result;
} }
/// <summary> /// <summary>
@ -297,11 +423,17 @@ namespace Roadie.Library.Utility
/// <param name="destinationFolder">Optional Root folder defaults to Library Folder from Settings</param> /// <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="discNumber">Optional disc number defaults to 0</param>
/// <param name="totalTrackNumber">Optional Total Tracks defaults to TrackNumber</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) 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)); var fileInfo = new FileInfo(TrackFullPath(configuration, artistId, artistSortName, releaseTitle, releaseDate, trackTitle, trackNumber, discNumber, totalTrackNumber));
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());
}
var result = Path.Combine(tf, fileInfo.Directory.Name);
return result;
} }
} }
} }

View file

@ -27,17 +27,25 @@ namespace Roadie.Library.Utility
c.AddRange(Path.GetInvalidFileNameChars()); 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 // 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) foreach (var badWindowsFilecharacter in badWindowsFileAndFoldercharacters)
{
if (!c.Contains(badWindowsFilecharacter)) if (!c.Contains(badWindowsFilecharacter))
{
c.Add(badWindowsFilecharacter); c.Add(badWindowsFilecharacter);
}
}
invalidFilenameChars = c.ToArray(); invalidFilenameChars = c.ToArray();
var f = new List<char>(); var f = new List<char>();
f.AddRange(Path.GetInvalidPathChars()); f.AddRange(Path.GetInvalidPathChars());
foreach (var badWindowsFilecharacter in badWindowsFileAndFoldercharacters) foreach (var badWindowsFilecharacter in badWindowsFileAndFoldercharacters)
{
if (!f.Contains(badWindowsFilecharacter)) if (!f.Contains(badWindowsFilecharacter))
{
f.Add(badWindowsFilecharacter); f.Add(badWindowsFilecharacter);
}
}
invalidFilenameChars = c.ToArray(); invalidFilenameChars = c.ToArray();
invalidPathChars = f.ToArray(); invalidPathChars = f.ToArray();
@ -77,17 +85,25 @@ namespace Roadie.Library.Utility
private static string Sanitize(string input, char[] invalidChars, char errorChar) private static string Sanitize(string input, char[] invalidChars, char errorChar)
{ {
// null always sanitizes to null // null always sanitizes to null
if (input == null) return null; if (input == null)
{
return null;
}
var result = new StringBuilder(); var result = new StringBuilder();
foreach (var characterToTest in input) foreach (var characterToTest in input)
{
// we binary search for the character in the invalid set. This should be lightning fast. // we binary search for the character in the invalid set. This should be lightning fast.
if (Array.BinarySearch(invalidChars, characterToTest) >= 0) if (Array.BinarySearch(invalidChars, characterToTest) >= 0)
{
// we found the character in the array of // we found the character in the array of
result.Append(errorChar); result.Append(errorChar);
}
else else
{
// the character was not found in invalid, so it is valid. // the character was not found in invalid, so it is valid.
result.Append(characterToTest); result.Append(characterToTest);
}
}
// we're done. // we're done.
return result.ToString(); return result.ToString();
} }

View file

@ -60,7 +60,7 @@ namespace Roadie.Api.Services
FileDirectoryProcessorService = fileDirectoryProcessorService; 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(); var sw = new Stopwatch();
sw.Start(); sw.Start();
@ -74,7 +74,7 @@ namespace Roadie.Api.Services
try try
{ {
var result = await ArtistService.Delete(user, artist); var result = await ArtistService.Delete(user, artist, deleteFolder);
if (!result.IsSuccess) if (!result.IsSuccess)
{ {
return new OperationResult<bool> 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> /// <summary>
/// Migrate images from Images table and Thumbnails to file storage. /// Migrate images from Images table and Thumbnails to file storage.
/// </summary> /// </summary>
@ -1111,7 +1313,7 @@ namespace Roadie.Api.Services
} }
await DbContext.SaveChangesAsync(); 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); var artistFolder = release.Artist.ArtistFileFolder(Configuration);
if (!Directory.Exists(artistFolder)) 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; var isSuccess = false;
try try
@ -156,7 +156,11 @@ namespace Roadie.Api.Services
{ {
DbContext.Artists.Remove(artist); DbContext.Artists.Remove(artist);
await DbContext.SaveChangesAsync(); 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); CacheManager.ClearRegion(artist.CacheRegion);
Logger.LogWarning("User `{0}` deleted Artist `{1}]`", user, artist); Logger.LogWarning("User `{0}` deleted Artist `{1}]`", user, artist);
isSuccess = true; isSuccess = true;
@ -1410,7 +1414,7 @@ namespace Roadie.Api.Services
} }
await DbContext.SaveChangesAsync(); await DbContext.SaveChangesAsync();
await Delete(user, artistToMerge); await Delete(user, artistToMerge, true);
result = true; result = true;

View file

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

View file

@ -9,7 +9,7 @@ namespace Roadie.Api.Services
{ {
public interface IAdminService 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); 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>> ValidateInviteToken(Guid? tokenId);
Task<OperationResult<bool>> MigrateImages(ApplicationUser user); 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<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); 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. // 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()) 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 trackPath = track.PathToTrack(Configuration);
var trackFileInfo = new FileInfo(trackPath); var trackFileInfo = new FileInfo(trackPath);
if (trackFileInfo.Exists) if (trackFileInfo.Exists)
@ -344,8 +344,12 @@ namespace Roadie.Api.Services
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
if (doUpdateArtistCounts) await UpdateArtistCounts(release.Artist.Id, now); if (doUpdateArtistCounts) await UpdateArtistCounts(release.Artist.Id, now);
if (releaseLabelIds != null && releaseLabelIds.Any()) if (releaseLabelIds != null && releaseLabelIds.Any())
{
foreach (var releaseLabelId in releaseLabelIds) foreach (var releaseLabelId in releaseLabelIds)
{
await UpdateLabelCounts(releaseLabelId, now); await UpdateLabelCounts(releaseLabelId, now);
}
}
sw.Stop(); sw.Stop();
Logger.LogWarning("User `{0}` deleted Release `{1}]`", user, release); Logger.LogWarning("User `{0}` deleted Release `{1}]`", user, release);
return new OperationResult<bool> return new OperationResult<bool>
@ -1109,7 +1113,7 @@ namespace Roadie.Api.Services
zipBytes = zipStream.ToArray(); zipBytes = zipStream.ToArray();
} }
zipFileName = $"{release.Artist.Name}_{release.Title}.zip".ToFileNameFriendly(); zipFileName = $"{release.Artist.Name}_{release.SortTitleValue}.zip".ToFileNameFriendly();
Logger.LogTrace( Logger.LogTrace(
$"User `{roadieUser}` downloaded Release `{release}` ZipFileName [{zipFileName}], Zip Size [{zipBytes?.Length}]"); $"User `{roadieUser}` downloaded Release `{release}` ZipFileName [{zipFileName}], Zip Size [{zipBytes?.Length}]");
} }
@ -1255,7 +1259,7 @@ namespace Roadie.Api.Services
string partTitles = null; string partTitles = null;
var audioMetaData = await AudioMetaDataHelper.GetInfo(file, doJustInfo); 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 // 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) if (audioMetaData.IsValid)
{ {
@ -1550,6 +1554,7 @@ namespace Roadie.Api.Services
release.IsVirtual = model.IsVirtual; release.IsVirtual = model.IsVirtual;
release.Status = SafeParser.ToEnum<Statuses>(model.Status); release.Status = SafeParser.ToEnum<Statuses>(model.Status);
release.Title = model.Title; release.Title = model.Title;
release.SortTitle = model.SortTitle;
var specialReleaseTitle = model.Title.ToAlphanumericName(); var specialReleaseTitle = model.Title.ToAlphanumericName();
var alt = new List<string>(model.AlternateNamesList); var alt = new List<string>(model.AlternateNamesList);
if (!model.AlternateNamesList.Contains(specialReleaseTitle, StringComparer.OrdinalIgnoreCase)) if (!model.AlternateNamesList.Contains(specialReleaseTitle, StringComparer.OrdinalIgnoreCase))

View file

@ -9,7 +9,7 @@
<PackageReference Include="Hashids.net" Version="1.3.0" /> <PackageReference Include="Hashids.net" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.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.IdentityModel.Tokens.Jwt" Version="5.6.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.19" /> <PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.19" />
</ItemGroup> </ItemGroup>

View file

@ -40,9 +40,9 @@ namespace Roadie.Api.Controllers
[HttpPost("delete/artist/{id}")] [HttpPost("delete/artist/{id}")]
[ProducesResponseType(200)] [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.IsSuccess)
{ {
if (result.Messages?.Any() ?? false) if (result.Messages?.Any() ?? false)
@ -354,5 +354,21 @@ namespace Roadie.Api.Controllers
return Ok(result); 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.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" 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.AspNetCore" Version="3.1.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" /> <PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <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 return (from r in DbContext.Releases
where r.ArtistId == artistId where r.ArtistId == artistId
orderby r.ReleaseYear, r.Title orderby r.ReleaseYear, r.SortTitleValue
select r).ToArray(); select r).ToArray();
}, "urn:DlnaServiceRegion"); }, "urn:DlnaServiceRegion");
} }
@ -481,7 +481,7 @@ namespace Roadie.Dlna.Services
join cr in DbContext.CollectionReleases on c.Id equals cr.CollectionId join cr in DbContext.CollectionReleases on c.Id equals cr.CollectionId
join r in DbContext.Releases on cr.ReleaseId equals r.Id join r in DbContext.Releases on cr.ReleaseId equals r.Id
where c.Id == collectionId where c.Id == collectionId
orderby cr.ListNumber, r.Title orderby cr.ListNumber, r.SortTitleValue
select r).ToArray(); select r).ToArray();
}, "urn:DlnaServiceRegion"); }, "urn:DlnaServiceRegion");
} }

View file

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{1BA7
Upgrade0004.sql = Upgrade0004.sql Upgrade0004.sql = Upgrade0004.sql
Upgrade0005.sql = Upgrade0005.sql Upgrade0005.sql = Upgrade0005.sql
Upgrade0006.sql = Upgrade0006.sql Upgrade0006.sql = Upgrade0006.sql
Upgrade0007.sql = Upgrade0007.sql
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roadie.Api.Services", "Roadie.Api.Services\Roadie.Api.Services.csproj", "{7B37031E-F2AE-4BE2-9F6F-005CA7A6FDF1}" 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, `playedCount` int(11) DEFAULT NULL,
`duration` int(11) DEFAULT NULL, `duration` int(11) DEFAULT NULL,
`rank` decimal(9,2) DEFAULT NULL, `rank` decimal(9,2) DEFAULT NULL,
`sortTitle` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `idx_releaseArtistAndTitle` (`artistId`,`title`), UNIQUE KEY `idx_releaseArtistAndTitle` (`artistId`,`title`),
KEY `ix_release_roadieId` (`roadieId`), KEY `ix_release_roadieId` (`roadieId`),