diff --git a/.gitignore b/.gitignore index 787caf71..7ab9c6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -184,7 +184,7 @@ publish/ *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted -*.pubxml +#*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to diff --git a/Source/.config/dotnet-tools.json b/Source/.config/dotnet-tools.json new file mode 100644 index 00000000..921204ae --- /dev/null +++ b/Source/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "frogvall.dotnetbumpversion": { + "version": "3.0.1", + "commands": [ + "bump-version" + ] + } + } +} \ No newline at end of file diff --git a/Source/AaxDecrypter/AaxDecrypter.csproj b/Source/AaxDecrypter/AaxDecrypter.csproj index 90fdc76d..84bfe99c 100644 --- a/Source/AaxDecrypter/AaxDecrypter.csproj +++ b/Source/AaxDecrypter/AaxDecrypter.csproj @@ -8,7 +8,16 @@ - + + embedded + + + + embedded + + + + diff --git a/Source/AppScaffolding/.msbump b/Source/AppScaffolding/.msbump deleted file mode 100644 index e9c0b591..00000000 --- a/Source/AppScaffolding/.msbump +++ /dev/null @@ -1,4 +0,0 @@ -{ - "//": "https://github.com/BalassaMarton/MSBump", - BumpRevision: true -} diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index 8e17cbea..67f36a31 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -1,22 +1,23 @@ - + - net6.0-windows - 8.1.4.1 + 8.1.4.31 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + embedded + + + embedded + + + + + \ No newline at end of file diff --git a/Source/AppScaffolding/UNSAFE_MigrationHelper.cs b/Source/AppScaffolding/UNSAFE_MigrationHelper.cs index 61af7aac..fbfe6bb2 100644 --- a/Source/AppScaffolding/UNSAFE_MigrationHelper.cs +++ b/Source/AppScaffolding/UNSAFE_MigrationHelper.cs @@ -25,7 +25,7 @@ namespace AppScaffolding : value; #region appsettings.json - private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json"); + private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), "appsettings.json"); public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON); diff --git a/Source/ApplicationServices/ApplicationServices.csproj b/Source/ApplicationServices/ApplicationServices.csproj index b2c3abea..cb4528e5 100644 --- a/Source/ApplicationServices/ApplicationServices.csproj +++ b/Source/ApplicationServices/ApplicationServices.csproj @@ -1,4 +1,4 @@ - + net6.0-windows @@ -14,4 +14,12 @@ + + embedded + + + + embedded + + diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 28fbd14f..35568c55 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -162,10 +162,19 @@ namespace AudibleUtilities if (exceptions is not null && exceptions.Any()) throw new AggregateException(exceptions); } - return items; } + private static List getValidators() + { + var type = typeof(IValidator); + var types = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(s => s.GetTypes()) + .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); + + return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList(); + } + #region episodes and podcasts private async Task> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent) @@ -197,7 +206,8 @@ namespace AudibleUtilities if (numSeriesParents != 1) { //There should only ever be 1 top-level parent per episode. If not, log - //and throw so we can figure out what to do about those special cases. + //so we can figure out what to do about those special cases, and don't + //import the episode. JsonSerializerSettings Settings = new() { MetadataPropertyHandling = MetadataPropertyHandling.Ignore, @@ -207,9 +217,8 @@ namespace AudibleUtilities new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } }, }; - var ex = new ApplicationException($"Found {numSeriesParents} parents for {parent.Asin}"); - Serilog.Log.Logger.Error(ex, $"Episode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}"); - throw ex; + Serilog.Log.Logger.Error($"Found {numSeriesParents} parents for {parent.Asin}\r\nEpisode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}"); + return new List(); } var realParent = seriesParents.Single(p => p.IsSeriesParent); @@ -329,15 +338,5 @@ namespace AudibleUtilities return results; } #endregion - - private static List getValidators() - { - var type = typeof(IValidator); - var types = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(s => s.GetTypes()) - .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); - - return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList(); - } } } diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index 1342b852..3ebae93c 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -11,5 +11,13 @@ + + + embedded + + + + embedded + diff --git a/Source/DataLayer/Configurations/BookConfig.cs b/Source/DataLayer/Configurations/BookConfig.cs index 88a92a46..17054ea7 100644 --- a/Source/DataLayer/Configurations/BookConfig.cs +++ b/Source/DataLayer/Configurations/BookConfig.cs @@ -12,11 +12,13 @@ namespace DataLayer.Configurations entity.OwnsOne(b => b.Rating); + entity.Property(nameof(Book._audioFormat)); // // CRUCIAL: ignore unmapped collections, even get-only // entity.Ignore(nameof(Book.Authors)); entity.Ignore(nameof(Book.Narrators)); + entity.Ignore(nameof(Book.AudioFormat)); //// these don't seem to matter //entity.Ignore(nameof(Book.AuthorNames)); //entity.Ignore(nameof(Book.NarratorNames)); diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj index 1690e74f..87bd7c77 100644 --- a/Source/DataLayer/DataLayer.csproj +++ b/Source/DataLayer/DataLayer.csproj @@ -6,9 +6,7 @@ true - Library - @@ -23,8 +21,16 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + embedded + + + + embedded + + + diff --git a/Source/DataLayer/EfClasses/AudioFormat.cs b/Source/DataLayer/EfClasses/AudioFormat.cs new file mode 100644 index 00000000..2d2f9d19 --- /dev/null +++ b/Source/DataLayer/EfClasses/AudioFormat.cs @@ -0,0 +1,62 @@ +using System; + +namespace DataLayer +{ + internal enum AudioFormatEnum : long + { + //Defining the enum this way ensures that when comparing: + //LC_128_44100_stereo > LC_64_44100_stereo > LC_64_22050_stereo > LC_64_22050_stereo + //This matches how audible interprets these codecs when specifying quality using AudibleApi.DownloadQuality + //I've never seen mono formats. + Unknown = 0, + LC_32_22050_stereo = (32L << 18) | (22050 << 2) | 2, + LC_64_22050_stereo = (64L << 18) | (22050 << 2) | 2, + LC_64_44100_stereo = (64L << 18) | (44100 << 2) | 2, + LC_128_44100_stereo = (128L << 18) | (44100 << 2) | 2, + } + + public class AudioFormat : IComparable, IComparable + { + + internal int AudioFormatID { get; private set; } + public int Bitrate { get; private init; } + public int SampleRate { get; private init; } + public int Channels { get; private init; } + public bool IsValid => Bitrate != 0 && SampleRate != 0 && Channels != 0; + + public static AudioFormat FromString(string formatStr) + { + if (Enum.TryParse(formatStr, ignoreCase: true, out AudioFormatEnum enumVal)) + return FromEnum(enumVal); + return FromEnum(AudioFormatEnum.Unknown); + } + + internal static AudioFormat FromEnum(AudioFormatEnum enumVal) + { + var val = (long)enumVal; + + return new() + { + Bitrate = (int)(val >> 18), + SampleRate = (int)(val >> 2) & ushort.MaxValue, + Channels = (int)(val & 3) + }; + } + internal AudioFormatEnum ToEnum() + { + var val = (AudioFormatEnum)(((long)Bitrate << 18) | ((long)SampleRate << 2) | (long)Channels); + + return Enum.IsDefined(val) ? + val : AudioFormatEnum.Unknown; + } + + public override string ToString() + => IsValid ? + $"{Bitrate} Kbps, {SampleRate / 1000d:F1} kHz, {(Channels == 2 ? "Stereo" : Channels)}" : + "Unknown"; + + public int CompareTo(AudioFormat other) => ToEnum().CompareTo(other.ToEnum()); + + public int CompareTo(object obj) => CompareTo(obj as AudioFormat); + } +} diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index ef072484..22007e13 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -25,6 +25,7 @@ namespace DataLayer Parent = 4, } + public class Book { // implementation detail. set by db only. only used by data layer @@ -38,6 +39,10 @@ namespace DataLayer public ContentType ContentType { get; private set; } public string Locale { get; private set; } + internal AudioFormatEnum _audioFormat; + + public AudioFormat AudioFormat { get => AudioFormat.FromEnum(_audioFormat); set => _audioFormat = value.ToEnum(); } + // mutable public string PictureId { get; set; } public string PictureLarge { get; set; } diff --git a/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.Designer.cs b/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.Designer.cs new file mode 100644 index 00000000..fce7a0ce --- /dev/null +++ b/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.Designer.cs @@ -0,0 +1,397 @@ +// +using System; +using DataLayer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DataLayer.Migrations +{ + [DbContext(typeof(LibationContext))] + [Migration("20220624214932_AddAudioFormat")] + partial class AddAudioFormat + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.6"); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Property("BookId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleProductId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .HasColumnType("INTEGER"); + + b.Property("DatePublished") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsAbridged") + .HasColumnType("INTEGER"); + + b.Property("LengthInMinutes") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("PictureId") + .HasColumnType("TEXT"); + + b.Property("PictureLarge") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("_audioFormat") + .HasColumnType("INTEGER"); + + b.HasKey("BookId"); + + b.HasIndex("AudibleProductId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("ContributorId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.HasKey("BookId", "ContributorId", "Role"); + + b.HasIndex("BookId"); + + b.HasIndex("ContributorId"); + + b.ToTable("BookContributor"); + }); + + modelBuilder.Entity("DataLayer.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleCategoryId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ParentCategoryCategoryId") + .HasColumnType("INTEGER"); + + b.HasKey("CategoryId"); + + b.HasIndex("AudibleCategoryId"); + + b.HasIndex("ParentCategoryCategoryId"); + + b.ToTable("Categories"); + + b.HasData( + new + { + CategoryId = -1, + AudibleCategoryId = "", + Name = "" + }); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Property("ContributorId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleContributorId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("ContributorId"); + + b.HasIndex("Name"); + + b.ToTable("Contributors"); + + b.HasData( + new + { + ContributorId = -1, + Name = "" + }); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("Account") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.HasKey("BookId"); + + b.ToTable("LibraryBooks"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Property("SeriesId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleSeriesId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("SeriesId"); + + b.HasIndex("AudibleSeriesId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("TEXT"); + + b.HasKey("SeriesId", "BookId"); + + b.HasIndex("BookId"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBook"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.HasOne("DataLayer.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("DataLayer.Rating", "Rating", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("OverallRating") + .HasColumnType("REAL"); + + b1.Property("PerformanceRating") + .HasColumnType("REAL"); + + b1.Property("StoryRating") + .HasColumnType("REAL"); + + b1.HasKey("BookId"); + + b1.ToTable("Books"); + + b1.WithOwner() + .HasForeignKey("BookId"); + }); + + b.OwnsMany("DataLayer.Supplement", "Supplements", b1 => + { + b1.Property("SupplementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("Url") + .HasColumnType("TEXT"); + + b1.HasKey("SupplementId"); + + b1.HasIndex("BookId"); + + b1.ToTable("Supplement"); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.Navigation("Book"); + }); + + b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("BookStatus") + .HasColumnType("INTEGER"); + + b1.Property("PdfStatus") + .HasColumnType("INTEGER"); + + b1.Property("Tags") + .HasColumnType("TEXT"); + + b1.HasKey("BookId"); + + b1.ToTable("UserDefinedItem", (string)null); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.OwnsOne("DataLayer.Rating", "Rating", b2 => + { + b2.Property("UserDefinedItemBookId") + .HasColumnType("INTEGER"); + + b2.Property("OverallRating") + .HasColumnType("REAL"); + + b2.Property("PerformanceRating") + .HasColumnType("REAL"); + + b2.Property("StoryRating") + .HasColumnType("REAL"); + + b2.HasKey("UserDefinedItemBookId"); + + b2.ToTable("UserDefinedItem"); + + b2.WithOwner() + .HasForeignKey("UserDefinedItemBookId"); + }); + + b1.Navigation("Book"); + + b1.Navigation("Rating"); + }); + + b.Navigation("Category"); + + b.Navigation("Rating"); + + b.Navigation("Supplements"); + + b.Navigation("UserDefinedItem"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("ContributorsLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Contributor", "Contributor") + .WithMany("BooksLink") + .HasForeignKey("ContributorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Contributor"); + }); + + modelBuilder.Entity("DataLayer.Category", b => + { + b.HasOne("DataLayer.Category", "ParentCategory") + .WithMany() + .HasForeignKey("ParentCategoryCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithOne() + .HasForeignKey("DataLayer.LibraryBook", "BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("SeriesLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Series", "Series") + .WithMany("BooksLink") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Navigation("ContributorsLink"); + + b.Navigation("SeriesLink"); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Navigation("BooksLink"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Navigation("BooksLink"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.cs b/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.cs new file mode 100644 index 00000000..38c7880a --- /dev/null +++ b/Source/DataLayer/Migrations/20220624214932_AddAudioFormat.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataLayer.Migrations +{ + public partial class AddAudioFormat : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "_audioFormat", + table: "Books", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "_audioFormat", + table: "Books"); + } + } +} diff --git a/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs b/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs index c198e3c0..5579839c 100644 --- a/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs +++ b/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace DataLayer.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.6"); modelBuilder.Entity("DataLayer.Book", b => { @@ -56,6 +56,9 @@ namespace DataLayer.Migrations b.Property("Title") .HasColumnType("TEXT"); + b.Property("_audioFormat") + .HasColumnType("INTEGER"); + b.HasKey("BookId"); b.HasIndex("AudibleProductId"); diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index 67d18f4e..3e05db63 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -162,6 +162,9 @@ namespace DtoImporterService { var item = importItem.DtoItem; + var codec = item.AvailableCodecs?.Max(f => AudioFormat.FromString(f.EnhancedCodec)) ?? new AudioFormat(); + book.AudioFormat = codec; + // set/update book-specific info which may have changed if (item.PictureId is not null) book.PictureId = item.PictureId; diff --git a/Source/DtoImporterService/ContributorImporter.cs b/Source/DtoImporterService/ContributorImporter.cs index dd3e273f..2f6d255f 100644 --- a/Source/DtoImporterService/ContributorImporter.cs +++ b/Source/DtoImporterService/ContributorImporter.cs @@ -91,7 +91,7 @@ namespace DtoImporterService return hash.Count; } - private Contributor addContributor(string name, string id = null) + private Contributor addContributor(string name, string id = null) { try { @@ -108,6 +108,6 @@ namespace DtoImporterService Serilog.Log.Logger.Error(ex, "Error adding contributor. {@DebugInfo}", new { name, id }); throw; } - } - } + } + } } diff --git a/Source/DtoImporterService/DtoImporterService.csproj b/Source/DtoImporterService/DtoImporterService.csproj index a9b17110..d784be6f 100644 --- a/Source/DtoImporterService/DtoImporterService.csproj +++ b/Source/DtoImporterService/DtoImporterService.csproj @@ -4,6 +4,14 @@ net6.0-windows + + embedded + + + + embedded + + diff --git a/Source/DtoImporterService/ImporterBase.cs b/Source/DtoImporterService/ImporterBase.cs index 278659c7..5f3298b9 100644 --- a/Source/DtoImporterService/ImporterBase.cs +++ b/Source/DtoImporterService/ImporterBase.cs @@ -55,7 +55,7 @@ namespace DtoImporterService protected ItemsImporterBase(LibationContext context) : base(context) { } protected abstract IValidator Validator { get; } - public sealed override IEnumerable Validate(IEnumerable importItems) + public sealed override IEnumerable Validate(IEnumerable importItems) => Validator.Validate(importItems.Select(i => i.DtoItem)); } } diff --git a/Source/DtoImporterService/LibraryBookImporter.cs b/Source/DtoImporterService/LibraryBookImporter.cs index 634d8494..38581b7c 100644 --- a/Source/DtoImporterService/LibraryBookImporter.cs +++ b/Source/DtoImporterService/LibraryBookImporter.cs @@ -49,7 +49,7 @@ namespace DtoImporterService // just use the first var hash = newItems.ToDictionarySafe(dto => dto.DtoItem.ProductId); foreach (var kvp in hash) - { + { var newItem = kvp.Value; var libraryBook = new LibraryBook( diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index a426cff2..855a0322 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -166,6 +166,9 @@ namespace FileLiberator }; var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList(); + + if (config.MergeOpeningAndEndCredits) + combineCredits(chapters); if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3) { @@ -192,7 +195,7 @@ namespace FileLiberator return dlOptions; } - public static List flattenChapters(IEnumerable chapters, string titleConcat = ": ") + public static List flattenChapters(IList chapters, string titleConcat = ": ") { List chaps = new(); @@ -217,6 +220,22 @@ namespace FileLiberator return chaps; } + public static void combineCredits(IList chapters) + { + if (chapters.Count > 1 && chapters[0].Title == "Opening Credits") + { + chapters[1].StartOffsetMs = chapters[0].StartOffsetMs; + chapters[1].StartOffsetSec = chapters[0].StartOffsetSec; + chapters[1].LengthMs += chapters[0].LengthMs; + chapters.RemoveAt(0); + } + if (chapters.Count > 1 && chapters[^1].Title == "End Credits") + { + chapters[^2].LengthMs += chapters[^1].LengthMs; + chapters.Remove(chapters[^1]); + } + } + private static void downloadValidation(LibraryBook libraryBook) { string errorString(string field) diff --git a/Source/FileLiberator/FileLiberator.csproj b/Source/FileLiberator/FileLiberator.csproj index 460308d6..65b461a1 100644 --- a/Source/FileLiberator/FileLiberator.csproj +++ b/Source/FileLiberator/FileLiberator.csproj @@ -11,4 +11,13 @@ + + embedded + + + + embedded + + + diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj index 16363aa5..28f22c83 100644 --- a/Source/FileManager/FileManager.csproj +++ b/Source/FileManager/FileManager.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -9,4 +9,12 @@ + + embedded + + + + embedded + + diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index c00232a9..4d79c5af 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -35,18 +35,24 @@ namespace FileManager } else { - file = replaceFileName(file, paramReplacements); - fileName = Path.GetDirectoryName(fileName); pathParts.Add(file); + fileName = Path.GetDirectoryName(fileName); } } pathParts.Reverse(); + var fileNamePart = pathParts[^1]; + pathParts.Remove(fileNamePart); - return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting); + LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray()); + + //If file already exists, GetValidFilename will append " (n)" to the filename. + //This could cause the filename length to exceed MaxFilenameLength, so reduce + //allowable filename length by 5 chars, allowing for up to 99 duplicates. + return FileUtility.GetValidFilename(Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - 5)), replacements, returnFirstExisting); } - private string replaceFileName(string filename, Dictionary paramReplacements) + private string replaceFileName(string filename, Dictionary paramReplacements, int maxFilenameLength) { List filenameParts = new(); //Build the filename in parts, replacing replacement parameters with @@ -82,7 +88,7 @@ namespace FileManager //Remove 1 character from the end of the longest filename part until //the total filename is less than max filename length - while (filenameParts.Sum(p => p.Length) > LongPath.MaxFilenameLength) + while (filenameParts.Sum(p => p.Length) > maxFilenameLength) { int maxLength = filenameParts.Max(p => p.Length); var maxEntry = filenameParts.First(p => p.Length == maxLength); diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index b81d3865..53fd9517 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -136,19 +136,21 @@ namespace FileManager { if (toReplace == Replacement.QUOTE_MARK) { - if (preceding == default || - (preceding != default - && !char.IsLetter(preceding) - && !char.IsNumber(preceding) - && (char.IsLetter(succeding) || char.IsNumber(succeding)) + if ( + preceding == default || + ( + !char.IsLetter(preceding) && + !char.IsNumber(preceding) && + (char.IsLetter(succeding) || char.IsNumber(succeding)) ) ) return OpenQuote; - else if (succeding == default || - (succeding != default - && !char.IsLetter(succeding) - && !char.IsNumber(succeding) - && (char.IsLetter(preceding) || char.IsNumber(preceding)) + else if ( + succeding == default || + ( + !char.IsLetter(succeding) && + !char.IsNumber(succeding) && + (char.IsLetter(preceding) || char.IsNumber(preceding)) ) ) return CloseQuote; diff --git a/Source/Hangover/Hangover.csproj b/Source/Hangover/Hangover.csproj index 7c90124e..d75c4de4 100644 --- a/Source/Hangover/Hangover.csproj +++ b/Source/Hangover/Hangover.csproj @@ -6,7 +6,6 @@ true hangover.ico enable - true win-x64 false @@ -24,11 +23,13 @@ edit debug and release output paths --> - ..\LibationWinForms\bin\Debug + ..\bin\Debug + embedded - ..\LibationWinForms\bin\Release + ..\bin\Release + embedded @@ -37,7 +38,7 @@ - + Form1.cs diff --git a/Source/Hangover/Properties/PublishProfiles/FolderProfile.pubxml b/Source/Hangover/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..3ed14ad3 --- /dev/null +++ b/Source/Hangover/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + ..\bin\publish\ + FileSystem + net6.0-windows + win-x64 + false + false + + \ No newline at end of file diff --git a/Source/LibationCli/LibationCli.csproj b/Source/LibationCli/LibationCli.csproj index 84f16e9b..e3b1fa09 100644 --- a/Source/LibationCli/LibationCli.csproj +++ b/Source/LibationCli/LibationCli.csproj @@ -4,12 +4,11 @@ Exe net6.0-windows - - true true win-x64 false false + True - ..\LibationWinForms\bin\Debug + ..\bin\Debug + embedded - ..\LibationWinForms\bin\Release + ..\bin\Release + embedded diff --git a/Source/LibationCli/Properties/PublishProfiles/FolderProfile.pubxml b/Source/LibationCli/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..3ed14ad3 --- /dev/null +++ b/Source/LibationCli/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + ..\bin\publish\ + FileSystem + net6.0-windows + win-x64 + false + false + + \ No newline at end of file diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index d545df22..f7000e07 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -117,6 +117,13 @@ namespace LibationFileManager set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value); } + [Description("Merge Opening/End Credits into the following/preceding chapters")] + public bool MergeOpeningAndEndCredits + { + get => persistentDictionary.GetNonString(nameof(MergeOpeningAndEndCredits)); + set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value); + } + [Description("Strip \"(Unabridged)\" from audiobook metadata tags")] public bool StripUnabridged { @@ -437,7 +444,7 @@ namespace LibationFileManager #endregion #region LibationFiles - private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json"); + private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), "appsettings.json"); private const string LIBATION_FILES_KEY = "LibationFiles"; [Description("Location for storage of program-created files")] diff --git a/Source/LibationFileManager/LibationFileManager.csproj b/Source/LibationFileManager/LibationFileManager.csproj index 4648a9ee..04ae43f2 100644 --- a/Source/LibationFileManager/LibationFileManager.csproj +++ b/Source/LibationFileManager/LibationFileManager.csproj @@ -14,4 +14,12 @@ + + embedded + + + + embedded + + diff --git a/Source/LibationSearchEngine/LibationSearchEngine.csproj b/Source/LibationSearchEngine/LibationSearchEngine.csproj index 9fc7e889..5e811683 100644 --- a/Source/LibationSearchEngine/LibationSearchEngine.csproj +++ b/Source/LibationSearchEngine/LibationSearchEngine.csproj @@ -15,5 +15,14 @@ + + + embedded + + + + embedded + + diff --git a/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs b/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs index fdbc7ac7..838b72b7 100644 --- a/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs +++ b/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs @@ -49,6 +49,7 @@ Title: {Book.Title} Author(s): {Book.AuthorNames()} Narrator(s): {Book.NarratorNames()} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} +Audio Bitrate: {Book.AudioFormat} Category: {string.Join(" > ", Book.CategoriesNames())} Purchase Date: {_libraryBook.DateAdded.ToString("d")} ".Trim(); diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs index 72f9a151..d7b53a2e 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs @@ -13,6 +13,7 @@ namespace LibationWinForms.Dialogs this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt)); this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile)); this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter)); + this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits)); this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); @@ -21,6 +22,7 @@ namespace LibationWinForms.Dialogs downloadCoverArtCbox.Checked = config.DownloadCoverArt; retainAaxFileCbox.Checked = config.RetainAaxFile; splitFilesByChapterCbox.Checked = config.SplitFilesByChapter; + mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits; stripUnabridgedCbox.Checked = config.StripUnabridged; stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio; convertLosslessRb.Checked = !config.DecryptToLossy; @@ -50,6 +52,7 @@ namespace LibationWinForms.Dialogs config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.RetainAaxFile = retainAaxFileCbox.Checked; config.SplitFilesByChapter = splitFilesByChapterCbox.Checked; + config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked; config.StripUnabridged = stripUnabridgedCbox.Checked; config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked; config.DecryptToLossy = convertLossyRb.Checked; diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index d117e4fe..19166a4f 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -108,6 +108,7 @@ this.retainAaxFileCbox = new System.Windows.Forms.CheckBox(); this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox(); this.createCueSheetCbox = new System.Windows.Forms.CheckBox(); + this.mergeOpeningEndCreditsCbox = new System.Windows.Forms.CheckBox(); this.badBookGb.SuspendLayout(); this.tabControl.SuspendLayout(); this.tab1ImportantSettings.SuspendLayout(); @@ -251,7 +252,7 @@ // stripAudibleBrandingCbox // this.stripAudibleBrandingCbox.AutoSize = true; - this.stripAudibleBrandingCbox.Location = new System.Drawing.Point(19, 168); + this.stripAudibleBrandingCbox.Location = new System.Drawing.Point(19, 193); this.stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox"; this.stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34); this.stripAudibleBrandingCbox.TabIndex = 13; @@ -285,7 +286,7 @@ // convertLossyRb // this.convertLossyRb.AutoSize = true; - this.convertLossyRb.Location = new System.Drawing.Point(19, 232); + this.convertLossyRb.Location = new System.Drawing.Point(19, 257); this.convertLossyRb.Name = "convertLossyRb"; this.convertLossyRb.Size = new System.Drawing.Size(329, 19); this.convertLossyRb.TabIndex = 12; @@ -297,7 +298,7 @@ // this.convertLosslessRb.AutoSize = true; this.convertLosslessRb.Checked = true; - this.convertLosslessRb.Location = new System.Drawing.Point(19, 207); + this.convertLosslessRb.Location = new System.Drawing.Point(19, 232); this.convertLosslessRb.Name = "convertLosslessRb"; this.convertLosslessRb.Size = new System.Drawing.Size(335, 19); this.convertLosslessRb.TabIndex = 11; @@ -608,6 +609,7 @@ this.tab4AudioFileOptions.Controls.Add(this.stripAudibleBrandingCbox); this.tab4AudioFileOptions.Controls.Add(this.convertLosslessRb); this.tab4AudioFileOptions.Controls.Add(this.stripUnabridgedCbox); + this.tab4AudioFileOptions.Controls.Add(this.mergeOpeningEndCreditsCbox); this.tab4AudioFileOptions.Controls.Add(this.splitFilesByChapterCbox); this.tab4AudioFileOptions.Controls.Add(this.retainAaxFileCbox); this.tab4AudioFileOptions.Controls.Add(this.downloadCoverArtCbox); @@ -980,7 +982,7 @@ // stripUnabridgedCbox // this.stripUnabridgedCbox.AutoSize = true; - this.stripUnabridgedCbox.Location = new System.Drawing.Point(19, 143); + this.stripUnabridgedCbox.Location = new System.Drawing.Point(19, 168); this.stripUnabridgedCbox.Name = "stripUnabridgedCbox"; this.stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19); this.stripUnabridgedCbox.TabIndex = 13; @@ -1024,6 +1026,17 @@ this.createCueSheetCbox.UseVisualStyleBackColor = true; this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged); // + // mergeBeginningEndCreditsCbox + // + this.mergeOpeningEndCreditsCbox.AutoSize = true; + this.mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 143); + this.mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; + this.mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(206, 19); + this.mergeOpeningEndCreditsCbox.TabIndex = 13; + this.mergeOpeningEndCreditsCbox.Text = "[MergeOpeningEndCredits desc]"; + this.mergeOpeningEndCreditsCbox.UseVisualStyleBackColor = true; + this.mergeOpeningEndCreditsCbox.CheckedChanged += new System.EventHandler(this.splitFilesByChapterCbox_CheckedChanged); + // // SettingsDialog // this.AcceptButton = this.saveBtn; @@ -1155,5 +1168,6 @@ private System.Windows.Forms.Button chapterTitleTemplateBtn; private System.Windows.Forms.TextBox chapterTitleTemplateTb; private System.Windows.Forms.Button editCharreplacementBtn; + private System.Windows.Forms.CheckBox mergeOpeningEndCreditsCbox; } } \ No newline at end of file diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index b21cadd3..b15b714c 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -7,7 +7,7 @@ true libation.ico Libation - + true win-x64 false @@ -27,6 +27,16 @@ en;es + + ..\bin\Debug + embedded + + + + ..\bin\Release + embedded + + diff --git a/Source/LibationWinForms/Properties/PublishProfiles/FolderProfile.pubxml b/Source/LibationWinForms/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..806982d7 --- /dev/null +++ b/Source/LibationWinForms/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + x64 + ..\bin\publish\ + FileSystem + net6.0-windows + win-x64 + false + false + + \ No newline at end of file diff --git a/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj b/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj index ef046649..f77a9580 100644 --- a/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj +++ b/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj @@ -2,7 +2,6 @@ net6.0-windows - false diff --git a/Source/_Tests/FileLiberator.Tests/DownloadDecryptBookTests.cs b/Source/_Tests/FileLiberator.Tests/DownloadDecryptBookTests.cs index a30e585f..c2df3666 100644 --- a/Source/_Tests/FileLiberator.Tests/DownloadDecryptBookTests.cs +++ b/Source/_Tests/FileLiberator.Tests/DownloadDecryptBookTests.cs @@ -14,8 +14,142 @@ namespace FileLiberator.Tests [TestClass] public class DownloadDecryptBookTests { + private static Chapter[] HierarchicalChapters => new Chapter[] + { + new () + { + Title = "Opening Credits", + StartOffsetMs = 0, + StartOffsetSec = 0, + LengthMs = 10000, + }, + new () + { + Title = "Book 1", + StartOffsetMs = 10000, + StartOffsetSec = 10, + LengthMs = 2000, + Chapters = new Chapter[] + { + new () + { + Title = "Part 1", + StartOffsetMs = 12000, + StartOffsetSec = 12, + LengthMs = 2000, + Chapters = new Chapter[] + { + new () + { + Title = "Chapter 1", + StartOffsetMs = 14000, + StartOffsetSec = 14, + LengthMs = 86000, + }, + new() + { + Title = "Chapter 2", + StartOffsetMs = 100000, + StartOffsetSec = 100, + LengthMs = 100000, + }, + } + }, + new() + { + Title = "Part 2", + StartOffsetMs = 200000, + StartOffsetSec = 200, + LengthMs = 2000, + Chapters = new Chapter[] + { + new() + { + Title = "Chapter 3", + StartOffsetMs = 202000, + StartOffsetSec = 202, + LengthMs = 98000, + }, + new() + { + Title = "Chapter 4", + StartOffsetMs = 300000, + StartOffsetSec = 300, + LengthMs = 100000, + }, + } + } + } + }, + new() + { + Title = "Book 2", + StartOffsetMs = 400000, + StartOffsetSec = 400, + LengthMs = 2000, + Chapters = new Chapter[] + { + new() + { + Title = "Part 3", + StartOffsetMs = 402000, + StartOffsetSec = 402, + LengthMs = 2000, + Chapters = new Chapter[] + { + new() + { + Title = "Chapter 5", + StartOffsetMs = 404000, + StartOffsetSec = 404, + LengthMs = 96000, + }, + new() + { + Title = "Chapter 6", + StartOffsetMs = 500000, + StartOffsetSec = 500, + LengthMs = 100000, + }, + } + }, + new() + { + Title = "Part 4", + StartOffsetMs = 600000, + StartOffsetSec = 600, + LengthMs = 2000, + Chapters = new Chapter[] + { + new() + { + Title = "Chapter 7", + StartOffsetMs = 602000, + StartOffsetSec = 602, + LengthMs = 98000, + }, + new() + { + Title = "Chapter 8", + StartOffsetMs = 700000, + StartOffsetSec = 700, + LengthMs = 100000, + }, + } + } + } + }, + new() + { + Title = "End Credits", + StartOffsetMs = 800000, + StartOffsetSec = 800, + LengthMs = 10000, + }, + }; + [TestMethod] - public void HierarchicalChapters_Flatten() + public void Chapters_CombineCredits() { var expected = new Chapter[] { @@ -73,129 +207,109 @@ namespace FileLiberator.Tests Title = "Book 2: Part 4: Chapter 8", StartOffsetMs = 700000, StartOffsetSec = 700, - LengthMs = 100000, + LengthMs = 110000, } }; - var hierarchicalChapters = new Chapter[] - { - new() - { - Title = "Book 1", - StartOffsetMs = 0, - StartOffsetSec = 0, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Part 1", - StartOffsetMs = 2000, - StartOffsetSec = 2, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Chapter 1", - StartOffsetMs = 4000, - StartOffsetSec = 4, - LengthMs = 96000, - }, - new() - { Title = "Chapter 2", - StartOffsetMs = 100000, - StartOffsetSec = 100, - LengthMs = 100000, - }, - } - }, - new() - { Title = "Part 2", - StartOffsetMs = 200000, - StartOffsetSec = 200, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Chapter 3", - StartOffsetMs = 202000, - StartOffsetSec = 202, - LengthMs = 98000, - }, - new() - { Title = "Chapter 4", - StartOffsetMs = 300000, - StartOffsetSec = 300, - LengthMs = 100000, - }, - } - } - } - }, - new() - { - Title = "Book 2", - StartOffsetMs = 400000, - StartOffsetSec = 400, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Part 3", - StartOffsetMs = 402000, - StartOffsetSec = 402, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Chapter 5", - StartOffsetMs = 404000, - StartOffsetSec = 404, - LengthMs = 96000, - }, - new() - { Title = "Chapter 6", - StartOffsetMs = 500000, - StartOffsetSec = 500, - LengthMs = 100000, - }, - } - }, - new() - { Title = "Part 4", - StartOffsetMs = 600000, - StartOffsetSec = 600, - LengthMs = 2000, - Chapters = new Chapter[] - { - new() - { Title = "Chapter 7", - StartOffsetMs = 602000, - StartOffsetSec = 602, - LengthMs = 98000, - }, - new() - { Title = "Chapter 8", - StartOffsetMs = 700000, - StartOffsetSec = 700, - LengthMs = 100000, - }, - } - } - } - } - }; + var flatChapters = DownloadDecryptBook.flattenChapters(HierarchicalChapters); + DownloadDecryptBook.combineCredits(flatChapters); + checkChapters(flatChapters, expected); + } - var flatChapters = DownloadDecryptBook.flattenChapters(hierarchicalChapters); - flatChapters.Count.Should().Be(expected.Length); - - for (int i = 0; i < flatChapters.Count; i++) + [TestMethod] + public void HierarchicalChapters_Flatten() + { + var expected = new Chapter[] { - flatChapters[i].Title.Should().Be(expected[i].Title); - flatChapters[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs); - flatChapters[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec); - flatChapters[i].LengthMs.Should().Be(expected[i].LengthMs); - flatChapters[i].Chapters.Should().BeNull(); + new() + { + Title = "Opening Credits", + StartOffsetMs = 0, + StartOffsetSec = 0, + LengthMs = 10000, + }, + new() + { + Title = "Book 1: Part 1: Chapter 1", + StartOffsetMs = 10000, + StartOffsetSec = 10, + LengthMs = 90000, + }, + new() + { + Title = "Book 1: Part 1: Chapter 2", + StartOffsetMs = 100000, + StartOffsetSec = 100, + LengthMs = 100000, + }, + new() + { + Title = "Book 1: Part 2: Chapter 3", + StartOffsetMs = 200000, + StartOffsetSec = 200, + LengthMs = 100000, + }, + new() + { + Title = "Book 1: Part 2: Chapter 4", + StartOffsetMs = 300000, + StartOffsetSec = 300, + LengthMs = 100000, + }, + new() + { + Title = "Book 2: Part 3: Chapter 5", + StartOffsetMs = 400000, + StartOffsetSec = 400, + LengthMs = 100000, + }, + new() + { + Title = "Book 2: Part 3: Chapter 6", + StartOffsetMs = 500000, + StartOffsetSec = 500, + LengthMs = 100000, + }, + new() + { + Title = "Book 2: Part 4: Chapter 7", + StartOffsetMs = 600000, + StartOffsetSec = 600, + LengthMs = 100000, + }, + new() + { + Title = "Book 2: Part 4: Chapter 8", + StartOffsetMs = 700000, + StartOffsetSec = 700, + LengthMs = 100000, + }, + new() + { + Title = "End Credits", + StartOffsetMs = 800000, + StartOffsetSec = 800, + LengthMs = 10000, + } + }; + + var flatChapters = DownloadDecryptBook.flattenChapters(HierarchicalChapters); + + checkChapters(flatChapters, expected); + } + + private static void checkChapters(IList value, IList expected) + { + value.Count.Should().Be(expected.Count); + + for (int i = 0; i < value.Count; i++) + { + value[i].Title.Should().Be(expected[i].Title); + value[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs); + value[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec); + value[i].LengthMs.Should().Be(expected[i].LengthMs); + value[i].Chapters.Should().BeNull(); } } } diff --git a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj index 61822f5e..4e1d1373 100644 --- a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj +++ b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj @@ -2,7 +2,6 @@ net6.0-windows - false diff --git a/Source/publish.ps1 b/Source/publish.ps1 new file mode 100644 index 00000000..23fba050 --- /dev/null +++ b/Source/publish.ps1 @@ -0,0 +1,16 @@ +<# You must enable running powershell scripts. + + Set-ExecutionPolicy -Scope CurrentUser Unrestricted +#> + +$pubDir = "bin\Publish" +Remove-Item $pubDir -Recurse -Force + +dotnet publish -c Release LibationWinForms\LibationWinForms.csproj -p:PublishProfile=LibationWinForms\Properties\PublishProfiles\FolderProfile.pubxml +dotnet publish -c Release LibationCli\LibationCli.csproj -p:PublishProfile=LibationCli\Properties\PublishProfiles\FolderProfile.pubxml +dotnet publish -c Release Hangover\Hangover.csproj -p:PublishProfile=Hangover\Properties\PublishProfiles\FolderProfile.pubxml + +$verMatch = Select-String -Path 'AppScaffolding\AppScaffolding.csproj' -Pattern '(\d{0,3}\.\d{0,3}\.\d{0,3})\.\d{0,3}' +$archiveName = "bin\Libation."+$verMatch.Matches.Groups[1].Value+".zip" +Get-ChildItem -Path $pubDir -Recurse | + Compress-Archive -DestinationPath $archiveName -Force \ No newline at end of file