Added support for bittorrent
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using FluentAssertions;
|
||||
using PageManager.Api.Services;
|
||||
using PageManager.Api.Tests.Helpers;
|
||||
|
||||
namespace PageManager.Api.Tests.Unit.Services;
|
||||
|
||||
public class FileOrganizerServiceTests
|
||||
{
|
||||
// ── SanitizePathComponent ─────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("Andy Weir", "Andy Weir")]
|
||||
[InlineData("Brandon Sanderson", "Brandon Sanderson")]
|
||||
[InlineData("Hello: World", "Hello_ World")] // colon is invalid
|
||||
[InlineData("Title/Sub", "Title_Sub")] // slash is invalid
|
||||
[InlineData("Name.", "Name")] // trailing dot stripped
|
||||
[InlineData(" Name ", "Name")] // whitespace trimmed
|
||||
public void SanitizePathComponent_ReturnsExpected(string input, string expected)
|
||||
{
|
||||
FileOrganizerService.SanitizePathComponent(input).Should().Be(expected);
|
||||
}
|
||||
|
||||
// ── ComputeCanonicalRelativePath — ebooks ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Ebook_WithYear_ProducesAuthorTitleYearFolder()
|
||||
{
|
||||
var book = BookFactory.Create(title: "The Martian", year: 2011)
|
||||
.WithAuthors((1, "Andy Weir"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
result.Should().Be(Path.Combine("Andy Weir", "The Martian (2011)", "The Martian.epub"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ebook_WithoutYear_OmitsYearFromFolder()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", year: null)
|
||||
.WithAuthors((1, "Frank Herbert"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
result.Should().Be(Path.Combine("Frank Herbert", "Dune", "Dune.epub"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ebook_SeriesIgnored_SeriesNotInEbookPath()
|
||||
{
|
||||
var book = BookFactory.Create(title: "The Way of Kings", year: 2010)
|
||||
.WithAuthors((1, "Brandon Sanderson"))
|
||||
.WithSeries(seriesName: "The Stormlight Archive");
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
// Series folder should NOT appear for ebooks
|
||||
result.Should().Be(Path.Combine("Brandon Sanderson", "The Way of Kings (2010)", "The Way of Kings.epub"));
|
||||
}
|
||||
|
||||
// ── ComputeCanonicalRelativePath — audiobooks ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Audiobook_WithoutSeries_ProducesAuthorTitleTitle()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Project Hail Mary", year: 2021)
|
||||
.WithAuthors((1, "Andy Weir"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".m4b", isAudio: true);
|
||||
|
||||
result.Should().Be(Path.Combine("Andy Weir", "Project Hail Mary", "Project Hail Mary.m4b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Audiobook_WithSeries_InsertSeriesFolderBetweenAuthorAndTitle()
|
||||
{
|
||||
var book = BookFactory.Create(title: "The Way of Kings", year: 2010)
|
||||
.WithAuthors((1, "Brandon Sanderson"))
|
||||
.WithSeries(seriesName: "The Stormlight Archive");
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".m4b", isAudio: true);
|
||||
|
||||
result.Should().Be(Path.Combine("Brandon Sanderson", "The Stormlight Archive", "The Way of Kings", "The Way of Kings.m4b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Audiobook_Mp3Extension_PreservedInFilename()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", year: 1965)
|
||||
.WithAuthors((1, "Frank Herbert"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".mp3", isAudio: true);
|
||||
|
||||
result.Should().Be(Path.Combine("Frank Herbert", "Dune", "Dune.mp3"));
|
||||
}
|
||||
|
||||
// ── Multiple authors ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MultipleAuthors_UsesPrimaryAuthorForPath()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Good Omens", year: 1990)
|
||||
.WithAuthors((1, "Terry Pratchett"), (2, "Neil Gaiman"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
result.Should().StartWith("Terry Pratchett");
|
||||
result.Should().NotContain("Neil Gaiman");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoAuthors_UsesUnknownFolder()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Anonymous Work", year: 2000);
|
||||
// No authors added
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
result.Should().StartWith("Unknown");
|
||||
}
|
||||
|
||||
// ── Sanitization edge cases ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TitleWithInvalidChars_Sanitized()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Title: A Story", year: 2020)
|
||||
.WithAuthors((1, "Author Name"));
|
||||
|
||||
var result = FileOrganizerService.ComputeCanonicalRelativePath(book, ".epub", isAudio: false);
|
||||
|
||||
result.Should().Be(Path.Combine("Author Name", "Title_ A Story (2020)", "Title_ A Story.epub"));
|
||||
}
|
||||
}
|
||||
@@ -87,4 +87,39 @@ public class FileScannerServiceTests
|
||||
var act = () => FileScannerService.FindMatch(file, books);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindMatch_YearPrefixedFilename_StillMatchesTitle()
|
||||
{
|
||||
var books = new[] { BookFactory.Create(id: 1, title: "Project Hail Mary") };
|
||||
var file = new BookFile { Filename = "2021 - Project Hail Mary.epub", Path = "2021 - Project Hail Mary.epub" };
|
||||
|
||||
FileScannerService.FindMatch(file, books)!.Id.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindMatch_AudiobookInSubfolder_MatchesByParentFolderName()
|
||||
{
|
||||
var books = new[] { BookFactory.Create(id: 2, title: "Project Hail Mary") };
|
||||
// m4b lives at "Andy Weir/2021 - Project Hail Mary/Project Hail Mary.m4b"
|
||||
var file = new BookFile
|
||||
{
|
||||
Filename = "Project Hail Mary.m4b",
|
||||
Path = "Andy Weir/2021 - Project Hail Mary/Project Hail Mary.m4b",
|
||||
};
|
||||
|
||||
FileScannerService.FindMatch(file, books)!.Id.Should().Be(2);
|
||||
}
|
||||
|
||||
// ── StripYearPrefix ───────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("2021 - project hail mary", "project hail mary")]
|
||||
[InlineData("2015 - artemis", "artemis")]
|
||||
[InlineData("project hail mary", "project hail mary")] // no prefix → unchanged
|
||||
[InlineData("diary of an asscan", "diary of an asscan")]
|
||||
public void StripYearPrefix_ReturnsExpected(string input, string expected)
|
||||
{
|
||||
FileScannerService.StripYearPrefix(input).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
using FluentAssertions;
|
||||
using PageManager.Api.Services;
|
||||
using PageManager.Api.Tests.Helpers;
|
||||
|
||||
namespace PageManager.Api.Tests.Unit.Services;
|
||||
|
||||
public class MetadataWriterServiceTests
|
||||
{
|
||||
// ── ParseOpfPath ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ParseOpfPath_ReturnsFullPath()
|
||||
{
|
||||
var xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>
|
||||
""";
|
||||
|
||||
MetadataWriterService.ParseOpfPath(xml).Should().Be("OEBPS/content.opf");
|
||||
}
|
||||
|
||||
// ── ModifyOpfXml ──────────────────────────────────────────────────────────
|
||||
|
||||
private const string MinimalOpf = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="uid">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||
<dc:title>Old Title</dc:title>
|
||||
<dc:creator>Old Author</dc:creator>
|
||||
<dc:language>en</dc:language>
|
||||
</metadata>
|
||||
<manifest/>
|
||||
<spine/>
|
||||
</package>
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_UpdatesTitle()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", year: 1965).WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("<dc:title>Dune</dc:title>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_UpdatesCreator()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune").WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("Frank Herbert");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_AddsDescription_WhenPresent()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", description: "A sci-fi epic.").WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("A sci-fi epic.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_SkipsDescription_WhenNull()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", description: null).WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().NotContain("dc:description");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_AddsIsbnIdentifier_WhenPresent()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", isbn: "9780441013593").WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("9780441013593");
|
||||
result.Should().Contain("ISBN");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_AddsDate_WhenYearPresent()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune", year: 1965).WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("1965");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_PreservesExistingXmlStructure()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Dune").WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
|
||||
// Manifest and spine should still be present
|
||||
result.Should().Contain("manifest");
|
||||
result.Should().Contain("spine");
|
||||
result.Should().Contain("dc:language");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_WithMultipleAuthors_UsesPrimaryAuthorForCreator()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Good Omens")
|
||||
.WithAuthors((1, "Terry Pratchett"), (2, "Neil Gaiman"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
result.Should().Contain("Terry Pratchett");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_WithNoAuthors_DoesNotAddCreator()
|
||||
{
|
||||
var book = BookFactory.Create(title: "Anonymous");
|
||||
// No authors added
|
||||
var result = MetadataWriterService.ModifyOpfXml(MinimalOpf, book);
|
||||
// Should not crash and should not add empty creator
|
||||
result.Should().NotContain("<dc:creator />");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModifyOpfXml_UpdatesExistingIsbnIdentifier_RatherThanAdding()
|
||||
{
|
||||
var opfWithIsbn = MinimalOpf.Replace(
|
||||
"<dc:language>en</dc:language>",
|
||||
"<dc:language>en</dc:language>\n <dc:identifier opf:scheme=\"ISBN\">OLD-ISBN</dc:identifier>");
|
||||
|
||||
var book = BookFactory.Create(title: "Dune", isbn: "9780441013593").WithAuthors((1, "Frank Herbert"));
|
||||
var result = MetadataWriterService.ModifyOpfXml(opfWithIsbn, book);
|
||||
|
||||
result.Should().Contain("9780441013593");
|
||||
result.Should().NotContain("OLD-ISBN");
|
||||
// Should not have two ISBN identifiers
|
||||
result.Split("ISBN").Length.Should().BeLessThanOrEqualTo(3); // "ISBN" appears in scheme + value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user