diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index b1961b6e4..4b4e615e3 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -79,6 +79,12 @@ const bookTokens = [ { token: '{Book Disambiguation}', example: 'Disambiguation' }, + { token: '{Book Series}', example: 'Series Title' }, + + { token: '{Book SeriesPosition}', example: '1' }, + + { token: '{Book SeriesTitle}', example: 'Series Title #1' }, + { token: '{PartNumber:0}', example: '2' }, { token: '{PartNumber:00}', example: '02' }, { token: '{PartCount:0}', example: '9' }, diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs index 7508b63ef..2f8c8c996 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs @@ -28,10 +28,23 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .With(s => s.Name = "Avenged Sevenfold") .Build(); + var series = Builder + .CreateNew() + .With(x => x.Title = "Series Title") + .Build(); + + var seriesLink = Builder + .CreateListOfSize(1) + .All() + .With(s => s.Position = "1-2") + .With(s => s.Series = series) + .BuildListOfNew(); + _book = Builder .CreateNew() .With(s => s.Title = "Hail to the King") .With(s => s.AuthorMetadata = _author.Metadata.Value) + .With(s => s.SeriesLinks = seriesLink) .Build(); _edition = Builder diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 6a8da1822..390d7e89b 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -35,10 +35,23 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests }) .Build(); + var series = Builder + .CreateNew() + .With(x => x.Title = "Series Title") + .Build(); + + var seriesLink = Builder + .CreateListOfSize(1) + .All() + .With(s => s.Position = "1-2") + .With(s => s.Series = series) + .BuildListOfNew(); + _book = Builder .CreateNew() .With(s => s.Title = "Hybrid Theory") .With(s => s.AuthorMetadata = _author.Metadata.Value) + .With(s => s.SeriesLinks = seriesLink) .Build(); _edition = Builder @@ -247,6 +260,33 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be("Hybrid.Theory.2000"); } + [Test] + public void should_set_series() + { + _namingConfig.StandardBookFormat = "{Book Series}"; + + Subject.BuildBookFileName(_author, _edition, _trackFile) + .Should().Be("Series Title"); + } + + [Test] + public void should_set_series_number() + { + _namingConfig.StandardBookFormat = "{Book SeriesPosition}"; + + Subject.BuildBookFileName(_author, _edition, _trackFile) + .Should().Be("1-2"); + } + + [Test] + public void should_set_series_title() + { + _namingConfig.StandardBookFormat = "{Book SeriesTitle}"; + + Subject.BuildBookFileName(_author, _edition, _trackFile) + .Should().Be("Series Title #1-2"); + } + [Test] public void should_set_part_number() { @@ -434,7 +474,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardBookFormat = "{Author.Name}.{Book.Title}"; - Subject.BuildBookFileName(new Author { Name = "In The Woods." }, new Edition { Title = "30 Rock", Book = new Book { AuthorMetadata = new AuthorMetadata { Name = "Author" } } }, _trackFile) + Subject.BuildBookFileName(new Author { Name = "In The Woods." }, new Edition { Title = "30 Rock", Book = new Book { AuthorMetadata = new AuthorMetadata { Name = "Author" }, SeriesLinks = new List() } }, _trackFile) .Should().Be("In.The.Woods.30.Rock"); } @@ -443,7 +483,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardBookFormat = "{Author.Name}.{Book.Title}"; - Subject.BuildBookFileName(new Author { Name = "In The Woods..." }, new Edition { Title = "30 Rock", Book = new Book { AuthorMetadata = new AuthorMetadata { Name = "Author" } } }, _trackFile) + Subject.BuildBookFileName(new Author { Name = "In The Woods..." }, new Edition { Title = "30 Rock", Book = new Book { AuthorMetadata = new AuthorMetadata { Name = "Author" }, SeriesLinks = new List() } }, _trackFile) .Should().Be("In.The.Woods.30.Rock"); } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs index 9e01cfbc4..c94c501e1 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs @@ -28,10 +28,23 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .With(s => s.Name = "Alien Ant Farm") .Build(); + var series = Builder + .CreateNew() + .With(x => x.Title = "Series Title") + .Build(); + + var seriesLink = Builder + .CreateListOfSize(1) + .All() + .With(s => s.Position = "1-2") + .With(s => s.Series = series) + .BuildListOfNew(); + _book = Builder .CreateNew() .With(s => s.Title = "Anthology") .With(s => s.AuthorMetadata = _author.Metadata.Value) + .With(s => s.SeriesLinks = seriesLink) .Build(); _edition = Builder diff --git a/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs b/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs index 3cbdefff8..b22149b36 100644 --- a/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs +++ b/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Core.Books public class SeriesBookLink : Entity { public string Position { get; set; } + public int SeriesPosition { get; set; } public int SeriesId { get; set; } public int BookId { get; set; } public bool IsPrimary { get; set; } @@ -18,6 +19,7 @@ namespace NzbDrone.Core.Books public override void UseMetadataFrom(SeriesBookLink other) { Position = other.Position; + SeriesPosition = other.SeriesPosition; IsPrimary = other.IsPrimary; } diff --git a/src/NzbDrone.Core/Datastore/Migration/018_add_series_position.cs b/src/NzbDrone.Core/Datastore/Migration/018_add_series_position.cs new file mode 100644 index 000000000..fcc69c6a7 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/018_add_series_position.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(18)] + public class AddSeriesPosition : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("SeriesBookLink").AddColumn("SeriesPosition").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs b/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs index e6afb317f..0382220f1 100644 --- a/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs @@ -196,7 +196,8 @@ namespace NzbDrone.Core.MetadataSource.BookInfo Book = bookDict[l.ForeignWorkId.ToString()], Series = curr, IsPrimary = l.Primary, - Position = l.PositionInSeries + Position = l.PositionInSeries, + SeriesPosition = l.SeriesPosition }).ToList(); } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 0d3043fab..1d59b0e74 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -245,6 +245,17 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Book CleanSubtitle}"] = m => CleanTitle(subtitle); tokenHandlers["{Book SubtitleThe}"] = m => TitleThe(subtitle); + var seriesLinks = edition.Book.Value.SeriesLinks.Value; + if (seriesLinks.Any()) + { + var primarySeries = seriesLinks.OrderBy(x => x.SeriesPosition).First(); + var seriesTitle = primarySeries.Series?.Value?.Title + (primarySeries.Position.IsNotNullOrWhiteSpace() ? $" #{primarySeries.Position}" : string.Empty); + + tokenHandlers["{Book Series}"] = m => primarySeries.Series.Value.Title; + tokenHandlers["{Book SeriesPosition}"] = m => primarySeries.Position; + tokenHandlers["{Book SeriesTitle}"] = m => seriesTitle; + } + if (edition.Disambiguation != null) { tokenHandlers["{Book Disambiguation}"] = m => edition.Disambiguation; diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 57c5fdc76..51cd1429b 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -37,12 +37,24 @@ namespace NzbDrone.Core.Organizer } }; + var series = new Series + { + Title = "Series Title" + }; + + var seriesLink = new SeriesBookLink + { + Position = "1", + Series = series + }; + _standardBook = new Book { Title = "The Book Title", ReleaseDate = System.DateTime.Today, Author = _standardAuthor, - AuthorMetadata = _standardAuthor.Metadata.Value + AuthorMetadata = _standardAuthor.Metadata.Value, + SeriesLinks = new List { seriesLink } }; _standardEdition = new Edition diff --git a/src/Readarr.Api.V1/Books/BookResource.cs b/src/Readarr.Api.V1/Books/BookResource.cs index ca5129bbc..35c576b4c 100644 --- a/src/Readarr.Api.V1/Books/BookResource.cs +++ b/src/Readarr.Api.V1/Books/BookResource.cs @@ -54,6 +54,9 @@ namespace Readarr.Api.V1.Books var title = selectedEdition?.Title ?? model.Title; var authorTitle = $"{model.Author?.Value?.Metadata?.Value?.SortNameLastFirst} {title}"; + var seriesLinks = model.SeriesLinks?.Value?.OrderBy(x => x.SeriesPosition); + var seriesTitle = seriesLinks?.Select(x => x?.Series?.Value?.Title + (x?.Position.IsNotNullOrWhiteSpace() ?? false ? $" #{x.Position}" : string.Empty)).ConcatToString("; "); + return new BookResource { Id = model.Id, @@ -67,7 +70,7 @@ namespace Readarr.Api.V1.Books Genres = model.Genres, Title = title, AuthorTitle = authorTitle, - SeriesTitle = model.SeriesLinks?.Value?.Select(x => x?.Series?.Value?.Title + (x?.Position.IsNotNullOrWhiteSpace() ?? false ? $" #{x.Position}" : string.Empty)).ConcatToString("; "), + SeriesTitle = seriesTitle, Disambiguation = selectedEdition?.Disambiguation, Overview = selectedEdition?.Overview, Images = selectedEdition?.Images ?? new List(), diff --git a/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs b/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs index 2fbb04a9b..82417ea0b 100644 --- a/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs +++ b/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs @@ -8,6 +8,7 @@ namespace Readarr.Api.V1.Series public class SeriesBookLinkResource : RestResource { public string Position { get; set; } + public int SeriesPosition { get; set; } public int SeriesId { get; set; } public int BookId { get; set; } } @@ -20,6 +21,7 @@ namespace Readarr.Api.V1.Series { Id = model.Id, Position = model.Position, + SeriesPosition = model.SeriesPosition, SeriesId = model.SeriesId, BookId = model.BookId };