Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Source/FileLiberator/DownloadOptions.Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ var chapters
stripBranding(chapters, licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs, licInfo.ContentMetadata.ChapterInfo.BrandOutroDurationMs);

if (config.SplitFilesByChapter)
{
combineShortChapters(chapters, config.MinimumFileDuration * 1000);
if (config.MaximumFileDuration > 0)
splitLongChapters(chapters, config.MaximumFileDuration * 1000);
}

var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
{
Expand Down Expand Up @@ -390,6 +394,39 @@ public static void combineShortChapters(List<Chapter> chapters, long minChapterL
}
}

public static void splitLongChapters(List<Chapter> chapters, long maxChapterLengthMs)
{
for (int i = 0; i < chapters.Count; i++)
{
if (chapters[i].LengthMs <= maxChapterLengthMs)
continue;

var original = chapters[i];
int totalParts = (int)Math.Ceiling((double)original.LengthMs / maxChapterLengthMs);
var parts = new List<Chapter>(totalParts);
long offset = original.StartOffsetMs;
long remaining = original.LengthMs;

for (int part = 1; part <= totalParts; part++)
{
long partLength = Math.Min(remaining, maxChapterLengthMs);
parts.Add(new Chapter
{
Title = $"{original.Title} (Part {part} of {totalParts})",
StartOffsetMs = offset,
StartOffsetSec = offset / 1000,
LengthMs = partLength,
});
offset += partLength;
remaining -= partLength;
}

chapters.RemoveAt(i);
chapters.InsertRange(i, parts);
i += parts.Count - 1;
}
}

