Merge pull request #664 from Mbucari/startup-2
New settings, context menu, and performance improvements
This commit is contained in:
commit
5976706e40
@ -118,11 +118,7 @@ namespace AaxDecrypter
|
|||||||
public abstract Task CancelAsync();
|
public abstract Task CancelAsync();
|
||||||
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||||
|
|
||||||
public virtual void SetCoverArt(byte[] coverArt)
|
public virtual void SetCoverArt(byte[] coverArt) { }
|
||||||
{
|
|
||||||
if (coverArt is not null)
|
|
||||||
OnRetrievedCoverArt(coverArt);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnRetrievedTitle(string title)
|
protected void OnRetrievedTitle(string title)
|
||||||
=> RetrievedTitle?.Invoke(this, title);
|
=> RetrievedTitle?.Invoke(this, title);
|
||||||
|
|||||||
@ -127,6 +127,8 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
|
||||||
|
|
||||||
|
if (Path.GetFileName(uriToSameFile.LocalPath) != Path.GetFileName(Uri.LocalPath))
|
||||||
|
throw new ArgumentException($"New uri to the same file must have the same file name.");
|
||||||
if (uriToSameFile.Host != Uri.Host)
|
if (uriToSameFile.Host != Uri.Host)
|
||||||
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
|
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
|
||||||
if (DownloadTask is not null)
|
if (DownloadTask is not null)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AaxDecrypter;
|
using AaxDecrypter;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using AudibleApi.Common;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
@ -129,7 +130,7 @@ namespace FileLiberator
|
|||||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
||||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||||
|
|
||||||
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
|
if (contentLic.DrmType != DrmType.Adrm)
|
||||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -153,15 +154,33 @@ namespace FileLiberator
|
|||||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||||
|
|
||||||
// REAL WORK DONE HERE
|
// REAL WORK DONE HERE
|
||||||
return await abDownloader.RunAsync();
|
var success = await abDownloader.RunAsync();
|
||||||
|
|
||||||
|
if (success && config.SaveMetadataToFile)
|
||||||
|
{
|
||||||
|
var metadataFile = Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
|
||||||
|
|
||||||
|
saveMetadata(libraryBook, contentLic.ContentMetadata, metadataFile);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
private void saveMetadata(LibraryBook libraryBook, ContentMetadata contentMetadata, string fileName)
|
||||||
|
{
|
||||||
|
var export = Newtonsoft.Json.Linq.JObject.FromObject(LibToDtos.ToDtos(new[] { libraryBook })[0]);
|
||||||
|
export.Add(nameof(contentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ChapterInfo));
|
||||||
|
export.Add(nameof(contentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ContentReference));
|
||||||
|
|
||||||
|
File.WriteAllText(fileName, export.ToString());
|
||||||
|
OnFileCreated(libraryBook, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic)
|
||||||
{
|
{
|
||||||
//If DrmType != Adrm the delivered file is an unencrypted mp3.
|
//If DrmType != Adrm the delivered file is an unencrypted mp3.
|
||||||
|
|
||||||
var outputFormat
|
var outputFormat
|
||||||
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
|
= contentLic.DrmType != DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
|
||||||
? OutputFormat.Mp3
|
? OutputFormat.Mp3
|
||||||
: OutputFormat.M4b;
|
: OutputFormat.M4b;
|
||||||
|
|
||||||
@ -183,7 +202,11 @@ namespace FileLiberator
|
|||||||
RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
|
RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
|
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||||
|
var chapters
|
||||||
|
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||||
|
.OrderBy(c => c.StartOffsetMs)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (config.MergeOpeningAndEndCredits)
|
if (config.MergeOpeningAndEndCredits)
|
||||||
combineCredits(chapters);
|
combineCredits(chapters);
|
||||||
@ -280,14 +303,19 @@ namespace FileLiberator
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public static List<AudibleApi.Common.Chapter> flattenChapters(IList<AudibleApi.Common.Chapter> chapters, string titleConcat = ": ")
|
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string titleConcat = ": ")
|
||||||
{
|
{
|
||||||
List<AudibleApi.Common.Chapter> chaps = new();
|
List<Chapter> chaps = new();
|
||||||
|
|
||||||
foreach (var c in chapters)
|
foreach (var c in chapters)
|
||||||
{
|
{
|
||||||
if (c.Chapters is null)
|
if (c.Chapters is null)
|
||||||
chaps.Add(c);
|
chaps.Add(c);
|
||||||
|
else if (titleConcat is null)
|
||||||
|
{
|
||||||
|
chaps.Add(c);
|
||||||
|
chaps.AddRange(flattenChapters(c.Chapters));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (c.LengthMs < 10000)
|
if (c.LengthMs < 10000)
|
||||||
@ -305,13 +333,12 @@ namespace FileLiberator
|
|||||||
child.Title = $"{c.Title}{titleConcat}{child.Title}";
|
child.Title = $"{c.Title}{titleConcat}{child.Title}";
|
||||||
|
|
||||||
chaps.AddRange(children);
|
chaps.AddRange(children);
|
||||||
c.Chapters = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chaps;
|
return chaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void combineCredits(IList<AudibleApi.Common.Chapter> chapters)
|
public static void combineCredits(IList<Chapter> chapters)
|
||||||
{
|
{
|
||||||
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
||||||
{
|
{
|
||||||
@ -351,10 +378,14 @@ namespace FileLiberator
|
|||||||
|
|
||||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
||||||
{
|
{
|
||||||
|
if (Configuration.Instance.AllowLibationFixup)
|
||||||
|
{
|
||||||
|
e = OnRequestCoverArt();
|
||||||
|
abDownloader.SetCoverArt(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (e is not null)
|
if (e is not null)
|
||||||
OnCoverImageDiscovered(e);
|
OnCoverImageDiscovered(e);
|
||||||
else if (Configuration.Instance.AllowLibationFixup)
|
|
||||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Move new files to 'Books' directory</summary>
|
/// <summary>Move new files to 'Books' directory</summary>
|
||||||
|
|||||||
@ -73,7 +73,15 @@
|
|||||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
<CheckBox IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
<CheckBox
|
||||||
|
ToolTip.Tip="{CompiledBinding CombineNestedChapterTitlesTip}"
|
||||||
|
IsChecked="{CompiledBinding CombineNestedChapterTitles, Mode=TwoWay}">
|
||||||
|
<TextBlock Text="{CompiledBinding CombineNestedChapterTitlesText}" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
ToolTip.Tip="{CompiledBinding AllowLibationFixupTip}"
|
||||||
|
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@ -165,8 +165,11 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:GroupBox>
|
</controls:GroupBox>
|
||||||
|
|
||||||
<CheckBox
|
<StackPanel
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
Margin="5"
|
Margin="5"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
IsVisible="{CompiledBinding !Config.IsLinux}"
|
IsVisible="{CompiledBinding !Config.IsLinux}"
|
||||||
@ -178,5 +181,16 @@
|
|||||||
|
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
Margin="5"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
IsChecked="{CompiledBinding SaveMetadataToFile, Mode=TwoWay}">
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{CompiledBinding SaveMetadataToFileText}" />
|
||||||
|
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using DataLayer;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
@ -41,16 +44,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private void Configure_BackupCounts()
|
private void Configure_BackupCounts()
|
||||||
{
|
{
|
||||||
MainWindow.Loaded += setBackupCounts;
|
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
|
||||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void setBackupCounts(object _, object __)
|
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||||
{
|
{
|
||||||
if (updateCountsTask?.IsCompleted ?? true)
|
if (updateCountsTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts());
|
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
|
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||||
var stats = await updateCountsTask;
|
var stats = await updateCountsTask;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||||
|
|
||||||
|
|||||||
@ -264,9 +264,12 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
byte[] coverData = PictureStorage
|
var quality
|
||||||
.GetPictureSynchronously(
|
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
|
||||||
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
|
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||||
|
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||||
|
|
||||||
|
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
|
||||||
|
|
||||||
AudioDecodable_CoverImageDiscovered(this, coverData);
|
AudioDecodable_CoverImageDiscovered(this, coverData);
|
||||||
return coverData;
|
return coverData;
|
||||||
|
|||||||
@ -91,37 +91,21 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region Display Functions
|
#region Display Functions
|
||||||
|
|
||||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
||||||
|
|
||||||
var geList = dbBooks
|
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||||
.Where(lb => lb.Book.IsProduct())
|
|
||||||
.Select(b => new LibraryBookEntry<AvaloniaEntryStatus>(b))
|
|
||||||
.ToList<IGridEntry>();
|
|
||||||
|
|
||||||
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()).ToList();
|
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||||
|
|
||||||
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
|
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
||||||
|
|
||||||
foreach (var parent in seriesBooks)
|
|
||||||
{
|
|
||||||
var seriesEpisodes = episodes.FindChildren(parent);
|
|
||||||
|
|
||||||
if (!seriesEpisodes.Any()) continue;
|
|
||||||
|
|
||||||
var seriesEntry = new SeriesEntry<AvaloniaEntryStatus>(parent, seriesEpisodes);
|
|
||||||
seriesEntry.Liberate.Expanded = false;
|
|
||||||
|
|
||||||
geList.Add(seriesEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Create the filtered-in list before adding entries to avoid a refresh
|
//Create the filtered-in list before adding entries to avoid a refresh
|
||||||
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
|
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||||
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
|
|
||||||
|
|
||||||
//Add all children beneath their parent
|
//Add all children beneath their parent
|
||||||
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
foreach (var series in seriesEntries)
|
||||||
{
|
{
|
||||||
var seriesIndex = SOURCE.IndexOf(series);
|
var seriesIndex = SOURCE.IndexOf(series);
|
||||||
foreach (var child in series.Children)
|
foreach (var child in series.Children)
|
||||||
|
|||||||
@ -44,6 +44,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public void LoadSettings(Configuration config)
|
public void LoadSettings(Configuration config)
|
||||||
{
|
{
|
||||||
CreateCueSheet = config.CreateCueSheet;
|
CreateCueSheet = config.CreateCueSheet;
|
||||||
|
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||||
AllowLibationFixup = config.AllowLibationFixup;
|
AllowLibationFixup = config.AllowLibationFixup;
|
||||||
DownloadCoverArt = config.DownloadCoverArt;
|
DownloadCoverArt = config.DownloadCoverArt;
|
||||||
RetainAaxFile = config.RetainAaxFile;
|
RetainAaxFile = config.RetainAaxFile;
|
||||||
@ -71,6 +72,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
{
|
{
|
||||||
config.CreateCueSheet = CreateCueSheet;
|
config.CreateCueSheet = CreateCueSheet;
|
||||||
|
config.CombineNestedChapterTitles = CombineNestedChapterTitles;
|
||||||
config.AllowLibationFixup = AllowLibationFixup;
|
config.AllowLibationFixup = AllowLibationFixup;
|
||||||
config.DownloadCoverArt = DownloadCoverArt;
|
config.DownloadCoverArt = DownloadCoverArt;
|
||||||
config.RetainAaxFile = RetainAaxFile;
|
config.RetainAaxFile = RetainAaxFile;
|
||||||
@ -99,7 +101,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
|
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
|
||||||
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
|
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
|
||||||
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
|
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
|
||||||
|
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles));
|
||||||
|
public string CombineNestedChapterTitlesTip => Configuration.GetHelpText(nameof(CombineNestedChapterTitles));
|
||||||
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
|
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
|
||||||
|
public string AllowLibationFixupTip => Configuration.GetHelpText(nameof(AllowLibationFixup));
|
||||||
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
|
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
|
||||||
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
|
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
|
||||||
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
|
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
|
||||||
@ -110,6 +115,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
||||||
|
|
||||||
public bool CreateCueSheet { get; set; }
|
public bool CreateCueSheet { get; set; }
|
||||||
|
public bool CombineNestedChapterTitles { get; set; }
|
||||||
public bool DownloadCoverArt { get; set; }
|
public bool DownloadCoverArt { get; set; }
|
||||||
public bool RetainAaxFile { get; set; }
|
public bool RetainAaxFile { get; set; }
|
||||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||||
|
|||||||
@ -38,6 +38,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
ChapterFileTemplate = config.ChapterFileTemplate;
|
ChapterFileTemplate = config.ChapterFileTemplate;
|
||||||
InProgressDirectory = config.InProgress;
|
InProgressDirectory = config.InProgress;
|
||||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||||
|
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
@ -54,9 +55,11 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
config.InProgress = InProgressDirectory;
|
config.InProgress = InProgressDirectory;
|
||||||
|
|
||||||
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
||||||
|
config.SaveMetadataToFile = SaveMetadataToFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||||
|
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
||||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
||||||
@ -72,6 +75,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
|
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
|
||||||
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
|
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
|
||||||
public bool UseCoverAsFolderIcon { get; set; }
|
public bool UseCoverAsFolderIcon { get; set; }
|
||||||
|
public bool SaveMetadataToFile { get; set; }
|
||||||
|
|
||||||
public bool BadBookAsk { get; set; }
|
public bool BadBookAsk { get; set; }
|
||||||
public bool BadBookAbort { get; set; }
|
public bool BadBookAbort { get; set; }
|
||||||
|
|||||||
@ -61,7 +61,7 @@ namespace LibationAvalonia.Views
|
|||||||
if (QuickFilters.UseDefault)
|
if (QuickFilters.UseDefault)
|
||||||
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|
||||||
ViewModel.ProductsDisplay.BindToGrid(dbBooks);
|
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectAndFocusSearchBox()
|
private void selectAndFocusSearchBox()
|
||||||
|
|||||||
@ -84,13 +84,13 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
<controls:DataGridTemplateColumnExt CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||||
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ namespace LibationAvalonia.Views
|
|||||||
catch { sampleEntries = new(); }
|
catch { sampleEntries = new(); }
|
||||||
|
|
||||||
var pdvm = new ProductsDisplayViewModel();
|
var pdvm = new ProductsDisplayViewModel();
|
||||||
pdvm.BindToGrid(sampleEntries);
|
_ = pdvm.BindToGridAsync(sampleEntries);
|
||||||
DataContext = pdvm;
|
DataContext = pdvm;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -78,9 +78,16 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
|
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
|
||||||
{
|
{
|
||||||
// stop light
|
if (args.Column.SortMemberPath is not "Liberate" and not "Cover")
|
||||||
if (args.Column.SortMemberPath == "Liberate")
|
|
||||||
{
|
{
|
||||||
|
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
|
||||||
|
|
||||||
|
menuItem.Click += async (s, e)
|
||||||
|
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
|
||||||
|
|
||||||
|
args.ContextMenuItems.Add(menuItem);
|
||||||
|
args.ContextMenuItems.Add(new Separator());
|
||||||
|
}
|
||||||
var entry = args.GridEntry;
|
var entry = args.GridEntry;
|
||||||
|
|
||||||
#region Liberate all Episodes
|
#region Liberate all Episodes
|
||||||
@ -195,6 +202,25 @@ namespace LibationAvalonia.Views
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Force Re-Download
|
||||||
|
if (!entry.Liberate.IsSeries)
|
||||||
|
{
|
||||||
|
var reDownloadMenuItem = new MenuItem()
|
||||||
|
{
|
||||||
|
Header = "Re-download this audiobook",
|
||||||
|
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||||
|
};
|
||||||
|
|
||||||
|
args.ContextMenuItems.Add(reDownloadMenuItem);
|
||||||
|
reDownloadMenuItem.Click += (s, _) =>
|
||||||
|
{
|
||||||
|
//No need to persist this change. It only needs to last long for the file to start downloading
|
||||||
|
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||||
|
LiberateClicked?.Invoke(s, entry.LibraryBook);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
args.ContextMenuItems.Add(new Separator());
|
args.ContextMenuItems.Add(new Separator());
|
||||||
|
|
||||||
#region View Bookmarks/Clips
|
#region View Bookmarks/Clips
|
||||||
@ -224,18 +250,6 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// any non-stop light column
|
|
||||||
// (except for the Cover column which does not have a context menu)
|
|
||||||
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
|
|
||||||
|
|
||||||
menuItem.Click += async (s, e)
|
|
||||||
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
|
|
||||||
|
|
||||||
args.ContextMenuItems.Add(menuItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LibationFileManager
|
||||||
|
{
|
||||||
|
public partial class Configuration
|
||||||
|
{
|
||||||
|
public static ReadOnlyDictionary<string, string> HelpText { get; } = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ nameof(CombineNestedChapterTitles),"""
|
||||||
|
If the book has nested chapters, e.g. a chapter named "Part 1"
|
||||||
|
that contains chapters "Chapter 1" and "Chapter 2", then combine
|
||||||
|
the chapter titles like the following example:
|
||||||
|
|
||||||
|
Part 1: Chapter 1
|
||||||
|
Part 1: Chapter 2
|
||||||
|
"""},
|
||||||
|
{nameof(AllowLibationFixup), """
|
||||||
|
In addition to the options that are enabled if you allow
|
||||||
|
"fixing up" the audiobook, it does the following:
|
||||||
|
|
||||||
|
* Sets the ©gen metadata tag for the genres.
|
||||||
|
* Adds the TCOM (@wrt in M4B files) metadata tag for the narrators.
|
||||||
|
* Unescapes the copyright symbol (replace © with ©)
|
||||||
|
* Replaces the recording copyright (P) string with ℗
|
||||||
|
* Adds various other metadata tags recognized by AudiobookShelf
|
||||||
|
* Sets the embedded cover art image with cover art retrieved from Audible
|
||||||
|
""" },
|
||||||
|
}
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
public static string GetHelpText(string settingName)
|
||||||
|
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -73,9 +73,12 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||||
|
|
||||||
[Description("Set cover art as the folder's icon. (Windows and macOS only)")]
|
[Description("Set cover art as the folder's icon.")]
|
||||||
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
|
[Description("Save audiobook metadata to metadata.json")]
|
||||||
|
public bool SaveMetadataToFile { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
@ -164,6 +167,9 @@ namespace LibationFileManager
|
|||||||
[Description("Save cover image alongside audiobook?")]
|
[Description("Save cover image alongside audiobook?")]
|
||||||
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
|
[Description("Combine nested chapter titles")]
|
||||||
|
public bool CombineNestedChapterTitles { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Download clips and bookmarks?")]
|
[Description("Download clips and bookmarks?")]
|
||||||
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationUiBase.GridView
|
namespace LibationUiBase.GridView
|
||||||
{
|
{
|
||||||
@ -29,6 +33,39 @@ namespace LibationUiBase.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
||||||
|
|
||||||
|
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||||
|
|
||||||
|
(int numPer, int rem) = int.DivRem(products.Length, parallelism);
|
||||||
|
if (rem != 0) numPer++;
|
||||||
|
|
||||||
|
var tasks = new Task<IGridEntry[]>[parallelism];
|
||||||
|
var syncContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
for (int i = 0; i < parallelism; i++)
|
||||||
|
{
|
||||||
|
int start = i * numPer;
|
||||||
|
tasks[i] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||||
|
|
||||||
|
int length = int.Min(numPer, products.Length - start);
|
||||||
|
var result = new IGridEntry[length];
|
||||||
|
|
||||||
|
for (int j = 0; j < length; j++)
|
||||||
|
result[j] = new LibraryBookEntry<TStatus>(products[start + j]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationUiBase.GridView
|
namespace LibationUiBase.GridView
|
||||||
{
|
{
|
||||||
@ -54,6 +58,60 @@ namespace LibationUiBase.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||||
|
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
|
||||||
|
|
||||||
|
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||||
|
|
||||||
|
var tasks = new Task[parallelism];
|
||||||
|
var syncContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
var q = new BlockingCollection<(int, LibraryBook episode)>();
|
||||||
|
|
||||||
|
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
|
||||||
|
var seriesEpisodes = new ConcurrentBag<ILibraryBookEntry>[seriesBooks.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < parallelism; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||||
|
|
||||||
|
while (q.TryTake(out var entry, -1))
|
||||||
|
{
|
||||||
|
var parent = seriesEntries[entry.Item1];
|
||||||
|
var episodeBag = seriesEpisodes[entry.Item1];
|
||||||
|
episodeBag.Add(new LibraryBookEntry<TStatus>(entry.episode, parent));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <seriesBooks.Length; i++)
|
||||||
|
{
|
||||||
|
var series = seriesBooks[i];
|
||||||
|
seriesEntries[i] = new SeriesEntry<TStatus>(series, Enumerable.Empty<LibraryBook>());
|
||||||
|
seriesEpisodes[i] = new ConcurrentBag<ILibraryBookEntry>();
|
||||||
|
|
||||||
|
foreach (var ep in allEpisodes.FindChildren(series))
|
||||||
|
q.Add((i, ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
q.CompleteAdding();
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
for (int i = 0; i < seriesBooks.Length; i++)
|
||||||
|
{
|
||||||
|
var series = seriesEntries[i];
|
||||||
|
series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder));
|
||||||
|
series.UpdateLibraryBook(series.LibraryBook);
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveChild(ILibraryBookEntry lbe)
|
public void RemoveChild(ILibraryBookEntry lbe)
|
||||||
{
|
{
|
||||||
Children.Remove(lbe);
|
Children.Remove(lbe);
|
||||||
|
|||||||
@ -14,12 +14,16 @@ namespace LibationWinForms.Dialogs
|
|||||||
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
|
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
|
||||||
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
|
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
|
||||||
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
||||||
|
this.combineNestedChapterTitlesCbox.Text = desc(nameof(config.CombineNestedChapterTitles));
|
||||||
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
||||||
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
|
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
|
||||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||||
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
|
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
|
||||||
|
|
||||||
|
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
|
||||||
|
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
|
||||||
|
|
||||||
fileDownloadQualityCb.Items.AddRange(
|
fileDownloadQualityCb.Items.AddRange(
|
||||||
new object[]
|
new object[]
|
||||||
{
|
{
|
||||||
@ -55,6 +59,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
|
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
|
||||||
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
|
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
|
||||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||||
|
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
|
||||||
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
||||||
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
|
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
|
||||||
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
||||||
@ -99,6 +104,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem;
|
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem;
|
||||||
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
|
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
|
||||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||||
|
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
|
||||||
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
||||||
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
|
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
|
||||||
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
||||||
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
||||||
useCoverAsFolderIconCb.Text = desc(nameof(config.UseCoverAsFolderIcon));
|
useCoverAsFolderIconCb.Text = desc(nameof(config.UseCoverAsFolderIcon));
|
||||||
|
saveMetadataToFileCbox.Text = desc(nameof(config.SaveMetadataToFile));
|
||||||
|
|
||||||
inProgressSelectControl.SetDirectoryItems(new()
|
inProgressSelectControl.SetDirectoryItems(new()
|
||||||
{
|
{
|
||||||
@ -60,6 +61,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
fileTemplateTb.Text = config.FileTemplate;
|
fileTemplateTb.Text = config.FileTemplate;
|
||||||
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
||||||
useCoverAsFolderIconCb.Checked = config.UseCoverAsFolderIcon;
|
useCoverAsFolderIconCb.Checked = config.UseCoverAsFolderIcon;
|
||||||
|
saveMetadataToFileCbox.Checked = config.SaveMetadataToFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save_DownloadDecrypt(Configuration config)
|
private void Save_DownloadDecrypt(Configuration config)
|
||||||
@ -77,6 +79,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
config.FileTemplate = fileTemplateTb.Text;
|
config.FileTemplate = fileTemplateTb.Text;
|
||||||
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
||||||
config.UseCoverAsFolderIcon = useCoverAsFolderIconCb.Checked;
|
config.UseCoverAsFolderIcon = useCoverAsFolderIconCb.Checked;
|
||||||
|
config.SaveMetadataToFile = saveMetadataToFileCbox.Checked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,12 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
private Configuration config { get; } = Configuration.Instance;
|
private Configuration config { get; } = Configuration.Instance;
|
||||||
private Func<string, string> desc { get; } = Configuration.GetDescription;
|
private Func<string, string> desc { get; } = Configuration.GetDescription;
|
||||||
|
private readonly ToolTip toolTip = new ToolTip
|
||||||
|
{
|
||||||
|
InitialDelay = 300,
|
||||||
|
AutoPopDelay = 10000,
|
||||||
|
ReshowDelay = 0
|
||||||
|
};
|
||||||
|
|
||||||
public SettingsDialog()
|
public SettingsDialog()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.Threading;
|
using Dinah.Core.Threading;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -14,7 +16,6 @@ namespace LibationWinForms
|
|||||||
beginBookBackupsToolStripMenuItem.Format(0);
|
beginBookBackupsToolStripMenuItem.Format(0);
|
||||||
beginPdfBackupsToolStripMenuItem.Format(0);
|
beginPdfBackupsToolStripMenuItem.Format(0);
|
||||||
|
|
||||||
Load += setBackupCounts;
|
|
||||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||||
|
|
||||||
@ -40,7 +41,11 @@ namespace LibationWinForms
|
|||||||
while (runBackupCountsAgain)
|
while (runBackupCountsAgain)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = false;
|
runBackupCountsAgain = false;
|
||||||
e.Result = LibraryCommands.GetCounts();
|
|
||||||
|
if (e.Argument is not IEnumerable<LibraryBook> lbs)
|
||||||
|
lbs = DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
|
|
||||||
|
e.Result = LibraryCommands.GetCounts(lbs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,8 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Dinah.Core;
|
using DataLayer;
|
||||||
using Dinah.Core.Threading;
|
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationWinForms.Dialogs;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -17,10 +15,6 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// Pre-requisite:
|
|
||||||
// Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs
|
|
||||||
using var _ = DbContexts.GetContext();
|
|
||||||
|
|
||||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
|
|
||||||
@ -57,8 +51,7 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
||||||
{
|
{
|
||||||
this.Load += (_, __) => productsDisplay.Display();
|
LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync());
|
||||||
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsDisplay.Display());
|
|
||||||
}
|
}
|
||||||
Shown += Form1_Shown;
|
Shown += Form1_Shown;
|
||||||
}
|
}
|
||||||
@ -78,6 +71,13 @@ namespace LibationWinForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
runBackupCountsAgain = true;
|
||||||
|
updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent()));
|
||||||
|
await productsDisplay.DisplayAsync(libraryBooks);
|
||||||
|
}
|
||||||
|
|
||||||
private void Form1_Load(object sender, EventArgs e)
|
private void Form1_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (this.DesignMode)
|
if (this.DesignMode)
|
||||||
|
|||||||
@ -30,7 +30,6 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||||
ListChanged += GridEntryBindingList_ListChanged;
|
ListChanged += GridEntryBindingList_ListChanged;
|
||||||
refreshEntries();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>All items in the list, including those filtered out.</returns>
|
/// <returns>All items in the list, including those filtered out.</returns>
|
||||||
|
|||||||
@ -206,6 +206,25 @@ namespace LibationWinForms.GridView
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Force Re-Download
|
||||||
|
if (!entry.Liberate.IsSeries)
|
||||||
|
{
|
||||||
|
var reDownloadMenuItem = new ToolStripMenuItem()
|
||||||
|
{
|
||||||
|
Text = "Re-download this audiobook",
|
||||||
|
Enabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||||
|
};
|
||||||
|
|
||||||
|
ctxMenu.Items.Add(reDownloadMenuItem);
|
||||||
|
reDownloadMenuItem.Click += (s, _) =>
|
||||||
|
{
|
||||||
|
//No need to persist this change. It only needs to last long for the file to start downloading
|
||||||
|
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||||
|
LiberateClicked?.Invoke(s, entry.LibraryBook);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
ctxMenu.Items.Add(new ToolStripSeparator());
|
ctxMenu.Items.Add(new ToolStripSeparator());
|
||||||
|
|
||||||
#region View Bookmarks/Clips
|
#region View Bookmarks/Clips
|
||||||
@ -306,22 +325,22 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
#region UI display functions
|
#region UI display functions
|
||||||
|
|
||||||
public void Display()
|
public async Task DisplayAsync(List<LibraryBook> libraryBooks = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
||||||
var lib = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
|
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||||
|
|
||||||
if (!hasBeenDisplayed)
|
if (!hasBeenDisplayed)
|
||||||
{
|
{
|
||||||
// bind
|
// bind
|
||||||
productsGrid.BindToGrid(lib);
|
await productsGrid.BindToGridAsync(libraryBooks);
|
||||||
hasBeenDisplayed = true;
|
hasBeenDisplayed = true;
|
||||||
InitialLoaded?.Invoke(this, new());
|
InitialLoaded?.Invoke(this, new());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
productsGrid.UpdateGrid(lib);
|
productsGrid.UpdateGrid(libraryBooks);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,8 +5,11 @@ using LibationUiBase.GridView;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
@ -53,15 +56,11 @@ namespace LibationWinForms.GridView
|
|||||||
if (e.RowIndex < 0)
|
if (e.RowIndex < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// cover
|
|
||||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
|
||||||
return;
|
|
||||||
|
|
||||||
e.ContextMenuStrip = new ContextMenuStrip();
|
e.ContextMenuStrip = new ContextMenuStrip();
|
||||||
// any non-stop light
|
// any column except cover & stop light
|
||||||
if (e.ColumnIndex != liberateGVColumn.Index)
|
if (e.ColumnIndex != liberateGVColumn.Index && e.ColumnIndex != coverGVColumn.Index)
|
||||||
{
|
{
|
||||||
e.ContextMenuStrip.Items.Add("Copy", null, (_, __) =>
|
e.ContextMenuStrip.Items.Add("Copy Cell Contents", null, (_, __) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -71,14 +70,13 @@ namespace LibationWinForms.GridView
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
|
e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var entry = getGridEntry(e.RowIndex);
|
var entry = getGridEntry(e.RowIndex);
|
||||||
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
|
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
|
||||||
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
|
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableDoubleBuffering()
|
private void EnableDoubleBuffering()
|
||||||
{
|
{
|
||||||
@ -160,27 +158,23 @@ namespace LibationWinForms.GridView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = dbBooks
|
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||||
.Where(lb => lb.Book.IsProduct())
|
|
||||||
.Select(b => new LibraryBookEntry<WinFormsEntryStatus>(b))
|
|
||||||
.ToList<IGridEntry>();
|
|
||||||
|
|
||||||
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
|
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||||
|
|
||||||
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
|
geList.AddRange(seriesEntries);
|
||||||
|
//Sort descending by date (default sort property)
|
||||||
|
var comparer = new RowComparer();
|
||||||
|
geList.Sort((a, b) => comparer.Compare(b, a));
|
||||||
|
|
||||||
foreach (var parent in seriesBooks)
|
//Add all children beneath their parent
|
||||||
|
foreach (var series in seriesEntries)
|
||||||
{
|
{
|
||||||
var seriesEpisodes = episodes.FindChildren(parent);
|
var seriesIndex = geList.IndexOf(series);
|
||||||
|
foreach (var child in series.Children)
|
||||||
if (!seriesEpisodes.Any()) continue;
|
geList.Insert(++seriesIndex, child);
|
||||||
|
|
||||||
var seriesEntry = new SeriesEntry<WinFormsEntryStatus>(parent, seriesEpisodes);
|
|
||||||
|
|
||||||
geList.Add(seriesEntry);
|
|
||||||
geList.AddRange(seriesEntry.Children);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingList = new GridEntryBindingList(geList);
|
bindingList = new GridEntryBindingList(geList);
|
||||||
|
|||||||
@ -249,9 +249,12 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
byte[] coverData = PictureStorage
|
var quality
|
||||||
.GetPictureSynchronously(
|
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
|
||||||
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
|
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||||
|
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||||
|
|
||||||
|
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
|
||||||
|
|
||||||
AudioDecodable_CoverImageDiscovered(this, coverData);
|
AudioDecodable_CoverImageDiscovered(this, coverData);
|
||||||
return coverData;
|
return coverData;
|
||||||
|
|||||||
@ -2,12 +2,13 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using ApplicationServices;
|
||||||
using AppScaffolding;
|
using AppScaffolding;
|
||||||
using Dinah.Core;
|
using DataLayer;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationWinForms.Dialogs;
|
using LibationWinForms.Dialogs;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -20,6 +21,7 @@ namespace LibationWinForms
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main()
|
static void Main()
|
||||||
{
|
{
|
||||||
|
Task<List<LibraryBook>> libraryLoadTask;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//// Uncomment to see Console. Must be called before anything writes to Console.
|
//// Uncomment to see Console. Must be called before anything writes to Console.
|
||||||
@ -48,6 +50,17 @@ namespace LibationWinForms
|
|||||||
// migrations which require Forms or are long-running
|
// migrations which require Forms or are long-running
|
||||||
RunWindowsOnlyMigrations(config);
|
RunWindowsOnlyMigrations(config);
|
||||||
|
|
||||||
|
//*******************************************************************//
|
||||||
|
// //
|
||||||
|
// Start loading the library as soon as possible //
|
||||||
|
// //
|
||||||
|
// Before calling anything else, including subscribing to events, //
|
||||||
|
// to ensure database exists. If we wait and let it happen lazily, //
|
||||||
|
// race conditions and errors are likely during new installs //
|
||||||
|
// //
|
||||||
|
//*******************************************************************//
|
||||||
|
libraryLoadTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||||
|
|
||||||
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
||||||
|
|
||||||
// logging is init'd here
|
// logging is init'd here
|
||||||
@ -71,7 +84,9 @@ namespace LibationWinForms
|
|||||||
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
|
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
|
||||||
postLoggingGlobalExceptionHandling();
|
postLoggingGlobalExceptionHandling();
|
||||||
|
|
||||||
Application.Run(new Form1());
|
var form1 = new Form1();
|
||||||
|
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
|
||||||
|
Application.Run(form1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunInstaller(Configuration config)
|
private static void RunInstaller(Configuration config)
|
||||||
|
|||||||
@ -540,7 +540,6 @@ namespace FileLiberator.Tests
|
|||||||
value[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs);
|
value[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs);
|
||||||
value[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec);
|
value[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec);
|
||||||
value[i].LengthMs.Should().Be(expected[i].LengthMs);
|
value[i].LengthMs.Should().Be(expected[i].LengthMs);
|
||||||
value[i].Chapters.Should().BeNull();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user