From b7cd7b20e89ac0081c93c49ce837fd86e3263f25 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 21 Dec 2025 19:51:44 -0600 Subject: [PATCH] feat(database): add Book and Audiobook entities (#122) Add entities for books and audiobooks with: - Book entity: title, ISBN, ASIN, publisher, author/series links - Audiobook entity: title, narrator, duration, abridged flag, book link - Database migration 246 for Books and Audiobooks tables - Entity registrations in TableMapping Note: Depends on PR #118 (Author/Series tables) for full FK support. Co-authored-by: admin --- src/NzbDrone.Core/Audiobooks/Audiobook.cs | 52 +++++++++++++++ src/NzbDrone.Core/Books/Book.cs | 46 ++++++++++++++ .../246_add_books_audiobooks_tables.cs | 63 +++++++++++++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 6 ++ 4 files changed, 167 insertions(+) create mode 100644 src/NzbDrone.Core/Audiobooks/Audiobook.cs create mode 100644 src/NzbDrone.Core/Books/Book.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/246_add_books_audiobooks_tables.cs diff --git a/src/NzbDrone.Core/Audiobooks/Audiobook.cs b/src/NzbDrone.Core/Audiobooks/Audiobook.cs new file mode 100644 index 0000000000..aa3c969dea --- /dev/null +++ b/src/NzbDrone.Core/Audiobooks/Audiobook.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaTypes; + +namespace NzbDrone.Core.Audiobooks +{ + public class Audiobook : ModelBase + { + public Audiobook() + { + Tags = new HashSet(); + MediaType = MediaType.Audiobook; + } + + public string Title { get; set; } + public string SortTitle { get; set; } + public string Description { get; set; } + public string ForeignAudiobookId { get; set; } + public string Isbn { get; set; } + public string Isbn13 { get; set; } + public string Asin { get; set; } + public DateTime? ReleaseDate { get; set; } + public string Publisher { get; set; } + public string Language { get; set; } + + public string Narrator { get; set; } + public int? DurationMinutes { get; set; } + public bool IsAbridged { get; set; } + + public MediaType MediaType { get; set; } + public bool Monitored { get; set; } + public int QualityProfileId { get; set; } + public string Path { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public HashSet Tags { get; set; } + public DateTime? LastSearchTime { get; set; } + + public int? AuthorId { get; set; } + public int? SeriesId { get; set; } + public int? SeriesPosition { get; set; } + + public int? BookId { get; set; } + + public override string ToString() + { + var narratorInfo = string.IsNullOrEmpty(Narrator) ? "" : $" - Narrated by {Narrator}"; + return $"{Title} ({ReleaseDate?.Year}){narratorInfo}"; + } + } +} diff --git a/src/NzbDrone.Core/Books/Book.cs b/src/NzbDrone.Core/Books/Book.cs new file mode 100644 index 0000000000..ef56362ac0 --- /dev/null +++ b/src/NzbDrone.Core/Books/Book.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaTypes; + +namespace NzbDrone.Core.Books +{ + public class Book : ModelBase + { + public Book() + { + Tags = new HashSet(); + MediaType = MediaType.Book; + } + + public string Title { get; set; } + public string SortTitle { get; set; } + public string Description { get; set; } + public string ForeignBookId { get; set; } + public string Isbn { get; set; } + public string Isbn13 { get; set; } + public string Asin { get; set; } + public int? PageCount { get; set; } + public DateTime? ReleaseDate { get; set; } + public string Publisher { get; set; } + public string Language { get; set; } + + public MediaType MediaType { get; set; } + public bool Monitored { get; set; } + public int QualityProfileId { get; set; } + public string Path { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public HashSet Tags { get; set; } + public DateTime? LastSearchTime { get; set; } + + public int? AuthorId { get; set; } + public int? SeriesId { get; set; } + public int? SeriesPosition { get; set; } + + public override string ToString() + { + return $"{Title} ({ReleaseDate?.Year})"; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/246_add_books_audiobooks_tables.cs b/src/NzbDrone.Core/Datastore/Migration/246_add_books_audiobooks_tables.cs new file mode 100644 index 0000000000..c111e22c5b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/246_add_books_audiobooks_tables.cs @@ -0,0 +1,63 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(246)] + public class add_books_audiobooks_tables : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Books") + .WithColumn("Title").AsString().NotNullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("Description").AsString().Nullable() + .WithColumn("ForeignBookId").AsString().Nullable() + .WithColumn("Isbn").AsString().Nullable() + .WithColumn("Isbn13").AsString().Nullable() + .WithColumn("Asin").AsString().Nullable() + .WithColumn("PageCount").AsInt32().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() + .WithColumn("Publisher").AsString().Nullable() + .WithColumn("Language").AsString().Nullable() + .WithColumn("MediaType").AsInt32().WithDefaultValue(4) + .WithColumn("Monitored").AsBoolean().WithDefaultValue(false) + .WithColumn("QualityProfileId").AsInt32().WithDefaultValue(0) + .WithColumn("Path").AsString().Nullable() + .WithColumn("RootFolderPath").AsString().Nullable() + .WithColumn("Added").AsDateTime().WithDefaultValue(System.DateTime.UtcNow) + .WithColumn("Tags").AsString().WithDefaultValue("[]") + .WithColumn("LastSearchTime").AsDateTime().Nullable() + .WithColumn("AuthorId").AsInt32().Nullable() + .WithColumn("SeriesId").AsInt32().Nullable() + .WithColumn("SeriesPosition").AsInt32().Nullable(); + + Create.TableForModel("Audiobooks") + .WithColumn("Title").AsString().NotNullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("Description").AsString().Nullable() + .WithColumn("ForeignAudiobookId").AsString().Nullable() + .WithColumn("Isbn").AsString().Nullable() + .WithColumn("Isbn13").AsString().Nullable() + .WithColumn("Asin").AsString().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() + .WithColumn("Publisher").AsString().Nullable() + .WithColumn("Language").AsString().Nullable() + .WithColumn("Narrator").AsString().Nullable() + .WithColumn("DurationMinutes").AsInt32().Nullable() + .WithColumn("IsAbridged").AsBoolean().WithDefaultValue(false) + .WithColumn("MediaType").AsInt32().WithDefaultValue(5) + .WithColumn("Monitored").AsBoolean().WithDefaultValue(false) + .WithColumn("QualityProfileId").AsInt32().WithDefaultValue(0) + .WithColumn("Path").AsString().Nullable() + .WithColumn("RootFolderPath").AsString().Nullable() + .WithColumn("Added").AsDateTime().WithDefaultValue(System.DateTime.UtcNow) + .WithColumn("Tags").AsString().WithDefaultValue("[]") + .WithColumn("LastSearchTime").AsDateTime().Nullable() + .WithColumn("AuthorId").AsInt32().Nullable() + .WithColumn("SeriesId").AsInt32().Nullable() + .WithColumn("SeriesPosition").AsInt32().Nullable() + .WithColumn("BookId").AsInt32().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f8bcfc2a61..78ce877b59 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -3,10 +3,12 @@ using System.Linq; using Dapper; using NzbDrone.Common.Reflection; +using NzbDrone.Core.Audiobooks; using NzbDrone.Core.Authentication; using NzbDrone.Core.Authors; using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Blocklisting; +using NzbDrone.Core.Books; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFilters; using NzbDrone.Core.CustomFormats; @@ -142,6 +144,10 @@ public static void Map() Mapper.Entity("ImportExclusions").RegisterModel(); + Mapper.Entity("Books").RegisterModel(); + + Mapper.Entity("Audiobooks").RegisterModel(); + Mapper.Entity("QualityDefinitions").RegisterModel() .Ignore(d => d.GroupName) .Ignore(d => d.Weight);