public static void stripBranding(List<Chapter> chapters, long introMs, long outroMs)
{
chapters[0].LengthMs -= introMs;
Expand Down
26 changes: 21 additions & 5 deletions Source/LibationAvalonia/Controls/Settings/Audio.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@
<Grid
Margin="0,0,0,5"
Grid.ColumnDefinitions="Auto,Auto"
Grid.RowDefinitions="Auto,Auto">
<CheckBox Margin="0" Grid.ColumnSpan="2" IsChecked="{Binding SplitFilesByChapter, Mode=TwoWay}">
<TextBlock Grid.ColumnSpan="2" Text="{Binding SplitFilesByChapterText}" />
Grid.RowDefinitions="Auto,Auto,Auto">

<CheckBox Margin="0" Grid.ColumnSpan="2" IsChecked="{Binding SplitFilesByChapter, Mode=TwoWay}">
<TextBlock Grid.ColumnSpan="2" Text="{Binding SplitFilesByChapterText}" />
</CheckBox>

<TextBlock Grid.Row="1" Margin="15,0" VerticalAlignment="Center" ToolTip.Tip="{Binding MinimumFileDurationTip}" Text="{Binding MinimumFileDurationText}" />

<NumericUpDown
Classes="SmallNumericUpDown"
Grid.Column="1"
Expand All @@ -175,6 +175,22 @@
ParsingNumberStyle="Integer"
IsEnabled="{Binding SplitFilesByChapter}"
Value="{Binding MinimumFileDuration, Mode=TwoWay}"/>

<TextBlock Grid.Row="2" Margin="15,0" VerticalAlignment="Center" ToolTip.Tip="{Binding MaximumFileDurationTip}" Text="{Binding MaximumFileDurationText}" />

<NumericUpDown
Classes="SmallNumericUpDown"
Grid.Column="1"
Grid.Row="2"
ToolTip.Tip="{Binding MaximumFileDurationTip}"
MinWidth="100"
Minimum="0"
Maximum="1440"
Increment="1"
FormatString="N0"
ParsingNumberStyle="Integer"
IsEnabled="{Binding SplitFilesByChapter}"
Value="{Binding MaximumFileDuration, Mode=TwoWay}"/>
</Grid>

<CheckBox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public AudioSettingsVM(Configuration config)
ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
SplitFilesByChapter = config.SplitFilesByChapter;
MinimumFileDuration = config.MinimumFileDuration;
MaximumFileDuration = config.MaximumFileDuration;
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
StripUnabridged = config.StripUnabridged;
Expand Down Expand Up @@ -73,6 +74,7 @@ public void SaveSettings(Configuration config)
config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
config.SplitFilesByChapter = SplitFilesByChapter;
config.MinimumFileDuration = MinimumFileDuration;
config.MaximumFileDuration = MaximumFileDuration;
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
config.StripAudibleBrandAudio = StripAudibleBrandAudio;
config.StripUnabridged = StripUnabridged;
Expand Down Expand Up @@ -121,6 +123,7 @@ public void SaveSettings(Configuration config)
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
public string MinimumFileDurationText { get; } = Configuration.GetDescription(nameof(Configuration.MinimumFileDuration));
public string MaximumFileDurationText { get; } = Configuration.GetDescription(nameof(Configuration.MaximumFileDuration));
public string MergeOpeningEndCreditsText { get; } = Configuration.GetDescription(nameof(Configuration.MergeOpeningAndEndCredits));
public string StripAudibleBrandingText { get; } = Configuration.GetDescription(nameof(Configuration.StripAudibleBrandAudio));
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
Expand Down Expand Up @@ -150,6 +153,7 @@ public void SaveSettings(Configuration config)
public bool DecryptToLossy { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
public string MinimumFileDurationTip => Configuration.GetHelpText(nameof(MinimumFileDuration));
public string MaximumFileDurationTip => Configuration.GetHelpText(nameof(MaximumFileDuration));
public bool MoveMoovToBeginning { get; set; }

public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
Expand All @@ -158,6 +162,7 @@ public void SaveSettings(Configuration config)

public bool SplitFilesByChapter { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
public int MinimumFileDuration { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
public int MaximumFileDuration { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
public bool LameTargetBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
public bool LameMatchSource { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
public int LameBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
Expand Down
7 changes: 7 additions & 0 deletions Source/LibationFileManager/Configuration.HelpText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ this duration will be merged with the following
chapter. Merged chapter titles will be joined with
a space between them.
""" },
{nameof(MaximumFileDuration), """
The maximum duration (in minutes) for a chapter to
be split into its own file. Chapters longer than
this duration will be divided into equal-length
parts. Part titles are suffixed with "(Part N of M)".
Set to 0 to disable.
""" },
{nameof(SpatialAudioCodec), """
The Dolby Digital Plus (E-AC-3) codec is more widely
supported than the AC-4 codec, but E-AC-3 files are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ public string InProgress
[Description("Minimum file duration (seconds)")]
public int MinimumFileDuration { get => Math.Max(0, GetNonString(defaultValue: 3)); set => SetNonString(value); }

[Description("Maximum file duration (seconds)")]
public int MaximumFileDuration { get => Math.Max(0, GetNonString(defaultValue: 0)); set => SetNonString(value); }

[Description("Merge Opening/End Credits into the following/preceding chapters")]
public bool MergeOpeningAndEndCredits { get => GetNonString(defaultValue: false); set => SetNonString(value); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private void Load_AudioSettings(Configuration config)
this.combineNestedChapterTitlesCbox.Text = desc(nameof(config.CombineNestedChapterTitles));
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
this.minFileDurationLbl.Text = desc(nameof(config.MinimumFileDuration));
this.maxFileDurationLbl.Text = desc(nameof(config.MaximumFileDuration));
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
Expand Down Expand Up @@ -80,6 +81,7 @@ private void Load_AudioSettings(Configuration config)
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
minFileDurationNud.Value = config.MinimumFileDuration;
maxFileDurationNud.Value = config.MaximumFileDuration;
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
stripUnabridgedCbox.Checked = config.StripUnabridged;
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
Expand Down Expand Up @@ -124,6 +126,7 @@ private void Save_AudioSettings(Configuration config)
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.MinimumFileDuration = (int)minFileDurationNud.Value;
config.MaximumFileDuration = (int)maxFileDurationNud.Value;
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
config.StripUnabridged = stripUnabridgedCbox.Checked;
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
Expand Down Expand Up @@ -159,7 +162,7 @@ private void LameMatchSourceBRCbox_CheckedChanged(object sender, EventArgs e)

private void splitFilesByChapterCbox_CheckedChanged(object sender, EventArgs e)
{
chapterTitleTemplateGb.Enabled = minFileDurationNud.Enabled = minFileDurationLbl.Enabled = splitFilesByChapterCbox.Checked;
chapterTitleTemplateGb.Enabled = minFileDurationNud.Enabled = minFileDurationLbl.Enabled = maxFileDurationNud.Enabled = maxFileDurationLbl.Enabled = splitFilesByChapterCbox.Checked;
}

private void chapterTitleTemplateBtn_Click(object sender, EventArgs e)
Expand Down
33 changes: 30 additions & 3 deletions Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions Source/_Tests/FileLiberator.Tests/DownloadDecryptBookTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,116 @@ private static void checkChapters(IList<Chapter> value, IList<Chapter> expected)
value[i].LengthMs.Should().Be(expected[i].LengthMs);
}
}

[TestMethod]
public void SplitLongChapters_ShortChapter_NotSplit()
{
var chapters = new List<Chapter>
{
new() { Title = "Chapter 1", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 60_000 },
};

DownloadOptions.splitLongChapters(chapters, 120_000);

chapters.Count.Should().Be(1);
chapters[0].Title.Should().Be("Chapter 1");
chapters[0].LengthMs.Should().Be(60_000);
}

[TestMethod]
public void SplitLongChapters_ExactlyAtLimit_NotSplit()
{
var chapters = new List<Chapter>
{
new() { Title = "Chapter 1", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 120_000 },
};

DownloadOptions.splitLongChapters(chapters, 120_000);

chapters.Count.Should().Be(1);
chapters[0].Title.Should().Be("Chapter 1");
}

[TestMethod]
public void SplitLongChapters_TwoParts_SplitsEvenly()
{
var chapters = new List<Chapter>
{
new() { Title = "Chapter 1", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 200_000 },
};

DownloadOptions.splitLongChapters(chapters, 100_000);

chapters.Count.Should().Be(2);
chapters[0].Title.Should().Be("Chapter 1 (Part 1 of 2)");
chapters[0].StartOffsetMs.Should().Be(0);
chapters[0].StartOffsetSec.Should().Be(0);
chapters[0].LengthMs.Should().Be(100_000);
chapters[1].Title.Should().Be("Chapter 1 (Part 2 of 2)");
chapters[1].StartOffsetMs.Should().Be(100_000);
chapters[1].StartOffsetSec.Should().Be(100);
chapters[1].LengthMs.Should().Be(100_000);
}

[TestMethod]
public void SplitLongChapters_ThreeParts_LastPartShorter()
{
var chapters = new List<Chapter>
{
new() { Title = "Long Chapter", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 250_000 },
};

DownloadOptions.splitLongChapters(chapters, 100_000);

chapters.Count.Should().Be(3);
chapters[0].LengthMs.Should().Be(100_000);
chapters[1].LengthMs.Should().Be(100_000);
chapters[2].LengthMs.Should().Be(50_000);
chapters[2].StartOffsetMs.Should().Be(200_000);
chapters[2].StartOffsetSec.Should().Be(200);
chapters[2].Title.Should().Be("Long Chapter (Part 3 of 3)");
}

[TestMethod]
public void SplitLongChapters_OffsetPreserved()
{
var chapters = new List<Chapter>
{
new() { Title = "Chapter 1", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 60_000 },
new() { Title = "Chapter 2", StartOffsetMs = 60_000, StartOffsetSec = 60, LengthMs = 200_000 },
};

DownloadOptions.splitLongChapters(chapters, 100_000);

chapters.Count.Should().Be(3);
chapters[0].Title.Should().Be("Chapter 1");
chapters[1].Title.Should().Be("Chapter 2 (Part 1 of 2)");
chapters[1].StartOffsetMs.Should().Be(60_000);
chapters[1].StartOffsetSec.Should().Be(60);
chapters[1].LengthMs.Should().Be(100_000);
chapters[2].Title.Should().Be("Chapter 2 (Part 2 of 2)");
chapters[2].StartOffsetMs.Should().Be(160_000);
chapters[2].StartOffsetSec.Should().Be(160);
chapters[2].LengthMs.Should().Be(100_000);
}

[TestMethod]
public void SplitLongChapters_MultipleChaptersOnlyLongOnesSplit()
{
var chapters = new List<Chapter>
{
new() { Title = "Short", StartOffsetMs = 0, StartOffsetSec = 0, LengthMs = 30_000 },
new() { Title = "Long", StartOffsetMs = 30_000, StartOffsetSec = 30, LengthMs = 300_000 },
new() { Title = "Short2", StartOffsetMs = 330_000, StartOffsetSec = 330, LengthMs = 30_000 },
};

DownloadOptions.splitLongChapters(chapters, 100_000);

chapters.Count.Should().Be(5);
chapters[0].Title.Should().Be("Short");
chapters[1].Title.Should().Be("Long (Part 1 of 3)");
chapters[2].Title.Should().Be("Long (Part 2 of 3)");
chapters[3].Title.Should().Be("Long (Part 3 of 3)");
chapters[4].Title.Should().Be("Short2");
}
}
Loading