From 7b68415b02d7c4d2386db40a08cf1e26dd7bfa34 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 8 Jan 2026 14:15:41 -0700 Subject: [PATCH] Add more properties to search engine and library export - Add `IsAudiblePlus` to search engine - Add `IsAudiblePlus` and `AbsentFromLastScan` properties to library export - Refactor library export ToXlsx method - Make nullable - Improve readability and extensability - Use same column header names as CSV - Extend export methods to accept optional list of books (future use) --- Source/ApplicationServices/ExportDto.cs | 142 +++++++ Source/ApplicationServices/LibraryExporter.cs | 355 +++--------------- Source/LibationSearchEngine/SearchEngine.cs | 1 + 3 files changed, 196 insertions(+), 302 deletions(-) create mode 100644 Source/ApplicationServices/ExportDto.cs diff --git a/Source/ApplicationServices/ExportDto.cs b/Source/ApplicationServices/ExportDto.cs new file mode 100644 index 00000000..7221deaf --- /dev/null +++ b/Source/ApplicationServices/ExportDto.cs @@ -0,0 +1,142 @@ +using CsvHelper.Configuration.Attributes; +using DataLayer; +using Newtonsoft.Json; +using System; +using System.Linq; + +#nullable enable +namespace ApplicationServices; + +internal class ExportDto(LibraryBook libBook) +{ + [Name("Account")] + public string Account { get; } = libBook.Account; + + [Name("Date Added to library")] + public DateTime DateAdded { get; } = libBook.DateAdded; + + [Name("Is Audible Plus?")] + public bool IsAudiblePlus { get; } = libBook.IsAudiblePlus; + + [Name("Absent from last scan?")] + public bool AbsentFromLastScan { get; } = libBook.AbsentFromLastScan; + + [Name("Audible Product Id")] + public string AudibleProductId { get; } = libBook.Book.AudibleProductId; + + [Name("Locale")] + public string Locale { get; } = libBook.Book.Locale; + + [Name("Title")] + public string Title { get; } = libBook.Book.Title; + + [Name("Subtitle")] + public string Subtitle { get; } = libBook.Book.Subtitle; + + [Name("Authors")] + public string AuthorNames { get; } = libBook.Book.AuthorNames; + + [Name("Narrators")] + public string NarratorNames { get; } = libBook.Book.NarratorNames; + + [Name("Length In Minutes")] + public int LengthInMinutes { get; } = libBook.Book.LengthInMinutes; + + [Name("Description")] + public string Description { get; } = libBook.Book.Description; + + [Name("Publisher")] + public string Publisher { get; } = libBook.Book.Publisher; + + [Name("Has PDF")] + public bool HasPdf { get; } = libBook.Book.HasPdf; + + [Name("Series Names")] + public string SeriesNames { get; } = libBook.Book.SeriesNames(); + + [Name("Series Order")] + public string SeriesOrder { get; } = libBook.Book.SeriesLink?.Any() is true ? string.Join(", ", libBook.Book.SeriesLink.Select(sl => $"{sl.Order} : {sl.Series.Name}")) : ""; + + [Name("Community Rating: Overall")] + public float? CommunityRatingOverall { get; } = ZeroIsNull(libBook.Book.Rating?.OverallRating); + + [Name("Community Rating: Performance")] + public float? CommunityRatingPerformance { get; } = ZeroIsNull(libBook.Book.Rating?.PerformanceRating); + + [Name("Community Rating: Story")] + public float? CommunityRatingStory { get; } = ZeroIsNull(libBook.Book.Rating?.StoryRating); + + [Name("Cover Id")] + public string PictureId { get; } = libBook.Book.PictureId; + + [Name("Cover Id Large")] + public string PictureLarge { get; } = libBook.Book.PictureLarge; + + [Name("Is Abridged?")] + public bool IsAbridged { get; } = libBook.Book.IsAbridged; + + [Name("Date Published")] + public DateTime? DatePublished { get; } = libBook.Book.DatePublished; + + [Name("Categories")] + public string CategoriesNames { get; } = string.Join("; ", libBook.Book.LowestCategoryNames()); + + [Name("My Rating: Overall")] + public float? MyRatingOverall { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.OverallRating); + + [Name("My Rating: Performance")] + public float? MyRatingPerformance { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.PerformanceRating); + + [Name("My Rating: Story")] + public float? MyRatingStory { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.StoryRating); + + [Name("My Libation Tags")] + public string MyLibationTags { get; } = libBook.Book.UserDefinedItem.Tags; + + [Name("Book Liberated Status")] + public string BookStatus { get; } = libBook.Book.UserDefinedItem.BookStatus.ToString(); + + [Name("PDF Liberated Status")] + public string? PdfStatus { get; } = libBook.Book.UserDefinedItem.PdfStatus.ToString(); + + [Name("Content Type")] + public string ContentType { get; } = libBook.Book.ContentType.ToString(); + + [Name("Language")] + public string Language { get; } = libBook.Book.Language; + + [Name("Last Downloaded")] + public DateTime? LastDownloaded { get; } = libBook.Book.UserDefinedItem.LastDownloaded; + + [Name("Last Downloaded Version")] + public string? LastDownloadedVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedVersion?.ToString(); + + [Name("Is Finished?")] + public bool IsFinished { get; } = libBook.Book.UserDefinedItem.IsFinished; + + [Name("Is Spatial?")] + public bool IsSpatial { get; } = libBook.Book.IsSpatial; + + [Name("Included Until")] + public DateTime? IncludedUntil { get; } = libBook.IncludedUntil; + + [Name("Last Downloaded File Version")] + public string? LastDownloadedFileVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedFileVersion; + + [Ignore /* csv ignore */] + public AudioFormat? LastDownloadedFormat { get; } = libBook.Book.UserDefinedItem.LastDownloadedFormat; + + [Name("Last Downloaded Codec"), JsonIgnore] + public string CodecString => LastDownloadedFormat?.CodecString ?? ""; + + [Name("Last Downloaded Sample rate"), JsonIgnore] + public int? SampleRate => LastDownloadedFormat?.SampleRate; + + [Name("Last Downloaded Audio Channels"), JsonIgnore] + public int? ChannelCount => LastDownloadedFormat?.ChannelCount; + + [Name("Last Downloaded Bitrate"), JsonIgnore] + public int? BitRate => LastDownloadedFormat?.BitRate; + + private static float? ZeroIsNull(float? value) => value is 0 ? null : value; +} diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index 20cbd2bf..afa73620 100644 --- a/Source/ApplicationServices/LibraryExporter.cs +++ b/Source/ApplicationServices/LibraryExporter.cs @@ -3,328 +3,79 @@ using CsvHelper; using CsvHelper.Configuration.Attributes; using DataLayer; using Newtonsoft.Json; -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection; -namespace ApplicationServices +#nullable enable +namespace ApplicationServices; + +public static class LibraryExporter { - public class ExportDto + public static void ToCsv(string saveFilePath, IEnumerable? libraryBooks = null) { - public static string GetName(string fieldName) - { - var property = typeof(ExportDto).GetProperty(fieldName); - var attribute = property.GetCustomAttributes(typeof(NameAttribute), true)[0]; - var description = (NameAttribute)attribute; - var text = description.Names; - return text[0]; - } + libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(); + var dtos = libraryBooks.ToDtos(); + if (dtos.Count == 0) + return; - [Name("Account")] - public string Account { get; set; } + using var csv = new CsvWriter(new System.IO.StreamWriter(saveFilePath), CultureInfo.CurrentCulture); + csv.WriteHeader(typeof(ExportDto)); + csv.NextRecord(); + csv.WriteRecords(dtos); } - [Name("Date Added to library")] - public DateTime DateAdded { get; set; } - - [Name("Audible Product Id")] - public string AudibleProductId { get; set; } - - [Name("Locale")] - public string Locale { get; set; } - - [Name("Title")] - public string Title { get; set; } - - [Name("Subtitle")] - public string Subtitle { get; set; } - - [Name("Authors")] - public string AuthorNames { get; set; } - - [Name("Narrators")] - public string NarratorNames { get; set; } - - [Name("Length In Minutes")] - public int LengthInMinutes { get; set; } - - [Name("Description")] - public string Description { get; set; } - - [Name("Publisher")] - public string Publisher { get; set; } - - [Name("Has PDF")] - public bool HasPdf { get; set; } - - [Name("Series Names")] - public string SeriesNames { get; set; } - - [Name("Series Order")] - public string SeriesOrder { get; set; } - - [Name("Community Rating: Overall")] - public float? CommunityRatingOverall { get; set; } - - [Name("Community Rating: Performance")] - public float? CommunityRatingPerformance { get; set; } - - [Name("Community Rating: Story")] - public float? CommunityRatingStory { get; set; } - - [Name("Cover Id")] - public string PictureId { get; set; } - - [Name("Is Abridged?")] - public bool IsAbridged { get; set; } - - [Name("Date Published")] - public DateTime? DatePublished { get; set; } - - [Name("Categories")] - public string CategoriesNames { get; set; } - - [Name("My Rating: Overall")] - public float? MyRatingOverall { get; set; } - - [Name("My Rating: Performance")] - public float? MyRatingPerformance { get; set; } - - [Name("My Rating: Story")] - public float? MyRatingStory { get; set; } - - [Name("My Libation Tags")] - public string MyLibationTags { get; set; } - - [Name("Book Liberated Status")] - public string BookStatus { get; set; } - - [Name("PDF Liberated Status")] - public string PdfStatus { get; set; } - - [Name("Content Type")] - public string ContentType { get; set; } - - [Name("Language")] - public string Language { get; set; } - - [Name("Last Downloaded")] - public DateTime? LastDownloaded { get; set; } - - [Name("Last Downloaded Version")] - public string LastDownloadedVersion { get; set; } - - [Name("Is Finished?")] - public bool IsFinished { get; set; } - - [Name("Is Spatial?")] - public bool IsSpatial { get; set; } - - [Name("Included Until")] - public DateTime? IncludedUntil { get; set; } - - [Name("Last Downloaded File Version")] - public string LastDownloadedFileVersion { get; set; } - - [Ignore /* csv ignore */] - public AudioFormat LastDownloadedFormat { get; set; } - - [Name("Last Downloaded Codec"), JsonIgnore] - public string CodecString => LastDownloadedFormat?.CodecString ?? ""; - - [Name("Last Downloaded Sample rate"), JsonIgnore] - public int? SampleRate => LastDownloadedFormat?.SampleRate; - - [Name("Last Downloaded Audio Channels"), JsonIgnore] - public int? ChannelCount => LastDownloadedFormat?.ChannelCount; - - [Name("Last Downloaded Bitrate"), JsonIgnore] - public int? BitRate => LastDownloadedFormat?.BitRate; + public static void ToJson(string saveFilePath, IEnumerable? libraryBooks = null) + { + libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(); + var dtos = libraryBooks.ToDtos(); + var serializer = new JsonSerializer(); + using var writer = new JsonTextWriter(new System.IO.StreamWriter(saveFilePath)) { Formatting = Formatting.Indented }; + serializer.Serialize(writer, dtos); } - public static class LibToDtos + public static void ToXlsx(string saveFilePath, IEnumerable? libraryBooks = null) { - public static List ToDtos(this IEnumerable library) - => library.Select(a => new ExportDto - { - Account = a.Account, - DateAdded = a.DateAdded, - AudibleProductId = a.Book.AudibleProductId, - Locale = a.Book.Locale, - Title = a.Book.Title, - Subtitle = a.Book.Subtitle, - AuthorNames = a.Book.AuthorNames, - NarratorNames = a.Book.NarratorNames, - LengthInMinutes = a.Book.LengthInMinutes, - Description = a.Book.Description, - Publisher = a.Book.Publisher, - HasPdf = a.Book.HasPdf, - SeriesNames = a.Book.SeriesNames(), - SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "", - CommunityRatingOverall = a.Book.Rating?.OverallRating.ZeroIsNull(), - CommunityRatingPerformance = a.Book.Rating?.PerformanceRating.ZeroIsNull(), - CommunityRatingStory = a.Book.Rating?.StoryRating.ZeroIsNull(), - PictureId = a.Book.PictureId, - IsAbridged = a.Book.IsAbridged, - DatePublished = a.Book.DatePublished, - CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()), - MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating.ZeroIsNull(), - MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating.ZeroIsNull(), - MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating.ZeroIsNull(), - MyLibationTags = a.Book.UserDefinedItem.Tags, - BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(), - PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(), - ContentType = a.Book.ContentType.ToString(), - Language = a.Book.Language, - LastDownloaded = a.Book.UserDefinedItem.LastDownloaded, - LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "", - IsFinished = a.Book.UserDefinedItem.IsFinished, - IsSpatial = a.Book.IsSpatial, - IncludedUntil = a.IncludedUntil, - LastDownloadedFileVersion = a.Book.UserDefinedItem.LastDownloadedFileVersion ?? "", - LastDownloadedFormat = a.Book.UserDefinedItem.LastDownloadedFormat - }).ToList(); + libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(); + var dtos = libraryBooks.ToDtos(); - private static float? ZeroIsNull(this float value) => value is 0 ? null : value; - } - public static class LibraryExporter - { - public static void ToCsv(string saveFilePath) + using var workbook = new XLWorkbook(); + var sheet = workbook.AddWorksheet("Library"); + var columns = typeof(ExportDto).GetProperties().Where(p => p.GetCustomAttribute() is not null).ToArray(); + + // headers + var currentRow = sheet.FirstRow(); + var currentCell = currentRow.FirstCell(); + foreach (var column in columns) { - var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos(); - if (!dtos.Any()) - return; - using var writer = new System.IO.StreamWriter(saveFilePath); - using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture); - - csv.WriteHeader(typeof(ExportDto)); - csv.NextRecord(); - csv.WriteRecords(dtos); + currentCell.Value = GetColumnName(column); + currentCell.Style.Font.Bold = true; + currentCell = currentCell.CellRight(); } - public static void ToJson(string saveFilePath) + var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss"; + + // Add data rows + foreach (var dto in dtos) { - var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos(); - var json = JsonConvert.SerializeObject(dtos, Formatting.Indented); - System.IO.File.WriteAllText(saveFilePath, json); - } + currentRow = currentRow.RowBelow(); + currentCell = currentRow.FirstCell(); - public static void ToXlsx(string saveFilePath) - { - var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos(); - - using var workbook = new XLWorkbook(); - var sheet = workbook.AddWorksheet("Library"); - - - // headers - var columns = new[] { - nameof(ExportDto.Account), - nameof(ExportDto.DateAdded), - nameof(ExportDto.AudibleProductId), - nameof(ExportDto.Locale), - nameof(ExportDto.Title), - nameof(ExportDto.Subtitle), - nameof(ExportDto.AuthorNames), - nameof(ExportDto.NarratorNames), - nameof(ExportDto.LengthInMinutes), - nameof(ExportDto.Description), - nameof(ExportDto.Publisher), - nameof(ExportDto.HasPdf), - nameof(ExportDto.SeriesNames), - nameof(ExportDto.SeriesOrder), - nameof(ExportDto.CommunityRatingOverall), - nameof(ExportDto.CommunityRatingPerformance), - nameof(ExportDto.CommunityRatingStory), - nameof(ExportDto.PictureId), - nameof(ExportDto.IsAbridged), - nameof(ExportDto.DatePublished), - nameof(ExportDto.CategoriesNames), - nameof(ExportDto.MyRatingOverall), - nameof(ExportDto.MyRatingPerformance), - nameof(ExportDto.MyRatingStory), - nameof(ExportDto.MyLibationTags), - nameof(ExportDto.BookStatus), - nameof(ExportDto.PdfStatus), - nameof(ExportDto.ContentType), - nameof(ExportDto.Language), - nameof(ExportDto.LastDownloaded), - nameof(ExportDto.LastDownloadedVersion), - nameof(ExportDto.IsFinished), - nameof(ExportDto.IsSpatial), - nameof(ExportDto.IncludedUntil), - nameof(ExportDto.LastDownloadedFileVersion), - nameof(ExportDto.CodecString), - nameof(ExportDto.SampleRate), - nameof(ExportDto.ChannelCount), - nameof(ExportDto.BitRate) - }; - - int rowIndex = 1, col = 1; - var headerRow = sheet.Row(rowIndex++); - foreach (var c in columns) + foreach (var column in columns) { - var headerCell = headerRow.Cell(col++); - headerCell.Value = ExportDto.GetName(c); - headerCell.Style.Font.Bold = true; + var value = column.GetValue(dto); + currentCell.Value = XLCellValue.FromObject(value); + currentCell.Style.DateFormat.Format = currentCell.DataType is XLDataType.DateTime ? dateFormat : string.Empty; + currentCell = currentCell.CellRight(); } - - var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss"; - - // Add data rows - foreach (var dto in dtos) - { - col = 1; - var row = sheet.Row(rowIndex++); - - row.Cell(col++).Value = dto.Account; - row.Cell(col++).SetDate(dto.DateAdded, dateFormat); - row.Cell(col++).Value = dto.AudibleProductId; - row.Cell(col++).Value = dto.Locale; - row.Cell(col++).Value = dto.Title; - row.Cell(col++).Value = dto.Subtitle; - row.Cell(col++).Value = dto.AuthorNames; - row.Cell(col++).Value = dto.NarratorNames; - row.Cell(col++).Value = dto.LengthInMinutes; - row.Cell(col++).Value = dto.Description; - row.Cell(col++).Value = dto.Publisher; - row.Cell(col++).Value = dto.HasPdf; - row.Cell(col++).Value = dto.SeriesNames; - row.Cell(col++).Value = dto.SeriesOrder; - row.Cell(col++).Value = dto.CommunityRatingOverall; - row.Cell(col++).Value = dto.CommunityRatingPerformance; - row.Cell(col++).Value = dto.CommunityRatingStory; - row.Cell(col++).Value = dto.PictureId; - row.Cell(col++).Value = dto.IsAbridged; - row.Cell(col++).SetDate(dto.DatePublished, dateFormat); - row.Cell(col++).Value = dto.CategoriesNames; - row.Cell(col++).Value = dto.MyRatingOverall; - row.Cell(col++).Value = dto.MyRatingPerformance; - row.Cell(col++).Value = dto.MyRatingStory; - row.Cell(col++).Value = dto.MyLibationTags; - row.Cell(col++).Value = dto.BookStatus; - row.Cell(col++).Value = dto.PdfStatus; - row.Cell(col++).Value = dto.ContentType; - row.Cell(col++).Value = dto.Language; - row.Cell(col++).SetDate(dto.LastDownloaded, dateFormat); - row.Cell(col++).Value = dto.LastDownloadedVersion; - row.Cell(col++).Value = dto.IsFinished; - row.Cell(col++).Value = dto.IsSpatial; - row.Cell(col++).Value = dto.IncludedUntil; - row.Cell(col++).Value = dto.LastDownloadedFileVersion; - row.Cell(col++).Value = dto.CodecString; - row.Cell(col++).Value = dto.SampleRate; - row.Cell(col++).Value = dto.ChannelCount; - row.Cell(col++).Value = dto.BitRate; - } - - workbook.SaveAs(saveFilePath); } - private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat) - { - cell.Value = value; - cell.Style.DateFormat.Format = dateFormat; - } + workbook.SaveAs(saveFilePath); } + + private static List ToDtos(this IEnumerable library) + => library.Select(a => new ExportDto(a)).ToList(); + + private static string GetColumnName(PropertyInfo property) + => property.GetCustomAttribute()?.Names?.FirstOrDefault() ?? property.Name; } diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index 5f2bede1..d7ad3056 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -59,6 +59,7 @@ namespace LibationSearchEngine { FieldType.Bool, lb => lb.AbsentFromLastScan.ToString(), "AbsentFromLastScan", "Absent" }, { FieldType.Bool, lb => (!string.IsNullOrWhiteSpace(lb.Book.SeriesNames())).ToString(), "IsInSeries", "InSeries" }, { FieldType.Bool, lb => lb.Book.UserDefinedItem.IsFinished.ToString(), nameof(UserDefinedItem.IsFinished), "Finished", "IsFinished" }, + { FieldType.Bool, lb => lb.IsAudiblePlus.ToString(), nameof(LibraryBook.IsAudiblePlus), "AudiblePlus", "Plus" }, // all numbers are padded to 8 char.s // This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd { FieldType.Number, lb => lb.Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" },