Add patterned rename and Move to, and keep settings, selection, and queue speed from resetting.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
161
tests/Explorer.Application.Tests/DestinationPatternTests.cs
Normal file
161
tests/Explorer.Application.Tests/DestinationPatternTests.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class DestinationPatternTests
|
||||
{
|
||||
private static RenameSubject File(string path, bool directory = false)
|
||||
=> new(path, PathRules.GetFileName(path), directory);
|
||||
|
||||
[Fact]
|
||||
public void Plex_style_folder_gets_the_file_inside()
|
||||
{
|
||||
Assert.True(DestinationPattern.TryResolve(
|
||||
@"\\10.0.0.31\media\movies\%filename_noext%",
|
||||
File(@"D:\downloads\Inception.mkv"),
|
||||
out var dest,
|
||||
out var error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(@"\\10.0.0.31\media\movies\Inception\Inception.mkv", dest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Explicit_filename_token_is_the_full_path()
|
||||
{
|
||||
Assert.True(DestinationPattern.TryResolve(
|
||||
@"\\10.0.0.31\media\movies\%filename_noext%\%filename%",
|
||||
File(@"D:\downloads\Inception.mkv"),
|
||||
out var dest,
|
||||
out _));
|
||||
Assert.Equal(@"\\10.0.0.31\media\movies\Inception\Inception.mkv", dest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Folder_moves_to_the_expanded_directory()
|
||||
{
|
||||
Assert.True(DestinationPattern.TryResolve(
|
||||
@"\\10.0.0.31\media\movies\%filename_noext%",
|
||||
File(@"D:\downloads\Inception", directory: true),
|
||||
out var dest,
|
||||
out _));
|
||||
Assert.Equal(@"\\10.0.0.31\media\movies\Inception", dest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Source_drive_year_and_parent_expand()
|
||||
{
|
||||
var fields = new MoveToFields(
|
||||
"clip.mp4",
|
||||
"clip",
|
||||
"mp4",
|
||||
"Vacation",
|
||||
"E:",
|
||||
new DateTimeOffset(new DateTime(2024, 8, 26, 0, 0, 0, DateTimeKind.Local)),
|
||||
null);
|
||||
Assert.True(DestinationPattern.TryExpand(
|
||||
@"%source_drive%\sorted\%year%\%month%\%parent%",
|
||||
fields,
|
||||
out var expanded,
|
||||
out _));
|
||||
Assert.Equal(@"E:\sorted\2024\08\Vacation", expanded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Brace_tokens_match_percent_tokens()
|
||||
{
|
||||
Assert.True(DestinationPattern.TryResolve(
|
||||
@"{source_drive}\Media\{filename_noext}\{filename}",
|
||||
File(@"C:\tmp\Photo.jpg"),
|
||||
out var dest,
|
||||
out _));
|
||||
Assert.Equal(@"C:\Media\Photo\Photo.jpg", dest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relative_paths_are_rejected()
|
||||
{
|
||||
Assert.False(DestinationPattern.TryResolve(
|
||||
@"%filename_noext%\%filename%",
|
||||
File(@"C:\tmp\a.mkv"),
|
||||
out _,
|
||||
out var error));
|
||||
Assert.Contains("full destination", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_placeholder_is_an_error()
|
||||
{
|
||||
Assert.False(DestinationPattern.TryExpand(
|
||||
@"C:\out\%nope%",
|
||||
new MoveToFields("a.txt", "a", "txt", "tmp", "C:", null, null),
|
||||
out _,
|
||||
out var error));
|
||||
Assert.Contains("%nope%", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class DestinationPatternsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Combine_puts_saved_ahead_of_built_in()
|
||||
{
|
||||
var combined = DestinationPatterns.Combine([@"\\10.0.0.31\media\movies\%filename_noext%"]);
|
||||
Assert.Equal(@"\\10.0.0.31\media\movies\%filename_noext%", combined[0]);
|
||||
Assert.Contains(DestinationPatterns.BuiltIn[0], combined);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_does_not_persist_built_in_examples()
|
||||
{
|
||||
Assert.Empty(DestinationPatterns.Normalize([@"\\host\share\movies\%filename_noext%"]));
|
||||
}
|
||||
}
|
||||
|
||||
public class MoveToPlannerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Queues_a_move_into_the_named_folder()
|
||||
{
|
||||
var plan = new MoveToPlanner().Build(
|
||||
[new RenameSubject(@"D:\downloads\Inception.mkv", "Inception.mkv", false)],
|
||||
@"\\10.0.0.31\media\movies\%filename_noext%");
|
||||
Assert.True(plan.CanEnqueue);
|
||||
Assert.Equal(TransferOp.Move, plan.Operations[0].Op);
|
||||
Assert.Equal(@"\\10.0.0.31\media\movies\Inception\Inception.mkv", plan.Operations[0].DestinationPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Collision_is_an_error()
|
||||
{
|
||||
var plan = new MoveToPlanner().Build(
|
||||
[
|
||||
new RenameSubject(@"D:\a\Inception.mkv", "Inception.mkv", false),
|
||||
new RenameSubject(@"D:\b\Inception.mkv", "Inception.mkv", false)
|
||||
],
|
||||
@"\\server\media\movies\%filename_noext%");
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Online_only_is_an_error()
|
||||
{
|
||||
var plan = new MoveToPlanner().Build(
|
||||
[new RenameSubject(@"C:\cloud\clip.mp4", "clip.mp4", false)],
|
||||
@"D:\out\%filename_noext%",
|
||||
wouldHydrate: _ => true);
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("online-only", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Example_host_is_rejected()
|
||||
{
|
||||
var plan = new MoveToPlanner().Build(
|
||||
[new RenameSubject(@"D:\downloads\Inception.mkv", "Inception.mkv", false)],
|
||||
@"\\host\share\movies\%filename_noext%");
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("Browse", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
106
tests/Explorer.Application.Tests/FilenamePatternTests.cs
Normal file
106
tests/Explorer.Application.Tests/FilenamePatternTests.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class FilenamePatternTests
|
||||
{
|
||||
[Fact]
|
||||
public void Formats_artist_title_and_dates()
|
||||
{
|
||||
var tags = new MediaTagFields
|
||||
{
|
||||
Artist = "Pink Floyd",
|
||||
Title = "Comfortably Numb",
|
||||
Track = 6,
|
||||
Created = new DateTimeOffset(new DateTime(2024, 8, 26, 12, 0, 0, DateTimeKind.Local)),
|
||||
Stem = "clip",
|
||||
Extension = "mp3",
|
||||
Width = 1920
|
||||
};
|
||||
Assert.Equal(
|
||||
"Pink Floyd - Comfortably Numb",
|
||||
FilenamePattern.Expand("{Artist} - {Title}", tags));
|
||||
Assert.Equal("06", FilenamePattern.Expand("{Track:00}", tags));
|
||||
Assert.Equal("2024-08-26", FilenamePattern.Expand("{CreatedDate}", tags));
|
||||
Assert.Equal("1920_clip", FilenamePattern.Expand("{Width}_{Name}", tags));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parses_filename_into_tags()
|
||||
{
|
||||
Assert.True(FilenamePattern.TryParse(
|
||||
"{Artist} - {Title}",
|
||||
"AC/DC - Hells Bells",
|
||||
out var tags,
|
||||
out _));
|
||||
Assert.Equal("AC/DC", tags.Artist);
|
||||
Assert.Equal("Hells Bells", tags.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_fails_when_the_name_does_not_match()
|
||||
{
|
||||
Assert.False(FilenamePattern.TryParse("{Artist} - {Title}", "JustOneName", out _, out var error));
|
||||
Assert.Contains("match", error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_tokens_stay_in_the_name()
|
||||
{
|
||||
Assert.Equal("{Foo}_a", FilenamePattern.Expand("{Foo}_{Name}", new MediaTagFields { Stem = "a" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sanitizes_illegal_filename_characters_from_tags()
|
||||
{
|
||||
var name = FilenamePattern.Expand("{Title}", new MediaTagFields { Title = @"a:b/c" });
|
||||
Assert.Equal("a-b-c", name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Taken_date_and_project_expand()
|
||||
{
|
||||
var tags = new MediaTagFields
|
||||
{
|
||||
Taken = new DateTimeOffset(new DateTime(2024, 8, 26, 15, 30, 0, DateTimeKind.Local)),
|
||||
Created = new DateTimeOffset(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local)),
|
||||
Stem = "DSC_001",
|
||||
Project = "Explorer",
|
||||
Parent = "photos"
|
||||
};
|
||||
Assert.Equal("20240826_DSC_001", FilenamePattern.Expand("{TakenDate:yyyyMMdd}_{Name}", tags));
|
||||
Assert.Equal("2024-08-26", FilenamePattern.Expand("{CreatedDate}", tags));
|
||||
Assert.Equal("Explorer_DSC_001", FilenamePattern.Expand("{Project}_{Name}", tags));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parses_taken_date_from_filename()
|
||||
{
|
||||
Assert.True(FilenamePattern.TryParse(
|
||||
"{TakenDate:yyyyMMdd}_{Name}",
|
||||
"20240826_DSC_001",
|
||||
out var tags,
|
||||
out _));
|
||||
Assert.Equal(2024, tags.Taken?.Year);
|
||||
Assert.Equal(8, tags.Taken?.Month);
|
||||
Assert.Equal(26, tags.Taken?.Day);
|
||||
Assert.Equal(2024, tags.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithProject_uses_repo_folder_then_parent()
|
||||
{
|
||||
var fields = new MediaTagFields { Parent = "photos" };
|
||||
Assert.Equal("Explorer", FilenamePattern.WithProject(fields, @"C:\src\Explorer").Project);
|
||||
Assert.Equal("photos", FilenamePattern.WithProject(fields, null).Project);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TakenDate_is_media_content_CreatedDate_and_Project_are_not()
|
||||
{
|
||||
Assert.True(FilenamePattern.UsesMediaContent("{TakenDate}_{Name}"));
|
||||
Assert.False(FilenamePattern.UsesMediaContent("{CreatedDate}_{Name}"));
|
||||
Assert.False(FilenamePattern.UsesMediaContent("{Project}_{Name}"));
|
||||
}
|
||||
}
|
||||
23
tests/Explorer.Application.Tests/FilenamePatternsTests.cs
Normal file
23
tests/Explorer.Application.Tests/FilenamePatternsTests.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Explorer.Application;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class FilenamePatternsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Combine_lists_built_in_then_saved()
|
||||
{
|
||||
var combined = FilenamePatterns.Combine(["{Album} - {Title}", "{Artist} - {Title}"]);
|
||||
Assert.Equal(FilenamePatterns.BuiltIn[0], combined[0]);
|
||||
Assert.Contains("{Album} - {Title}", combined);
|
||||
Assert.Equal(1, combined.Count(p => p.Equals("{Artist} - {Title}", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_puts_the_new_pattern_first_and_drops_built_ins_from_saved()
|
||||
{
|
||||
var saved = FilenamePatterns.Add(null, "{Album} - {Title}");
|
||||
Assert.Equal(["{Album} - {Title}"], saved);
|
||||
Assert.Empty(FilenamePatterns.Normalize(["{Artist} - {Title}", " "]));
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,59 @@ public class RenamePlannerTests
|
||||
[File(@"C:\a\clip.mp4")],
|
||||
new RenameRuleSet { Prefix = "Clip_{Counter}_", UseCounter = true, CounterPadding = 3 });
|
||||
Assert.Equal("Clip_001_clip.mp4", plan.Preview[0].NewName);
|
||||
Assert.Contains("{Width}", new RenamePlanner().Build(
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Media_placeholders_use_supplied_tags()
|
||||
{
|
||||
var plan = new RenamePlanner().Build(
|
||||
[File(@"C:\a\clip.mp4")],
|
||||
new RenameRuleSet { Prefix = "{Width}_" }).Preview[0].NewName);
|
||||
new RenameRuleSet { Prefix = "{Width}_", NamePattern = "{Artist} - {Title}" },
|
||||
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[@"C:\a\clip.mp4"] = new()
|
||||
{
|
||||
Width = 1920,
|
||||
Artist = "Queen",
|
||||
Title = "Bohemian Rhapsody",
|
||||
Stem = "clip",
|
||||
Extension = "mp4"
|
||||
}
|
||||
});
|
||||
Assert.Equal("1920_Queen - Bohemian Rhapsody.mp4", plan.Preview[0].NewName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Online_only_media_placeholder_is_an_error()
|
||||
{
|
||||
var plan = new RenamePlanner().Build(
|
||||
[File(@"C:\cloud\clip.mp4")],
|
||||
new RenameRuleSet { Prefix = "{Artist}_" },
|
||||
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[@"C:\cloud\clip.mp4"] = new() { HydrationBlocked = true, Stem = "clip", Extension = "mp4" }
|
||||
});
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.Contains(plan.Issues, i => i.Message.Contains("online-only", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_and_taken_placeholders_expand()
|
||||
{
|
||||
var plan = new RenamePlanner().Build(
|
||||
[File(@"C:\src\Explorer\photos\DSC_001.jpg")],
|
||||
new RenameRuleSet { NamePattern = "{Project}_{TakenDate:yyyyMMdd}_{Name}" },
|
||||
tagsByPath: new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[@"C:\src\Explorer\photos\DSC_001.jpg"] = new()
|
||||
{
|
||||
Project = "Explorer",
|
||||
Taken = new DateTimeOffset(new DateTime(2024, 8, 26, 0, 0, 0, DateTimeKind.Local)),
|
||||
Stem = "DSC_001",
|
||||
Extension = "jpg"
|
||||
}
|
||||
});
|
||||
Assert.Equal("Explorer_20240826_DSC_001.jpg", plan.Preview[0].NewName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
36
tests/Explorer.Application.Tests/TagRenamePlannerTests.cs
Normal file
36
tests/Explorer.Application.Tests/TagRenamePlannerTests.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class TagRenamePlannerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Filename_to_tags_queues_a_write()
|
||||
{
|
||||
var plan = new TagRenamePlanner().BuildWrite(
|
||||
[new RenameSubject(@"C:\music\Pink Floyd - Time.mp3", "Pink Floyd - Time.mp3", false)],
|
||||
"{Artist} - {Title}",
|
||||
new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase));
|
||||
Assert.True(plan.CanEnqueue);
|
||||
Assert.Equal(TransferOp.WriteTags, plan.Operations[0].Op);
|
||||
var tags = MediaTagFields.FromPayload(plan.Operations[0].DestinationPath);
|
||||
Assert.Equal("Pink Floyd", tags.Artist);
|
||||
Assert.Equal("Time", tags.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unchanged_tags_are_not_queued()
|
||||
{
|
||||
var existing = new MediaTagFields { Artist = "Pink Floyd", Title = "Time" };
|
||||
var plan = new TagRenamePlanner().BuildWrite(
|
||||
[new RenameSubject(@"C:\music\Pink Floyd - Time.mp3", "Pink Floyd - Time.mp3", false)],
|
||||
"{Artist} - {Title}",
|
||||
new Dictionary<string, MediaTagFields>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[@"C:\music\Pink Floyd - Time.mp3"] = existing
|
||||
});
|
||||
Assert.False(plan.CanEnqueue);
|
||||
Assert.True(plan.TagPreview[0].TagsUnchanged);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,8 @@ public class UiPreferencesStoreTests
|
||||
Assert.Null(prefs.FfmpegPath);
|
||||
Assert.True(prefs.SessionTabs is null || prefs.SessionTabs.Count == 0);
|
||||
Assert.True(prefs.FavoriteFolders is null || prefs.FavoriteFolders.Count == 0);
|
||||
Assert.True(prefs.NamePatterns is null || prefs.NamePatterns.Count == 0);
|
||||
Assert.True(prefs.MoveToPatterns is null || prefs.MoveToPatterns.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -167,6 +169,31 @@ public class UiPreferencesStoreTests
|
||||
Assert.Equal(@"C:\Users\Dominique\Documents", prefs.FavoriteFolders[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_name_patterns_and_skips_built_ins()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(
|
||||
[
|
||||
"name-pattern={Album} - {Title}",
|
||||
"name-pattern={Album} - {Title}",
|
||||
"name-pattern={Artist} - {Title}",
|
||||
"name-pattern="
|
||||
]);
|
||||
Assert.NotNull(prefs.NamePatterns);
|
||||
Assert.Equal(["{Album} - {Title}"], prefs.NamePatterns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_move_to_patterns()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(
|
||||
[
|
||||
@"move-to=\\10.0.0.31\media\movies\%filename_noext%",
|
||||
@"move-to=\\10.0.0.31\media\movies\%filename_noext%"
|
||||
]);
|
||||
Assert.Equal([@"\\10.0.0.31\media\movies\%filename_noext%"], prefs.MoveToPatterns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_tab_roundtrip_escapes_semicolons_in_paths()
|
||||
{
|
||||
@@ -199,7 +226,9 @@ public class UiPreferencesStoreTests
|
||||
new SessionTabState(@"C:\Temp", @"D:\", true, 0.6, true)
|
||||
],
|
||||
SessionActiveTab: 0,
|
||||
PreferFavoritesInTree: true));
|
||||
PreferFavoritesInTree: true,
|
||||
NamePatterns: ["{Album} - {Title}"],
|
||||
MoveToPatterns: [@"\\10.0.0.31\media\movies\%filename_noext%"]));
|
||||
var loaded = store.Load();
|
||||
Assert.Equal("Light", loaded.Theme);
|
||||
Assert.True(loaded.GroupNetworkPlaces);
|
||||
@@ -218,6 +247,8 @@ public class UiPreferencesStoreTests
|
||||
Assert.Equal(720, loaded.WindowHeight);
|
||||
Assert.Equal(300, loaded.TreeWidth);
|
||||
Assert.Equal([@"D:\Photos", @"C:\Users\Dominique\Documents"], loaded.FavoriteFolders);
|
||||
Assert.Equal(["{Album} - {Title}"], loaded.NamePatterns);
|
||||
Assert.Equal([@"\\10.0.0.31\media\movies\%filename_noext%"], loaded.MoveToPatterns);
|
||||
Assert.NotNull(loaded.SessionTabs);
|
||||
var tab = Assert.Single(loaded.SessionTabs);
|
||||
Assert.Equal(@"C:\Temp", tab.LeftPath);
|
||||
|
||||
Reference in New Issue
Block a user