Merge pull request #277 from Mbucari/master
Issues #183 and #186, and a lot of other little things
This commit is contained in:
commit
7baefe2f44
@ -5,8 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AAXClean" Version="0.4.7" />
|
<PackageReference Include="AAXClean.Codecs" Version="0.2.9" />
|
||||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using AAXClean;
|
using AAXClean;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
|
|
||||||
@ -10,8 +11,8 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
protected AaxFile AaxFile;
|
protected AaxFile AaxFile;
|
||||||
|
|
||||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
: base(outFileName, cacheDirectory, dlLic) { }
|
: base(outFileName, cacheDirectory, dlOptions) { }
|
||||||
|
|
||||||
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
|
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
|
||||||
public override void SetCoverArt(byte[] coverArt)
|
public override void SetCoverArt(byte[] coverArt)
|
||||||
@ -109,10 +110,11 @@ namespace AaxDecrypter
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Cancel()
|
public override async Task CancelAsync()
|
||||||
{
|
{
|
||||||
IsCanceled = true;
|
IsCanceled = true;
|
||||||
AaxFile?.Cancel();
|
if (AaxFile != null)
|
||||||
|
await AaxFile.CancelAsync();
|
||||||
AaxFile?.Dispose();
|
AaxFile?.Dispose();
|
||||||
CloseInputFileStream();
|
CloseInputFileStream();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AAXClean;
|
using AAXClean;
|
||||||
using AAXClean.Codecs;
|
using AAXClean.Codecs;
|
||||||
|
using Dinah.Core;
|
||||||
using Dinah.Core.StepRunner;
|
using Dinah.Core.StepRunner;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
|
|
||||||
@ -12,15 +13,11 @@ namespace AaxDecrypter
|
|||||||
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
||||||
{
|
{
|
||||||
protected override StepSequence Steps { get; }
|
protected override StepSequence Steps { get; }
|
||||||
|
|
||||||
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
|
|
||||||
|
|
||||||
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
||||||
private List<string> multiPartFilePaths { get; } = new List<string>();
|
private List<string> multiPartFilePaths { get; } = new List<string>();
|
||||||
|
|
||||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic,
|
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
|
: base(outFileName, cacheDirectory, dlOptions)
|
||||||
: base(outFileName, cacheDirectory, dlLic)
|
|
||||||
{
|
{
|
||||||
Steps = new StepSequence
|
Steps = new StepSequence
|
||||||
{
|
{
|
||||||
@ -30,7 +27,6 @@ namespace AaxDecrypter
|
|||||||
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
|
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
|
||||||
["Step 3: Cleanup"] = Step_Cleanup,
|
["Step 3: Cleanup"] = Step_Cleanup,
|
||||||
};
|
};
|
||||||
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -61,7 +57,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
{
|
{
|
||||||
var zeroProgress = Step_DownloadAudiobook_Start();
|
var zeroProgress = Step_DownloadAudiobook_Start();
|
||||||
|
|
||||||
var chapters = DownloadOptions.ChapterInfo.Chapters.ToList();
|
var chapters = DownloadOptions.ChapterInfo.Chapters;
|
||||||
|
|
||||||
// Ensure split files are at least minChapterLength in duration.
|
// Ensure split files are at least minChapterLength in duration.
|
||||||
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
|
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
|
||||||
@ -103,39 +99,57 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
|
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||||
{
|
{
|
||||||
var chapterCount = 0;
|
var chapterCount = 0;
|
||||||
return AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
|
return AaxFile.ConvertToMultiMp4a
|
||||||
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback),
|
(
|
||||||
DownloadOptions.TrimOutputToChapterLength);
|
splitChapters,
|
||||||
|
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
|
||||||
|
DownloadOptions.TrimOutputToChapterLength
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
|
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||||
{
|
{
|
||||||
var chapterCount = 0;
|
var chapterCount = 0;
|
||||||
return AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
|
return AaxFile.ConvertToMultiMp3
|
||||||
{
|
(
|
||||||
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
|
splitChapters,
|
||||||
((NAudio.Lame.LameConfig)newSplitCallback.UserState).ID3.Track = chapterCount.ToString();
|
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
|
||||||
}, DownloadOptions.LameConfig, DownloadOptions.TrimOutputToChapterLength);
|
DownloadOptions.LameConfig,
|
||||||
|
DownloadOptions.TrimOutputToChapterLength
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
|
||||||
|
private void Callback(int currentChapter, ChapterInfo splitChapters, NewMP3SplitCallback newSplitCallback)
|
||||||
|
=> Callback(currentChapter, splitChapters, newSplitCallback);
|
||||||
|
|
||||||
|
private void Callback(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
||||||
{
|
{
|
||||||
var fileName = multipartFileNameCallback(new()
|
MultiConvertFileProperties props = new()
|
||||||
{
|
{
|
||||||
OutputFileName = OutputFileName,
|
OutputFileName = OutputFileName,
|
||||||
PartsPosition = currentChapter,
|
PartsPosition = currentChapter,
|
||||||
PartsTotal = splitChapters.Count,
|
PartsTotal = splitChapters.Count,
|
||||||
Title = newSplitCallback?.Chapter?.Title
|
Title = newSplitCallback?.Chapter?.Title,
|
||||||
});
|
};
|
||||||
|
newSplitCallback.OutputFile = createOutputFileStream(props);
|
||||||
|
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitleName(props);
|
||||||
|
newSplitCallback.TrackNumber = currentChapter;
|
||||||
|
newSplitCallback.TrackCount = splitChapters.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
|
||||||
|
{
|
||||||
|
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
|
||||||
fileName = FileUtility.GetValidFilename(fileName);
|
fileName = FileUtility.GetValidFilename(fileName);
|
||||||
|
|
||||||
multiPartFilePaths.Add(fileName);
|
multiPartFilePaths.Add(fileName);
|
||||||
|
|
||||||
FileUtility.SaferDelete(fileName);
|
FileUtility.SaferDelete(fileName);
|
||||||
|
|
||||||
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
|
var file = File.Open(fileName, FileMode.OpenOrCreate);
|
||||||
|
|
||||||
OnFileCreated(fileName);
|
OnFileCreated(fileName);
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
protected override StepSequence Steps { get; }
|
protected override StepSequence Steps { get; }
|
||||||
|
|
||||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
: base(outFileName, cacheDirectory, dlLic)
|
: base(outFileName, cacheDirectory, dlOptions)
|
||||||
{
|
{
|
||||||
Steps = new StepSequence
|
Steps = new StepSequence
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
using Dinah.Core.StepRunner;
|
using Dinah.Core.StepRunner;
|
||||||
@ -21,9 +22,10 @@ namespace AaxDecrypter
|
|||||||
public event EventHandler<string> FileCreated;
|
public event EventHandler<string> FileCreated;
|
||||||
|
|
||||||
public bool IsCanceled { get; set; }
|
public bool IsCanceled { get; set; }
|
||||||
|
public string TempFilePath { get; }
|
||||||
|
|
||||||
protected string OutputFileName { get; private set; }
|
protected string OutputFileName { get; private set; }
|
||||||
protected DownloadOptions DownloadOptions { get; }
|
protected IDownloadOptions DownloadOptions { get; }
|
||||||
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
|
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
|
||||||
|
|
||||||
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
|
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
|
||||||
@ -33,29 +35,28 @@ namespace AaxDecrypter
|
|||||||
private NetworkFileStreamPersister nfsPersister;
|
private NetworkFileStreamPersister nfsPersister;
|
||||||
|
|
||||||
private string jsonDownloadState { get; }
|
private string jsonDownloadState { get; }
|
||||||
public string TempFilePath { get; }
|
|
||||||
|
|
||||||
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
{
|
{
|
||||||
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||||
|
|
||||||
var outDir = Path.GetDirectoryName(OutputFileName);
|
var outDir = Path.GetDirectoryName(OutputFileName);
|
||||||
if (!Directory.Exists(outDir))
|
if (!Directory.Exists(outDir))
|
||||||
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(outDir)}");
|
Directory.CreateDirectory(outDir);
|
||||||
|
|
||||||
if (!Directory.Exists(cacheDirectory))
|
if (!Directory.Exists(cacheDirectory))
|
||||||
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
|
Directory.CreateDirectory(cacheDirectory);
|
||||||
|
|
||||||
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
|
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
|
||||||
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
|
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
|
||||||
|
|
||||||
DownloadOptions = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
|
||||||
|
|
||||||
// delete file after validation is complete
|
// delete file after validation is complete
|
||||||
FileUtility.SaferDelete(OutputFileName);
|
FileUtility.SaferDelete(OutputFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Cancel();
|
public abstract Task CancelAsync();
|
||||||
|
|
||||||
public virtual void SetCoverArt(byte[] coverArt)
|
public virtual void SetCoverArt(byte[] coverArt)
|
||||||
{
|
{
|
||||||
@ -65,7 +66,7 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
public bool Run()
|
public bool Run()
|
||||||
{
|
{
|
||||||
var (IsSuccess, Elapsed) = Steps.Run();
|
var (IsSuccess, _) = Steps.Run();
|
||||||
|
|
||||||
if (!IsSuccess)
|
if (!IsSuccess)
|
||||||
Serilog.Log.Logger.Error("Conversion failed");
|
Serilog.Log.Logger.Error("Conversion failed");
|
||||||
@ -79,10 +80,8 @@ namespace AaxDecrypter
|
|||||||
=> RetrievedAuthors?.Invoke(this, authors);
|
=> RetrievedAuthors?.Invoke(this, authors);
|
||||||
protected void OnRetrievedNarrators(string narrators)
|
protected void OnRetrievedNarrators(string narrators)
|
||||||
=> RetrievedNarrators?.Invoke(this, narrators);
|
=> RetrievedNarrators?.Invoke(this, narrators);
|
||||||
|
|
||||||
protected void OnRetrievedCoverArt(byte[] coverArt)
|
protected void OnRetrievedCoverArt(byte[] coverArt)
|
||||||
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
||||||
|
|
||||||
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
||||||
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
||||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
using AAXClean;
|
|
||||||
using Dinah.Core;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
|
||||||
{
|
|
||||||
public class DownloadOptions
|
|
||||||
{
|
|
||||||
public string DownloadUrl { get; }
|
|
||||||
public string UserAgent { get; }
|
|
||||||
public string AudibleKey { get; init; }
|
|
||||||
public string AudibleIV { get; init; }
|
|
||||||
public OutputFormat OutputFormat { get; init; }
|
|
||||||
public bool TrimOutputToChapterLength { get; init; }
|
|
||||||
public bool RetainEncryptedFile { get; init; }
|
|
||||||
public bool StripUnabridged { get; init; }
|
|
||||||
public bool CreateCueSheet { get; init; }
|
|
||||||
public ChapterInfo ChapterInfo { get; set; }
|
|
||||||
public NAudio.Lame.LameConfig LameConfig { get; set; }
|
|
||||||
public bool Downsample { get; set; }
|
|
||||||
public bool MatchSourceBitrate { get; set; }
|
|
||||||
|
|
||||||
public DownloadOptions(string downloadUrl, string userAgent)
|
|
||||||
{
|
|
||||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
|
||||||
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
|
||||||
|
|
||||||
// no null/empty check for key/iv. unencrypted files do not have them
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
Source/AaxDecrypter/IDownloadOptions.cs
Normal file
23
Source/AaxDecrypter/IDownloadOptions.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using AAXClean;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public interface IDownloadOptions
|
||||||
|
{
|
||||||
|
string DownloadUrl { get; }
|
||||||
|
string UserAgent { get; }
|
||||||
|
string AudibleKey { get; }
|
||||||
|
string AudibleIV { get; }
|
||||||
|
OutputFormat OutputFormat { get; }
|
||||||
|
bool TrimOutputToChapterLength { get; }
|
||||||
|
bool RetainEncryptedFile { get; }
|
||||||
|
bool StripUnabridged { get; }
|
||||||
|
bool CreateCueSheet { get; }
|
||||||
|
ChapterInfo ChapterInfo { get; set; }
|
||||||
|
NAudio.Lame.LameConfig LameConfig { get; set; }
|
||||||
|
bool Downsample { get; set; }
|
||||||
|
bool MatchSourceBitrate { get; set; }
|
||||||
|
string GetMultipartFileName(MultiConvertFileProperties props);
|
||||||
|
string GetMultipartTitleName(MultiConvertFileProperties props);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
using Dinah.Core.StepRunner;
|
using Dinah.Core.StepRunner;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
@ -10,7 +11,7 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
protected override StepSequence Steps { get; }
|
protected override StepSequence Steps { get; }
|
||||||
|
|
||||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadOptions dlLic)
|
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
|
||||||
: base(outFileName, cacheDirectory, dlLic)
|
: base(outFileName, cacheDirectory, dlLic)
|
||||||
{
|
{
|
||||||
Steps = new StepSequence
|
Steps = new StepSequence
|
||||||
@ -24,10 +25,11 @@ namespace AaxDecrypter
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Cancel()
|
public override Task CancelAsync()
|
||||||
{
|
{
|
||||||
IsCanceled = true;
|
IsCanceled = true;
|
||||||
CloseInputFileStream();
|
CloseInputFileStream();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool Step_GetMetadata()
|
protected bool Step_GetMetadata()
|
||||||
|
|||||||
@ -126,6 +126,9 @@ namespace AppScaffolding
|
|||||||
if (!config.Exists(nameof(config.ChapterFileTemplate)))
|
if (!config.Exists(nameof(config.ChapterFileTemplate)))
|
||||||
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
|
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
|
||||||
|
|
||||||
|
if (!config.Exists(nameof(config.ChapterTitleTemplate)))
|
||||||
|
config.ChapterTitleTemplate = Templates.ChapterTitle.DefaultTemplate;
|
||||||
|
|
||||||
if (!config.Exists(nameof(config.AutoScan)))
|
if (!config.Exists(nameof(config.AutoScan)))
|
||||||
config.AutoScan = true;
|
config.AutoScan = true;
|
||||||
|
|
||||||
|
|||||||
@ -128,7 +128,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
|
|
||||||
Log.Logger.Information("Begin scan for orphaned episode parents");
|
Log.Logger.Information("Begin scan for orphaned episode parents");
|
||||||
var newParents = await findAndAddMissingParents(apiExtendedfunc, accounts);
|
var newParents = await findAndAddMissingParents(accounts);
|
||||||
Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}");
|
Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}");
|
||||||
|
|
||||||
if (newParents >= 0)
|
if (newParents >= 0)
|
||||||
@ -255,7 +255,7 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task<int> findAndAddMissingParents(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts)
|
static async Task<int> findAndAddMissingParents(Account[] accounts)
|
||||||
{
|
{
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
@ -274,11 +274,11 @@ namespace ApplicationServices
|
|||||||
.DistinctBy(s => s.Series.AudibleSeriesId)
|
.DistinctBy(s => s.Series.AudibleSeriesId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// We're only calling the Catalog endpoint, so it doesn't matter which account we use.
|
// The Catalog endpointdoes not require authentication.
|
||||||
var apiExtended = await apiExtendedfunc(accounts[0]);
|
var api = new ApiUnauthenticated(accounts[0].Locale);
|
||||||
|
|
||||||
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
|
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
|
||||||
var items = await apiExtended.Api.GetCatalogProductsAsync(seriesParents, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
var items = await api.GetCatalogProductsAsync(seriesParents, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
|
|
||||||
List<ImportItem> newParentsImportItems = new();
|
List<ImportItem> newParentsImportItems = new();
|
||||||
foreach (var sp in orphanedSeries)
|
foreach (var sp in orphanedSeries)
|
||||||
|
|||||||
@ -137,7 +137,7 @@ namespace AudibleUtilities
|
|||||||
//Get child episodes asynchronously and await all at the end
|
//Get child episodes asynchronously and await all at the end
|
||||||
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
||||||
}
|
}
|
||||||
else if (!item.IsEpisodes)
|
else if (!item.IsEpisodes && !item.IsSeriesParent)
|
||||||
items.Add(item);
|
items.Add(item);
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using LibationFileManager;
|
||||||
|
using NAudio.Lame;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
@ -10,8 +13,32 @@ namespace FileLiberator
|
|||||||
public event EventHandler<string> AuthorsDiscovered;
|
public event EventHandler<string> AuthorsDiscovered;
|
||||||
public event EventHandler<string> NarratorsDiscovered;
|
public event EventHandler<string> NarratorsDiscovered;
|
||||||
public event EventHandler<byte[]> CoverImageDiscovered;
|
public event EventHandler<byte[]> CoverImageDiscovered;
|
||||||
public abstract void Cancel();
|
public abstract Task CancelAsync();
|
||||||
|
|
||||||
|
protected LameConfig GetLameOptions(Configuration config)
|
||||||
|
{
|
||||||
|
LameConfig lameConfig = new();
|
||||||
|
lameConfig.Mode = MPEGMode.Mono;
|
||||||
|
|
||||||
|
if (config.LameTargetBitrate)
|
||||||
|
{
|
||||||
|
if (config.LameConstantBitrate)
|
||||||
|
lameConfig.BitRate = config.LameBitrate;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lameConfig.ABRRateKbps = config.LameBitrate;
|
||||||
|
lameConfig.VBR = VBRMode.ABR;
|
||||||
|
lameConfig.WriteVBRTag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lameConfig.VBR = VBRMode.Default;
|
||||||
|
lameConfig.VBRQuality = config.LameVBRQuality;
|
||||||
|
lameConfig.WriteVBRTag = true;
|
||||||
|
}
|
||||||
|
return lameConfig;
|
||||||
|
}
|
||||||
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
|
||||||
protected void OnTitleDiscovered(object _, string title)
|
protected void OnTitleDiscovered(object _, string title)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,19 +8,6 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
public static class AudioFileStorageExt
|
public static class AudioFileStorageExt
|
||||||
{
|
{
|
||||||
private class MultipartRenamer
|
|
||||||
{
|
|
||||||
private LibraryBook libraryBook { get; }
|
|
||||||
|
|
||||||
internal MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
|
||||||
|
|
||||||
internal string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props)
|
|
||||||
=> Templates.ChapterFile.GetFilename(libraryBook.ToDto(), props);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
|
|
||||||
=> new MultipartRenamer(libraryBook).MultipartFilename;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DownloadDecryptBook:
|
/// DownloadDecryptBook:
|
||||||
/// File path for where to move files into.
|
/// File path for where to move files into.
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AAXClean;
|
using AAXClean;
|
||||||
using AAXClean.Codecs;
|
using AAXClean.Codecs;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
@ -20,17 +20,12 @@ namespace FileLiberator
|
|||||||
private long fileSize;
|
private long fileSize;
|
||||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||||
|
|
||||||
private bool cancelled = false;
|
public override Task CancelAsync() => m4bBook?.CancelAsync() ?? Task.CompletedTask;
|
||||||
public override void Cancel()
|
|
||||||
{
|
|
||||||
m4bBook?.Cancel();
|
|
||||||
cancelled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
var paths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId);
|
||||||
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
|
return paths.Any(path => path?.ToString()?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
|
||||||
@ -39,11 +34,15 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
OnBegin(libraryBook);
|
OnBegin(libraryBook);
|
||||||
|
|
||||||
OnStreamingBegin($"Begin converting {libraryBook} to mp3");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId);
|
||||||
|
|
||||||
|
foreach (var m4bPath in m4bPaths)
|
||||||
|
{
|
||||||
|
var proposedMp3Path = Mp3FileName(m4bPath);
|
||||||
|
if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue;
|
||||||
|
|
||||||
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
||||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||||
|
|
||||||
@ -55,25 +54,29 @@ namespace FileLiberator
|
|||||||
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
||||||
|
|
||||||
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
||||||
|
var lameConfig = GetLameOptions(Configuration.Instance);
|
||||||
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File));
|
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File, lameConfig));
|
||||||
m4bBook.InputStream.Close();
|
m4bBook.InputStream.Close();
|
||||||
mp3File.Close();
|
mp3File.Close();
|
||||||
|
|
||||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
if (result == ConversionResult.Failed)
|
||||||
|
{
|
||||||
|
FileUtility.SaferDelete(mp3File.Name);
|
||||||
|
return new StatusHandler { "Conversion failed" };
|
||||||
|
}
|
||||||
|
else if (result == ConversionResult.Cancelled)
|
||||||
|
{
|
||||||
|
FileUtility.SaferDelete(mp3File.Name);
|
||||||
|
return new StatusHandler { "Cancelled" };
|
||||||
|
}
|
||||||
|
|
||||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
||||||
OnFileCreated(libraryBook, realMp3Path);
|
OnFileCreated(libraryBook, realMp3Path);
|
||||||
|
}
|
||||||
if (result == ConversionResult.Failed)
|
|
||||||
return new StatusHandler { "Conversion failed" };
|
|
||||||
else if (result == ConversionResult.Cancelled)
|
|
||||||
return new StatusHandler { "Cancelled" };
|
|
||||||
else
|
|
||||||
return new StatusHandler();
|
return new StatusHandler();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
OnStreamingCompleted($"Completed converting to mp3: {libraryBook.Book.Title}");
|
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ namespace FileLiberator
|
|||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||||
|
|
||||||
public override void Cancel() => abDownloader?.Cancel();
|
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask;
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
@ -94,10 +94,6 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||||
{
|
|
||||||
OnStreamingBegin($"Begin decrypting {libraryBook}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
|
|
||||||
@ -105,20 +101,19 @@ namespace FileLiberator
|
|||||||
|
|
||||||
var api = await libraryBook.GetApiAsync();
|
var api = await libraryBook.GetApiAsync();
|
||||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||||
var audiobookDlLic = BuildDownloadOptions(config, contentLic);
|
var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||||
|
|
||||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.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 != AudibleApi.Common.DrmType.Adrm)
|
||||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
|
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AaxcDownloadConvertBase converter
|
AaxcDownloadConvertBase converter
|
||||||
= config.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
|
= config.SplitFilesByChapter ?
|
||||||
outFileName, cacheDir, audiobookDlLic,
|
new AaxcDownloadMultiConverter(outFileName, cacheDir, dlOptions) :
|
||||||
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook))
|
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
|
||||||
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic);
|
|
||||||
|
|
||||||
if (config.AllowLibationFixup)
|
if (config.AllowLibationFixup)
|
||||||
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
|
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
|
||||||
@ -139,13 +134,8 @@ namespace FileLiberator
|
|||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
OnStreamingCompleted($"Completed downloading and decrypting {libraryBook.Book.Title}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||||
{
|
{
|
||||||
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
||||||
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
||||||
@ -156,8 +146,9 @@ namespace FileLiberator
|
|||||||
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
||||||
OutputFormat.Mp3 : OutputFormat.M4b;
|
OutputFormat.Mp3 : OutputFormat.M4b;
|
||||||
|
|
||||||
var audiobookDlLic = new DownloadOptions
|
var dlOptions = new DownloadOptions
|
||||||
(
|
(
|
||||||
|
libraryBook,
|
||||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||||
Resources.USER_AGENT
|
Resources.USER_AGENT
|
||||||
)
|
)
|
||||||
@ -170,16 +161,16 @@ namespace FileLiberator
|
|||||||
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
||||||
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
||||||
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
||||||
CreateCueSheet = config.CreateCueSheet
|
CreateCueSheet = config.CreateCueSheet,
|
||||||
|
LameConfig = GetLameOptions(config)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
|
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
|
||||||
{
|
{
|
||||||
|
long startMs = dlOptions.TrimOutputToChapterLength ?
|
||||||
long startMs = audiobookDlLic.TrimOutputToChapterLength ?
|
|
||||||
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
||||||
|
|
||||||
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs));
|
dlOptions.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs));
|
||||||
|
|
||||||
for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++)
|
for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++)
|
||||||
{
|
{
|
||||||
@ -192,32 +183,11 @@ namespace FileLiberator
|
|||||||
if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1)
|
if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1)
|
||||||
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||||
|
|
||||||
audiobookDlLic.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audiobookDlLic.LameConfig = new();
|
return dlOptions;
|
||||||
audiobookDlLic.LameConfig.Mode = NAudio.Lame.MPEGMode.Mono;
|
|
||||||
|
|
||||||
if (config.LameTargetBitrate)
|
|
||||||
{
|
|
||||||
if (config.LameConstantBitrate)
|
|
||||||
audiobookDlLic.LameConfig.BitRate = config.LameBitrate;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
audiobookDlLic.LameConfig.ABRRateKbps = config.LameBitrate;
|
|
||||||
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.ABR;
|
|
||||||
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
audiobookDlLic.LameConfig.VBR = NAudio.Lame.VBRMode.Default;
|
|
||||||
audiobookDlLic.LameConfig.VBRQuality = config.LameVBRQuality;
|
|
||||||
audiobookDlLic.LameConfig.WriteVBRTag = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return audiobookDlLic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void downloadValidation(LibraryBook libraryBook)
|
private static void downloadValidation(LibraryBook libraryBook)
|
||||||
|
|||||||
44
Source/FileLiberator/DownloadOptions.cs
Normal file
44
Source/FileLiberator/DownloadOptions.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using AaxDecrypter;
|
||||||
|
using AAXClean;
|
||||||
|
using Dinah.Core;
|
||||||
|
using DataLayer;
|
||||||
|
using LibationFileManager;
|
||||||
|
|
||||||
|
namespace FileLiberator
|
||||||
|
{
|
||||||
|
public class DownloadOptions : IDownloadOptions
|
||||||
|
{
|
||||||
|
public LibraryBook LibraryBook { get; }
|
||||||
|
public LibraryBookDto LibraryBookDto { get; }
|
||||||
|
public string DownloadUrl { get; }
|
||||||
|
public string UserAgent { get; }
|
||||||
|
public string AudibleKey { get; init; }
|
||||||
|
public string AudibleIV { get; init; }
|
||||||
|
public AaxDecrypter.OutputFormat OutputFormat { get; init; }
|
||||||
|
public bool TrimOutputToChapterLength { get; init; }
|
||||||
|
public bool RetainEncryptedFile { get; init; }
|
||||||
|
public bool StripUnabridged { get; init; }
|
||||||
|
public bool CreateCueSheet { get; init; }
|
||||||
|
public ChapterInfo ChapterInfo { get; set; }
|
||||||
|
public NAudio.Lame.LameConfig LameConfig { get; set; }
|
||||||
|
public bool Downsample { get; set; }
|
||||||
|
public bool MatchSourceBitrate { get; set; }
|
||||||
|
|
||||||
|
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||||
|
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||||
|
|
||||||
|
public string GetMultipartTitleName(MultiConvertFileProperties props)
|
||||||
|
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
|
||||||
|
|
||||||
|
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
|
||||||
|
{
|
||||||
|
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||||
|
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||||
|
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||||
|
|
||||||
|
LibraryBookDto = LibraryBook.ToDto();
|
||||||
|
|
||||||
|
// no null/empty check for key/iv. unencrypted files do not have them
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,10 +56,6 @@ namespace FileLiberator
|
|||||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||||
|
|
||||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||||
{
|
|
||||||
OnStreamingBegin(proposedDownloadFilePath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var api = await libraryBook.GetApiAsync();
|
var api = await libraryBook.GetApiAsync();
|
||||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||||
@ -74,11 +70,6 @@ namespace FileLiberator
|
|||||||
OnStatusUpdate(actualDownloadedFilePath);
|
OnStatusUpdate(actualDownloadedFilePath);
|
||||||
return actualDownloadedFilePath;
|
return actualDownloadedFilePath;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
OnStreamingCompleted(proposedDownloadFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||||
=> !File.Exists(actualDownloadedFilePath)
|
=> !File.Exists(actualDownloadedFilePath)
|
||||||
|
|||||||
@ -5,17 +5,22 @@ using System.Threading.Tasks;
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
|
using Dinah.Core.Net.Http;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public abstract class Processable : Streamable
|
public abstract class Processable
|
||||||
{
|
{
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
public event EventHandler<LibraryBook> Begin;
|
public event EventHandler<LibraryBook> Begin;
|
||||||
|
|
||||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||||
public event EventHandler<string> StatusUpdate;
|
public event EventHandler<string> StatusUpdate;
|
||||||
|
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||||
|
public event EventHandler<(string id, string path)> FileCreated;
|
||||||
|
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||||
|
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||||
|
|
||||||
public event EventHandler<LibraryBook> Completed;
|
public event EventHandler<LibraryBook> Completed;
|
||||||
|
|
||||||
@ -69,6 +74,23 @@ namespace FileLiberator
|
|||||||
StatusUpdate?.Invoke(this, statusUpdate);
|
StatusUpdate?.Invoke(this, statusUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void OnFileCreated(LibraryBook libraryBook, string path)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), libraryBook.Book.AudibleProductId, path });
|
||||||
|
FilePathCache.Insert(libraryBook.Book.AudibleProductId, path);
|
||||||
|
FileCreated?.Invoke(this, (libraryBook.Book.AudibleProductId, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnStreamingProgressChanged(DownloadProgress progress)
|
||||||
|
=> OnStreamingProgressChanged(null, progress);
|
||||||
|
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
|
||||||
|
=> StreamingProgressChanged?.Invoke(this, progress);
|
||||||
|
|
||||||
|
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
|
||||||
|
=> OnStreamingTimeRemaining(null, timeRemaining);
|
||||||
|
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
|
||||||
|
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
||||||
|
|
||||||
protected void OnCompleted(LibraryBook libraryBook)
|
protected void OnCompleted(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
|
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Dinah.Core.Net.Http;
|
|
||||||
|
|
||||||
namespace FileLiberator
|
|
||||||
{
|
|
||||||
public abstract class Streamable
|
|
||||||
{
|
|
||||||
public event EventHandler<string> StreamingBegin;
|
|
||||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
|
||||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
|
||||||
public event EventHandler<string> StreamingCompleted;
|
|
||||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
|
||||||
public event EventHandler<(string id, string path)> FileCreated;
|
|
||||||
|
|
||||||
protected void OnStreamingBegin(string filePath)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = filePath });
|
|
||||||
StreamingBegin?.Invoke(this, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnStreamingProgressChanged(DownloadProgress progress) => OnStreamingProgressChanged(null, progress);
|
|
||||||
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
|
|
||||||
{
|
|
||||||
StreamingProgressChanged?.Invoke(this, progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining) => OnStreamingTimeRemaining(null, timeRemaining);
|
|
||||||
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
|
|
||||||
{
|
|
||||||
StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnStreamingCompleted(string filePath)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = filePath });
|
|
||||||
StreamingCompleted?.Invoke(this, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnFileCreated(DataLayer.LibraryBook libraryBook, string path) => OnFileCreated(libraryBook.Book.AudibleProductId, path);
|
|
||||||
protected void OnFileCreated(string id, string path)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path });
|
|
||||||
LibationFileManager.FilePathCache.Insert(id, path);
|
|
||||||
FileCreated?.Invoke(this, (id, path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,7 +12,7 @@ namespace FileManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BackgroundFileSystem
|
public class BackgroundFileSystem
|
||||||
{
|
{
|
||||||
public string RootDirectory { get; private set; }
|
public LongPath RootDirectory { get; private set; }
|
||||||
public string SearchPattern { get; private set; }
|
public string SearchPattern { get; private set; }
|
||||||
public SearchOption SearchOption { get; private set; }
|
public SearchOption SearchOption { get; private set; }
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ namespace FileManager
|
|||||||
private Task backgroundScanner { get; set; }
|
private Task backgroundScanner { get; set; }
|
||||||
|
|
||||||
private object fsCacheLocker { get; } = new();
|
private object fsCacheLocker { get; } = new();
|
||||||
private List<string> fsCache { get; } = new();
|
private List<LongPath> fsCache { get; } = new();
|
||||||
|
|
||||||
public BackgroundFileSystem(string rootDirectory, string searchPattern, SearchOption searchOptions)
|
public BackgroundFileSystem(LongPath rootDirectory, string searchPattern, SearchOption searchOptions)
|
||||||
{
|
{
|
||||||
RootDirectory = rootDirectory;
|
RootDirectory = rootDirectory;
|
||||||
SearchPattern = searchPattern;
|
SearchPattern = searchPattern;
|
||||||
@ -32,12 +32,18 @@ namespace FileManager
|
|||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FindFile(System.Text.RegularExpressions.Regex regex)
|
public LongPath FindFile(System.Text.RegularExpressions.Regex regex)
|
||||||
{
|
{
|
||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LongPath> FindFiles(System.Text.RegularExpressions.Regex regex)
|
||||||
|
{
|
||||||
|
lock (fsCacheLocker)
|
||||||
|
return fsCache.Where(s => regex.IsMatch(s)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public void RefreshFiles()
|
public void RefreshFiles()
|
||||||
{
|
{
|
||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
@ -124,16 +130,18 @@ namespace FileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemovePath(string path)
|
private void RemovePath(LongPath path)
|
||||||
{
|
{
|
||||||
var pathsToRemove = fsCache.Where(p => p.StartsWith(path)).ToArray();
|
path = path.LongPathName;
|
||||||
|
var pathsToRemove = fsCache.Where(p => ((string)p).StartsWith(path)).ToArray();
|
||||||
|
|
||||||
foreach (var p in pathsToRemove)
|
foreach (var p in pathsToRemove)
|
||||||
fsCache.Remove(p);
|
fsCache.Remove(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPath(string path)
|
private void AddPath(LongPath path)
|
||||||
{
|
{
|
||||||
|
path = path.LongPathName;
|
||||||
if (!File.Exists(path) && !Directory.Exists(path))
|
if (!File.Exists(path) && !Directory.Exists(path))
|
||||||
return;
|
return;
|
||||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||||
@ -141,12 +149,14 @@ namespace FileManager
|
|||||||
else
|
else
|
||||||
AddUniqueFile(path);
|
AddUniqueFile(path);
|
||||||
}
|
}
|
||||||
private void AddUniqueFiles(IEnumerable<string> newFiles)
|
|
||||||
|
private void AddUniqueFiles(IEnumerable<LongPath> newFiles)
|
||||||
{
|
{
|
||||||
foreach (var file in newFiles)
|
foreach (var file in newFiles)
|
||||||
AddUniqueFile(file);
|
AddUniqueFile(file);
|
||||||
}
|
}
|
||||||
private void AddUniqueFile(string newFile)
|
|
||||||
|
private void AddUniqueFile(LongPath newFile)
|
||||||
{
|
{
|
||||||
if (!fsCache.Contains(newFile))
|
if (!fsCache.Contains(newFile))
|
||||||
fsCache.Add(newFile);
|
fsCache.Add(newFile);
|
||||||
|
|||||||
@ -1,48 +1,76 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core;
|
using System.Text;
|
||||||
|
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
||||||
public class FileNamingTemplate
|
public class FileNamingTemplate : NamingTemplate
|
||||||
{
|
{
|
||||||
/// <summary>Proposed full file path. May contain optional html-styled template tags. Eg: <name></summary>
|
|
||||||
public string Template { get; }
|
|
||||||
|
|
||||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||||
public FileNamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
public FileNamingTemplate(string template) : base(template) { }
|
||||||
|
|
||||||
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
|
||||||
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
|
|
||||||
|
|
||||||
/// <summary>Convenience method</summary>
|
|
||||||
public void AddParameterReplacement(string key, object value)
|
|
||||||
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
|
|
||||||
=> ParameterReplacements.Add(key, value);
|
|
||||||
|
|
||||||
/// <summary>If set, truncate each parameter replacement to this many characters. Default 50</summary>
|
|
||||||
public int? ParameterMaxSize { get; set; } = 50;
|
|
||||||
|
|
||||||
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
|
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
|
||||||
public string IllegalCharacterReplacements { get; set; }
|
public string IllegalCharacterReplacements { get; set; }
|
||||||
|
|
||||||
/// <summary>Generate a valid path for this file or directory</summary>
|
/// <summary>Generate a valid path for this file or directory</summary>
|
||||||
public string GetFilePath(bool returnFirstExisting = false)
|
public LongPath GetFilePath(bool returnFirstExisting = false)
|
||||||
{
|
{
|
||||||
var filename = Template;
|
int lastSlash = Template.LastIndexOf('\\');
|
||||||
|
|
||||||
foreach (var r in ParameterReplacements)
|
var directoryName = lastSlash >= 0 ? Template[..(lastSlash + 1)] : string.Empty;
|
||||||
filename = filename.Replace($"<{formatKey(r.Key)}>", formatValue(r.Value));
|
var filename = lastSlash >= 0 ? Template[(lastSlash + 1)..] : Template;
|
||||||
|
|
||||||
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements, returnFirstExisting);
|
List<StringBuilder> filenameParts = new();
|
||||||
|
|
||||||
|
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value));
|
||||||
|
|
||||||
|
//Build the filename in parts, replacing replacement parameters with
|
||||||
|
//their values, and storing the parts in a list.
|
||||||
|
while(!string.IsNullOrEmpty(filename))
|
||||||
|
{
|
||||||
|
int openIndex = filename.IndexOf('<');
|
||||||
|
int closeIndex = filename.IndexOf('>');
|
||||||
|
|
||||||
|
if (openIndex == 0 && closeIndex > 0)
|
||||||
|
{
|
||||||
|
var key = filename[..(closeIndex + 1)];
|
||||||
|
|
||||||
|
if (paramReplacements.ContainsKey(key))
|
||||||
|
filenameParts.Add(new StringBuilder(paramReplacements[key]));
|
||||||
|
else
|
||||||
|
filenameParts.Add(new StringBuilder(key));
|
||||||
|
|
||||||
|
filename = filename[(closeIndex + 1)..];
|
||||||
|
}
|
||||||
|
else if (openIndex > 0 && closeIndex > openIndex)
|
||||||
|
{
|
||||||
|
var other = filename[..openIndex];
|
||||||
|
filenameParts.Add(new StringBuilder(other));
|
||||||
|
filename = filename[openIndex..];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filenameParts.Add(new StringBuilder(filename));
|
||||||
|
filename = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string formatKey(string key)
|
//Remove 1 character from the end of the longest filename part until
|
||||||
=> key
|
//the total filename is less than max filename length
|
||||||
.Replace("<", "")
|
while(filenameParts.Sum(p => p.Length) > LongPath.MaxFilenameLength)
|
||||||
.Replace(">", "");
|
{
|
||||||
|
int maxLength = filenameParts.Max(p => p.Length);
|
||||||
|
var maxEntry = filenameParts.First(p => p.Length == maxLength);
|
||||||
|
|
||||||
|
maxEntry.Remove(maxLength - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = string.Join("", filenameParts);
|
||||||
|
|
||||||
|
return FileUtility.GetValidFilename(directoryName + filename, IllegalCharacterReplacements, returnFirstExisting);
|
||||||
|
}
|
||||||
|
|
||||||
private string formatValue(object value)
|
private string formatValue(object value)
|
||||||
{
|
{
|
||||||
@ -51,14 +79,10 @@ namespace FileManager
|
|||||||
|
|
||||||
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
||||||
// Esp important for file templates.
|
// Esp important for file templates.
|
||||||
var val = value
|
return value
|
||||||
.ToString()
|
.ToString()
|
||||||
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
||||||
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
||||||
return
|
|
||||||
ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
|
|
||||||
? val.Truncate(ParameterMaxSize.Value)
|
|
||||||
: val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,6 @@ namespace FileManager
|
|||||||
return position.ToString().PadLeft(total.ToString().Length, '0');
|
return position.ToString().PadLeft(total.ToString().Length, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int MAX_FILENAME_LENGTH = 255;
|
|
||||||
private const int MAX_DIRECTORY_LENGTH = 247;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensure valid file name path:
|
/// Ensure valid file name path:
|
||||||
@ -48,7 +46,7 @@ namespace FileManager
|
|||||||
/// <br/>- ensure uniqueness
|
/// <br/>- ensure uniqueness
|
||||||
/// <br/>- enforce max file length
|
/// <br/>- enforce max file length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetValidFilename(string path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
|
public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||||
|
|
||||||
@ -57,14 +55,15 @@ namespace FileManager
|
|||||||
|
|
||||||
// ensure uniqueness and check lengths
|
// ensure uniqueness and check lengths
|
||||||
var dir = Path.GetDirectoryName(path);
|
var dir = Path.GetDirectoryName(path);
|
||||||
dir = dir.Truncate(MAX_DIRECTORY_LENGTH);
|
dir = dir?.Truncate(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||||
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
var fileStem = Path.Combine(dir, filename);
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
var fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - extension.Length) + extension;
|
var filename = Path.GetFileNameWithoutExtension(path).Truncate(LongPath.MaxFilenameLength - extension.Length);
|
||||||
|
var fileStem = Path.Combine(dir, filename);
|
||||||
|
|
||||||
|
|
||||||
|
var fullfilename = fileStem.Truncate(LongPath.MaxPathLength - extension.Length) + extension;
|
||||||
|
|
||||||
fullfilename = removeInvalidWhitespace(fullfilename);
|
fullfilename = removeInvalidWhitespace(fullfilename);
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ namespace FileManager
|
|||||||
while (File.Exists(fullfilename) && !returnFirstExisting)
|
while (File.Exists(fullfilename) && !returnFirstExisting)
|
||||||
{
|
{
|
||||||
var increm = $" ({++i})";
|
var increm = $" ({++i})";
|
||||||
fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension;
|
fullfilename = fileStem.Truncate(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullfilename;
|
return fullfilename;
|
||||||
@ -85,16 +84,18 @@ namespace FileManager
|
|||||||
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
||||||
|
|
||||||
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
|
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||||
public static string GetSafePath(string path, string illegalCharacterReplacements = "")
|
public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "")
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||||
|
|
||||||
path = replaceInvalidChars(path, illegalCharacterReplacements);
|
var pathNoPrefix = path.PathWithoutPrefix;
|
||||||
path = standardizeSlashes(path);
|
|
||||||
path = replaceColons(path, illegalCharacterReplacements);
|
|
||||||
path = removeDoubleSlashes(path);
|
|
||||||
|
|
||||||
return path;
|
pathNoPrefix = replaceColons(pathNoPrefix, "꞉");
|
||||||
|
pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix);
|
||||||
|
pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements);
|
||||||
|
pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
|
||||||
|
|
||||||
|
return pathNoPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||||
@ -106,24 +107,6 @@ namespace FileManager
|
|||||||
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
||||||
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
||||||
|
|
||||||
private static string standardizeSlashes(string path)
|
|
||||||
=> path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
|
||||||
|
|
||||||
private static string replaceColons(string path, string illegalCharacterReplacements)
|
|
||||||
{
|
|
||||||
// replace all colons except within the first 2 chars
|
|
||||||
var builder = new System.Text.StringBuilder();
|
|
||||||
for (var i = 0; i < path.Length; i++)
|
|
||||||
{
|
|
||||||
var c = path[i];
|
|
||||||
if (i >= 2 && c == ':')
|
|
||||||
builder.Append(illegalCharacterReplacements);
|
|
||||||
else
|
|
||||||
builder.Append(c);
|
|
||||||
}
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string removeDoubleSlashes(string path)
|
private static string removeDoubleSlashes(string path)
|
||||||
{
|
{
|
||||||
if (path.Length < 2)
|
if (path.Length < 2)
|
||||||
@ -139,6 +122,60 @@ namespace FileManager
|
|||||||
return path[0] + remainder;
|
return path[0] + remainder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string replaceIllegalWithUnicodeAnalog(string path)
|
||||||
|
{
|
||||||
|
char[] replaced = path.ToCharArray();
|
||||||
|
|
||||||
|
char GetQuote(int position)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
position == 0
|
||||||
|
|| (position > 0
|
||||||
|
&& position < replaced.Length
|
||||||
|
&& !char.IsLetter(replaced[position - 1])
|
||||||
|
&& !char.IsNumber(replaced[position - 1])
|
||||||
|
)
|
||||||
|
) return '“';
|
||||||
|
else if (
|
||||||
|
position == replaced.Length - 1
|
||||||
|
|| (position >= 0
|
||||||
|
&& position < replaced.Length - 1
|
||||||
|
&& !char.IsLetter(replaced[position + 1])
|
||||||
|
&& !char.IsNumber(replaced[position + 1])
|
||||||
|
)
|
||||||
|
) return '”';
|
||||||
|
else return '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < replaced.Length; i++)
|
||||||
|
{
|
||||||
|
replaced[i] = replaced[i] switch
|
||||||
|
{
|
||||||
|
'?' => '?',
|
||||||
|
'*' => '✱',
|
||||||
|
'<' => '<',
|
||||||
|
'>' => '>',
|
||||||
|
'"' => GetQuote(i),
|
||||||
|
_ => replaced[i]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new string(replaced);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string replaceColons(string path, string illegalCharacterReplacements)
|
||||||
|
{
|
||||||
|
// replace all colons except within the first 2 chars
|
||||||
|
var builder = new System.Text.StringBuilder();
|
||||||
|
for (var i = 0; i < path.Length; i++)
|
||||||
|
{
|
||||||
|
var c = path[i];
|
||||||
|
if (i >= 2 && c == ':')
|
||||||
|
builder.Append(illegalCharacterReplacements);
|
||||||
|
else
|
||||||
|
builder.Append(c);
|
||||||
|
}
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
|
private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
|
||||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||||
|
|
||||||
@ -169,7 +206,7 @@ namespace FileManager
|
|||||||
/// <br/>- Perform <see cref="SaferMove"/>
|
/// <br/>- Perform <see cref="SaferMove"/>
|
||||||
/// <br/>- Return valid path
|
/// <br/>- Return valid path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string SaferMoveToValidPath(string source, string destination)
|
public static string SaferMoveToValidPath(LongPath source, LongPath destination)
|
||||||
{
|
{
|
||||||
destination = GetValidFilename(destination);
|
destination = GetValidFilename(destination);
|
||||||
SaferMove(source, destination);
|
SaferMove(source, destination);
|
||||||
@ -184,7 +221,7 @@ namespace FileManager
|
|||||||
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
|
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
|
||||||
|
|
||||||
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||||
public static void SaferDelete(string source)
|
public static void SaferDelete(LongPath source)
|
||||||
=> retryPolicy.Execute(() =>
|
=> retryPolicy.Execute(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -207,7 +244,7 @@ namespace FileManager
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||||
public static void SaferMove(string source, string destination)
|
public static void SaferMove(LongPath source, LongPath destination)
|
||||||
=> retryPolicy.Execute(() =>
|
=> retryPolicy.Execute(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -242,15 +279,15 @@ namespace FileManager
|
|||||||
/// <param name="patternMatch">Filename pattern match</param>
|
/// <param name="patternMatch">Filename pattern match</param>
|
||||||
/// <param name="searchOption">Search subdirectories or only top level directory for files</param>
|
/// <param name="searchOption">Search subdirectories or only top level directory for files</param>
|
||||||
/// <returns>List of files</returns>
|
/// <returns>List of files</returns>
|
||||||
public static IEnumerable<string> SaferEnumerateFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
public static IEnumerable<LongPath> SaferEnumerateFiles(LongPath path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||||
{
|
{
|
||||||
var foundFiles = Enumerable.Empty<string>();
|
var foundFiles = Enumerable.Empty<LongPath>();
|
||||||
|
|
||||||
if (searchOption == SearchOption.AllDirectories)
|
if (searchOption == SearchOption.AllDirectories)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IEnumerable<string> subDirs = Directory.EnumerateDirectories(path);
|
IEnumerable <LongPath> subDirs = Directory.EnumerateDirectories(path).Select(p => (LongPath)p);
|
||||||
// Add files in subdirectories recursively to the list
|
// Add files in subdirectories recursively to the list
|
||||||
foreach (string dir in subDirs)
|
foreach (string dir in subDirs)
|
||||||
foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
|
foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
|
||||||
@ -262,7 +299,7 @@ namespace FileManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Add files from the current directory
|
// Add files from the current directory
|
||||||
foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern));
|
foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern).Select(f => (LongPath)f));
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException) { }
|
catch (UnauthorizedAccessException) { }
|
||||||
|
|
||||||
|
|||||||
113
Source/FileManager/LongPath.cs
Normal file
113
Source/FileManager/LongPath.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FileManager
|
||||||
|
{
|
||||||
|
public class LongPath
|
||||||
|
{
|
||||||
|
//https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd
|
||||||
|
|
||||||
|
public const int MaxDirectoryLength = MaxPathLength - 13;
|
||||||
|
public const int MaxPathLength = short.MaxValue;
|
||||||
|
public const int MaxFilenameLength = 255;
|
||||||
|
|
||||||
|
private const int MAX_PATH = 260;
|
||||||
|
private const string LONG_PATH_PREFIX = "\\\\?\\";
|
||||||
|
private static readonly StringBuilder longPathBuffer = new(MaxPathLength);
|
||||||
|
|
||||||
|
public string Path { get; init; }
|
||||||
|
public override string ToString() => Path;
|
||||||
|
|
||||||
|
public static implicit operator LongPath(string path)
|
||||||
|
{
|
||||||
|
if (path is null) return null;
|
||||||
|
|
||||||
|
//File I/O functions in the Windows API convert "/" to "\" as part of converting
|
||||||
|
//the name to an NT-style name, except when using the "\\?\" prefix
|
||||||
|
path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
if (path.StartsWith(LONG_PATH_PREFIX))
|
||||||
|
return new LongPath { Path = path };
|
||||||
|
else if ((path.Length > 2 && path[1] == ':') || path.StartsWith("UNC\\"))
|
||||||
|
return new LongPath { Path = LONG_PATH_PREFIX + path };
|
||||||
|
else if (path.StartsWith("\\\\"))
|
||||||
|
//The "\\?\" prefix can also be used with paths constructed according to the
|
||||||
|
//universal naming convention (UNC). To specify such a path using UNC, use
|
||||||
|
//the "\\?\UNC\" prefix.
|
||||||
|
return new LongPath { Path = LONG_PATH_PREFIX + "UNC\\" + path.Substring(2) };
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//These prefixes are not used as part of the path itself. They indicate that
|
||||||
|
//the path should be passed to the system with minimal modification, which
|
||||||
|
//means that you cannot use forward slashes to represent path separators, or
|
||||||
|
//a period to represent the current directory, or double dots to represent the
|
||||||
|
//parent directory. Because you cannot use the "\\?\" prefix with a relative
|
||||||
|
//path, relative paths are always limited to a total of MAX_PATH characters.
|
||||||
|
if (path.Length > MAX_PATH)
|
||||||
|
throw new System.IO.PathTooLongException();
|
||||||
|
return new LongPath { Path = path };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator string(LongPath path) => path?.Path;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string ShortPathName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
//Short Path names are useful for navigating to the file in windows explorer,
|
||||||
|
//which will not recognize paths longer than MAX_PATH. Short path names are not
|
||||||
|
//always enabled on every volume. So to check if a volume enables short path
|
||||||
|
//names (aka 8dot3 names), run the following command from an elevated command
|
||||||
|
//prompt:
|
||||||
|
//
|
||||||
|
// fsutil 8dot3name query c:
|
||||||
|
//
|
||||||
|
//It will say:
|
||||||
|
//
|
||||||
|
// "Based on the above settings, 8dot3 name creation is [enabled/disabled] on c:"
|
||||||
|
//
|
||||||
|
//To enable short names on all volumes on the system, run the following command
|
||||||
|
//from an elevated command prompt:
|
||||||
|
//
|
||||||
|
// fsutil 8dot3name set c: 0
|
||||||
|
//
|
||||||
|
//Note that after enabling 8dot3 names on a volume, they will only be available
|
||||||
|
//for newly-created entries in ther file system. Existing entries made while
|
||||||
|
//8dot3 names were disabled will not be reachable by short paths.
|
||||||
|
|
||||||
|
if (Path is null) return null;
|
||||||
|
GetShortPathName(Path, longPathBuffer, MaxPathLength);
|
||||||
|
return longPathBuffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string LongPathName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Path is null) return null;
|
||||||
|
GetLongPathName(Path, longPathBuffer, MaxPathLength);
|
||||||
|
return longPathBuffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string PathWithoutPrefix
|
||||||
|
=> Path?.StartsWith(LONG_PATH_PREFIX) == true ?
|
||||||
|
Path.Remove(0, LONG_PATH_PREFIX.Length) :
|
||||||
|
Path;
|
||||||
|
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int GetLongPathName([MarshalAs(UnmanagedType.LPWStr)] string lpszShortPath, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszLongPath, int cchBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Source/FileManager/MetadataNamingTemplate.cs
Normal file
20
Source/FileManager/MetadataNamingTemplate.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FileManager
|
||||||
|
{
|
||||||
|
public class MetadataNamingTemplate : NamingTemplate
|
||||||
|
{
|
||||||
|
public MetadataNamingTemplate(string template) : base(template) { }
|
||||||
|
|
||||||
|
public string GetTagContents()
|
||||||
|
{
|
||||||
|
var tagValue = Template;
|
||||||
|
|
||||||
|
foreach (var r in ParameterReplacements)
|
||||||
|
tagValue = tagValue.Replace($"<{formatKey(r.Key)}>", r.Value?.ToString() ?? "");
|
||||||
|
|
||||||
|
return tagValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Source/FileManager/NamingTemplate.cs
Normal file
28
Source/FileManager/NamingTemplate.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Dinah.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FileManager
|
||||||
|
{
|
||||||
|
public class NamingTemplate
|
||||||
|
{
|
||||||
|
/// <summary>Proposed full name. May contain optional html-styled template tags. Eg: <name></summary>
|
||||||
|
public string Template { get; }
|
||||||
|
|
||||||
|
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||||
|
public NamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||||
|
|
||||||
|
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
||||||
|
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
/// <summary>Convenience method</summary>
|
||||||
|
public void AddParameterReplacement(string key, object value)
|
||||||
|
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
|
||||||
|
=> ParameterReplacements.Add(key, value);
|
||||||
|
|
||||||
|
protected static string formatKey(string key)
|
||||||
|
=> key
|
||||||
|
.Replace("<", "")
|
||||||
|
.Replace(">", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,18 +9,19 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
public abstract class AudibleFileStorage
|
public abstract class AudibleFileStorage
|
||||||
{
|
{
|
||||||
protected abstract string GetFilePathCustom(string productId);
|
protected abstract LongPath GetFilePathCustom(string productId);
|
||||||
|
protected abstract List<LongPath> GetFilePathsCustom(string productId);
|
||||||
|
|
||||||
#region static
|
#region static
|
||||||
public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||||
public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
public static LongPath DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||||
|
|
||||||
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||||
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
||||||
|
|
||||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||||
|
|
||||||
public static string BooksDirectory
|
public static LongPath BooksDirectory
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -43,7 +44,7 @@ namespace LibationFileManager
|
|||||||
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
|
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string GetFilePath(string productId)
|
protected LongPath GetFilePath(string productId)
|
||||||
{
|
{
|
||||||
// primary lookup
|
// primary lookup
|
||||||
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
||||||
@ -58,6 +59,9 @@ namespace LibationFileManager
|
|||||||
return firstOrNull;
|
return firstOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LongPath> GetPaths(string productId)
|
||||||
|
=> GetFilePathsCustom(productId);
|
||||||
|
|
||||||
protected Regex GetBookSearchRegex(string productId)
|
protected Regex GetBookSearchRegex(string productId)
|
||||||
{
|
{
|
||||||
var pattern = string.Format(regexTemplate, productId);
|
var pattern = string.Format(regexTemplate, productId);
|
||||||
@ -70,12 +74,15 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||||
|
|
||||||
protected override string GetFilePathCustom(string productId)
|
protected override LongPath GetFilePathCustom(string productId)
|
||||||
|
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||||
|
|
||||||
|
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||||
{
|
{
|
||||||
var regex = GetBookSearchRegex(productId);
|
var regex = GetBookSearchRegex(productId);
|
||||||
return FileUtility
|
return FileUtility
|
||||||
.SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
.SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
||||||
.FirstOrDefault(s => regex.IsMatch(s));
|
.Where(s => regex.IsMatch(s)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Exists(string productId) => GetFilePath(productId) is not null;
|
public bool Exists(string productId) => GetFilePath(productId) is not null;
|
||||||
@ -88,7 +95,11 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||||
private static object bookDirectoryFilesLocker { get; } = new();
|
private static object bookDirectoryFilesLocker { get; } = new();
|
||||||
protected override string GetFilePathCustom(string productId)
|
|
||||||
|
protected override LongPath GetFilePathCustom(string productId)
|
||||||
|
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||||
|
|
||||||
|
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||||
{
|
{
|
||||||
// If user changed the BooksDirectory: reinitialize
|
// If user changed the BooksDirectory: reinitialize
|
||||||
lock (bookDirectoryFilesLocker)
|
lock (bookDirectoryFilesLocker)
|
||||||
@ -96,11 +107,12 @@ namespace LibationFileManager
|
|||||||
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
var regex = GetBookSearchRegex(productId);
|
var regex = GetBookSearchRegex(productId);
|
||||||
return BookDirectoryFiles.FindFile(regex);
|
return BookDirectoryFiles.FindFiles(regex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||||
|
|
||||||
public string GetPath(string productId) => GetFilePath(productId);
|
public LongPath GetPath(string productId) => GetFilePath(productId);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,7 +124,7 @@ namespace LibationFileManager
|
|||||||
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Description("Allow Libation to remove audible branding from the start\r\nand end of audiobooks. (e.g. \"This is Audible\")")]
|
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
|
||||||
public bool StripAudibleBrandAudio
|
public bool StripAudibleBrandAudio
|
||||||
{
|
{
|
||||||
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
|
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
|
||||||
@ -305,6 +305,13 @@ namespace LibationFileManager
|
|||||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Description("How to format the file's Tile stored in metadata")]
|
||||||
|
public string ChapterTitleTemplate
|
||||||
|
{
|
||||||
|
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
|
||||||
|
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||||
{
|
{
|
||||||
@ -455,7 +462,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string libationFilesPathCache;
|
private static string libationFilesPathCache { get; set; }
|
||||||
|
|
||||||
private string getLibationFilesSettingFromJson()
|
private string getLibationFilesSettingFromJson()
|
||||||
{
|
{
|
||||||
@ -478,7 +485,7 @@ namespace LibationFileManager
|
|||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
// not found. write to file. read from file
|
// not found. write to file. read from file
|
||||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented);
|
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile.ToString() } }.ToString(Formatting.Indented);
|
||||||
if (startingContents != endingContents)
|
if (startingContents != endingContents)
|
||||||
{
|
{
|
||||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||||
|
|||||||
@ -3,13 +3,14 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core.Collections.Immutable;
|
using Dinah.Core.Collections.Immutable;
|
||||||
|
using FileManager;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
public static class FilePathCache
|
public static class FilePathCache
|
||||||
{
|
{
|
||||||
public record CacheEntry(string Id, FileType FileType, string Path);
|
public record CacheEntry(string Id, FileType FileType, LongPath Path);
|
||||||
|
|
||||||
private const string FILENAME = "FileLocations.json";
|
private const string FILENAME = "FileLocations.json";
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||||
|
|
||||||
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
|
private static LongPath jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
|
||||||
|
|
||||||
static FilePathCache()
|
static FilePathCache()
|
||||||
{
|
{
|
||||||
@ -44,12 +45,12 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
|
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
|
||||||
|
|
||||||
public static List<(FileType fileType, string path)> GetFiles(string id)
|
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
|
||||||
=> getEntries(entry => entry.Id == id)
|
=> getEntries(entry => entry.Id == id)
|
||||||
.Select(entry => (entry.FileType, entry.Path))
|
.Select(entry => (entry.FileType, entry.Path))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public static string GetFirstPath(string id, FileType type)
|
public static LongPath GetFirstPath(string id, FileType type)
|
||||||
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
||||||
?.FirstOrDefault()
|
?.FirstOrDefault()
|
||||||
?.Path;
|
?.Path;
|
||||||
@ -62,7 +63,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
|
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
|
||||||
|
|
||||||
return entries;
|
return cache.Where(predicate).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void remove(List<CacheEntry> entries)
|
private static void remove(List<CacheEntry> entries)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ namespace LibationFileManager
|
|||||||
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
||||||
public static FileTemplate File { get; } = new FileTemplate();
|
public static FileTemplate File { get; } = new FileTemplate();
|
||||||
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
||||||
|
public static ChapterTitleTemplate ChapterTitle { get; } = new ChapterTitleTemplate();
|
||||||
|
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
public abstract string Description { get; }
|
public abstract string Description { get; }
|
||||||
@ -105,7 +106,7 @@ namespace LibationFileManager
|
|||||||
=> string.IsNullOrWhiteSpace(template)
|
=> string.IsNullOrWhiteSpace(template)
|
||||||
? ""
|
? ""
|
||||||
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
||||||
.GetFilePath();
|
.GetFilePath().PathWithoutPrefix;
|
||||||
|
|
||||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
@ -146,7 +147,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public IEnumerable<TemplateTags> GetTemplateTags()
|
public virtual IEnumerable<TemplateTags> GetTemplateTags()
|
||||||
=> TemplateTags.GetAll()
|
=> TemplateTags.GetAll()
|
||||||
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
||||||
.Where(t => IsChapterized || !t.IsChapterOnly);
|
.Where(t => IsChapterized || !t.IsChapterOnly);
|
||||||
@ -276,9 +277,61 @@ namespace LibationFileManager
|
|||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||||
|
|
||||||
return fileNamingTemplate.GetFilePath();
|
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ChapterTitleTemplate : Templates
|
||||||
|
{
|
||||||
|
private List<TemplateTags> _templateTags { get; } = new()
|
||||||
|
{
|
||||||
|
TemplateTags.Title,
|
||||||
|
TemplateTags.TitleShort,
|
||||||
|
TemplateTags.Series,
|
||||||
|
TemplateTags.ChCount,
|
||||||
|
TemplateTags.ChNumber,
|
||||||
|
TemplateTags.ChNumber0,
|
||||||
|
TemplateTags.ChTitle,
|
||||||
|
};
|
||||||
|
public override string Name => "Chapter Title Template";
|
||||||
|
|
||||||
|
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||||
|
|
||||||
|
public override string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||||
|
|
||||||
|
protected override bool IsChapterized => true;
|
||||||
|
|
||||||
|
public override IEnumerable<string> GetErrors(string template)
|
||||||
|
=> new List<string>();
|
||||||
|
|
||||||
|
public override IEnumerable<string> GetWarnings(string template)
|
||||||
|
=> GetStandardWarnings(template).ToList();
|
||||||
|
|
||||||
|
public string GetTitle(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
||||||
|
=> GetPortionTitle(libraryBookDto, Configuration.Instance.ChapterTitleTemplate, props);
|
||||||
|
|
||||||
|
public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||||
|
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||||
|
|
||||||
|
var fileNamingTemplate = new MetadataNamingTemplate(template);
|
||||||
|
|
||||||
|
var title = libraryBookDto.Title ?? "";
|
||||||
|
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||||
|
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||||
|
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||||
|
|
||||||
|
return fileNamingTemplate.GetTagContents();
|
||||||
|
}
|
||||||
|
public override IEnumerable<TemplateTags> GetTemplateTags() => _templateTags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
public static class UtilityExtensions
|
public static class UtilityExtensions
|
||||||
{
|
{
|
||||||
public static void AddParameterReplacement(this FileNamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
public static void AddParameterReplacement(this NamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
||||||
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
private void templateTb_TextChanged(object sender, EventArgs e)
|
private void templateTb_TextChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
workingTemplateText = templateTb.Text;
|
workingTemplateText = templateTb.Text;
|
||||||
|
var isChapterTitle = template == Templates.ChapterTitle;
|
||||||
var isFolder = template == Templates.Folder;
|
var isFolder = template == Templates.Folder;
|
||||||
|
|
||||||
var libraryBookDto = new LibraryBookDto
|
var libraryBookDto = new LibraryBookDto
|
||||||
@ -85,22 +85,35 @@ namespace LibationWinForms.Dialogs
|
|||||||
var chapterNumber = 4;
|
var chapterNumber = 4;
|
||||||
var chaptersTotal = 10;
|
var chaptersTotal = 10;
|
||||||
|
|
||||||
|
var partFileProperties = new AaxDecrypter.MultiConvertFileProperties()
|
||||||
|
{
|
||||||
|
OutputFileName = "",
|
||||||
|
PartsPosition = chapterNumber,
|
||||||
|
PartsTotal = chaptersTotal,
|
||||||
|
Title = chapterName
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
var books = config.Books;
|
var books = config.Books;
|
||||||
var folder = Templates.Folder.GetPortionFilename(
|
var folder = Templates.Folder.GetPortionFilename(
|
||||||
libraryBookDto,
|
libraryBookDto,
|
||||||
isFolder ? workingTemplateText : config.FolderTemplate);
|
isFolder ? workingTemplateText : config.FolderTemplate);
|
||||||
|
|
||||||
|
|
||||||
var file
|
var file
|
||||||
= template == Templates.ChapterFile
|
= template == Templates.ChapterFile
|
||||||
? Templates.ChapterFile.GetPortionFilename(
|
? Templates.ChapterFile.GetPortionFilename(
|
||||||
libraryBookDto,
|
libraryBookDto,
|
||||||
workingTemplateText,
|
workingTemplateText,
|
||||||
new() { OutputFileName = "", PartsPosition = chapterNumber, PartsTotal = chaptersTotal, Title = chapterName },
|
partFileProperties,
|
||||||
"")
|
"")
|
||||||
: Templates.File.GetPortionFilename(
|
: Templates.File.GetPortionFilename(
|
||||||
libraryBookDto,
|
libraryBookDto,
|
||||||
isFolder ? config.FileTemplate : workingTemplateText);
|
isFolder ? config.FileTemplate : workingTemplateText);
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
|
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
||||||
|
|
||||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
var sing = $"{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
@ -126,6 +139,14 @@ namespace LibationWinForms.Dialogs
|
|||||||
richTextBox1.Clear();
|
richTextBox1.Clear();
|
||||||
richTextBox1.SelectionFont = reg;
|
richTextBox1.SelectionFont = reg;
|
||||||
|
|
||||||
|
if (isChapterTitle)
|
||||||
|
richTextBox1.SelectionFont = bold;
|
||||||
|
|
||||||
|
richTextBox1.AppendText(chapterTitle);
|
||||||
|
|
||||||
|
if (isChapterTitle)
|
||||||
|
return;
|
||||||
|
|
||||||
richTextBox1.AppendText(slashWrap(books));
|
richTextBox1.AppendText(slashWrap(books));
|
||||||
richTextBox1.AppendText(sing);
|
richTextBox1.AppendText(sing);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using FileManager;
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs
|
namespace LibationWinForms.Dialogs
|
||||||
{
|
{
|
||||||
@ -47,7 +48,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
|
|
||||||
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||||
{
|
{
|
||||||
string dir = "";
|
LongPath dir = "";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dir = LibationFileManager.Configuration.Instance.LibationFiles;
|
dir = LibationFileManager.Configuration.Instance.LibationFiles;
|
||||||
@ -56,7 +57,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Go.To.Folder(dir);
|
Go.To.Folder(dir.ShortPathName);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,13 +1,69 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using LibationFileManager;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs
|
namespace LibationWinForms.Dialogs
|
||||||
{
|
{
|
||||||
partial class SettingsDialog
|
partial class SettingsDialog
|
||||||
{
|
{
|
||||||
|
private void Load_AudioSettings(Configuration config)
|
||||||
|
{
|
||||||
|
this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup));
|
||||||
|
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
|
||||||
|
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
|
||||||
|
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
||||||
|
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
||||||
|
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||||
|
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||||
|
|
||||||
|
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||||
|
createCueSheetCbox.Checked = config.CreateCueSheet;
|
||||||
|
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
|
||||||
|
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||||
|
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
||||||
|
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
||||||
|
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
|
||||||
|
convertLosslessRb.Checked = !config.DecryptToLossy;
|
||||||
|
convertLossyRb.Checked = config.DecryptToLossy;
|
||||||
|
|
||||||
|
lameTargetBitrateRb.Checked = config.LameTargetBitrate;
|
||||||
|
lameTargetQualityRb.Checked = !config.LameTargetBitrate;
|
||||||
|
lameDownsampleMonoCbox.Checked = config.LameDownsampleMono;
|
||||||
|
lameBitrateTb.Value = config.LameBitrate;
|
||||||
|
lameConstantBitrateCbox.Checked = config.LameConstantBitrate;
|
||||||
|
LameMatchSourceBRCbox.Checked = config.LameMatchSourceBR;
|
||||||
|
lameVBRQualityTb.Value = config.LameVBRQuality;
|
||||||
|
|
||||||
|
chapterTitleTemplateGb.Text = desc(nameof(config.ChapterTitleTemplate));
|
||||||
|
chapterTitleTemplateTb.Text = config.ChapterTitleTemplate;
|
||||||
|
|
||||||
|
lameTargetRb_CheckedChanged(this, EventArgs.Empty);
|
||||||
|
LameMatchSourceBRCbox_CheckedChanged(this, EventArgs.Empty);
|
||||||
|
convertFormatRb_CheckedChanged(this, EventArgs.Empty);
|
||||||
|
allowLibationFixupCbox_CheckedChanged(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_AudioSettings(Configuration config)
|
||||||
|
{
|
||||||
|
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||||
|
config.CreateCueSheet = createCueSheetCbox.Checked;
|
||||||
|
config.DownloadCoverArt = downloadCoverArtCbox.Checked;
|
||||||
|
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||||
|
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
||||||
|
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
||||||
|
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
|
||||||
|
config.DecryptToLossy = convertLossyRb.Checked;
|
||||||
|
|
||||||
|
config.LameTargetBitrate = lameTargetBitrateRb.Checked;
|
||||||
|
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
|
||||||
|
config.LameBitrate = lameBitrateTb.Value;
|
||||||
|
config.LameConstantBitrate = lameConstantBitrateCbox.Checked;
|
||||||
|
config.LameMatchSourceBR = LameMatchSourceBRCbox.Checked;
|
||||||
|
config.LameVBRQuality = lameVBRQualityTb.Value;
|
||||||
|
|
||||||
|
config.ChapterTitleTemplate = chapterTitleTemplateTb.Text;
|
||||||
|
}
|
||||||
|
|
||||||
private void lameTargetRb_CheckedChanged(object sender, EventArgs e)
|
private void lameTargetRb_CheckedChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
lameBitrateGb.Enabled = lameTargetBitrateRb.Checked;
|
lameBitrateGb.Enabled = lameTargetBitrateRb.Checked;
|
||||||
@ -19,6 +75,13 @@ namespace LibationWinForms.Dialogs
|
|||||||
lameBitrateTb.Enabled = !LameMatchSourceBRCbox.Checked;
|
lameBitrateTb.Enabled = !LameMatchSourceBRCbox.Checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void splitFilesByChapterCbox_CheckedChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
chapterTitleTemplateGb.Enabled = splitFilesByChapterCbox.Checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void chapterTitleTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterTitle, chapterTitleTemplateTb);
|
||||||
|
|
||||||
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
lameTargetRb_CheckedChanged(sender, e);
|
lameTargetRb_CheckedChanged(sender, e);
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
this.tabControl = new System.Windows.Forms.TabControl();
|
this.tabControl = new System.Windows.Forms.TabControl();
|
||||||
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||||
|
this.saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
||||||
this.autoDownloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
this.autoDownloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||||
this.autoScanCb = new System.Windows.Forms.CheckBox();
|
this.autoScanCb = new System.Windows.Forms.CheckBox();
|
||||||
@ -69,6 +70,9 @@
|
|||||||
this.folderTemplateTb = new System.Windows.Forms.TextBox();
|
this.folderTemplateTb = new System.Windows.Forms.TextBox();
|
||||||
this.folderTemplateLbl = new System.Windows.Forms.Label();
|
this.folderTemplateLbl = new System.Windows.Forms.Label();
|
||||||
this.tab4AudioFileOptions = new System.Windows.Forms.TabPage();
|
this.tab4AudioFileOptions = new System.Windows.Forms.TabPage();
|
||||||
|
this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
|
||||||
|
this.chapterTitleTemplateBtn = new System.Windows.Forms.Button();
|
||||||
|
this.chapterTitleTemplateTb = new System.Windows.Forms.TextBox();
|
||||||
this.lameOptionsGb = new System.Windows.Forms.GroupBox();
|
this.lameOptionsGb = new System.Windows.Forms.GroupBox();
|
||||||
this.lameDownsampleMonoCbox = new System.Windows.Forms.CheckBox();
|
this.lameDownsampleMonoCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.lameBitrateGb = new System.Windows.Forms.GroupBox();
|
this.lameBitrateGb = new System.Windows.Forms.GroupBox();
|
||||||
@ -103,7 +107,6 @@
|
|||||||
this.retainAaxFileCbox = new System.Windows.Forms.CheckBox();
|
this.retainAaxFileCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
|
this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.createCueSheetCbox = new System.Windows.Forms.CheckBox();
|
this.createCueSheetCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox();
|
|
||||||
this.badBookGb.SuspendLayout();
|
this.badBookGb.SuspendLayout();
|
||||||
this.tabControl.SuspendLayout();
|
this.tabControl.SuspendLayout();
|
||||||
this.tab1ImportantSettings.SuspendLayout();
|
this.tab1ImportantSettings.SuspendLayout();
|
||||||
@ -113,6 +116,7 @@
|
|||||||
this.inProgressFilesGb.SuspendLayout();
|
this.inProgressFilesGb.SuspendLayout();
|
||||||
this.customFileNamingGb.SuspendLayout();
|
this.customFileNamingGb.SuspendLayout();
|
||||||
this.tab4AudioFileOptions.SuspendLayout();
|
this.tab4AudioFileOptions.SuspendLayout();
|
||||||
|
this.chapterTitleTemplateGb.SuspendLayout();
|
||||||
this.lameOptionsGb.SuspendLayout();
|
this.lameOptionsGb.SuspendLayout();
|
||||||
this.lameBitrateGb.SuspendLayout();
|
this.lameBitrateGb.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.lameBitrateTb)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.lameBitrateTb)).BeginInit();
|
||||||
@ -262,6 +266,7 @@
|
|||||||
this.splitFilesByChapterCbox.TabIndex = 13;
|
this.splitFilesByChapterCbox.TabIndex = 13;
|
||||||
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
||||||
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
||||||
|
this.splitFilesByChapterCbox.CheckedChanged += new System.EventHandler(this.splitFilesByChapterCbox_CheckedChanged);
|
||||||
//
|
//
|
||||||
// allowLibationFixupCbox
|
// allowLibationFixupCbox
|
||||||
//
|
//
|
||||||
@ -391,6 +396,16 @@
|
|||||||
this.booksGb.TabStop = false;
|
this.booksGb.TabStop = false;
|
||||||
this.booksGb.Text = "Books location";
|
this.booksGb.Text = "Books location";
|
||||||
//
|
//
|
||||||
|
// saveEpisodesToSeriesFolderCbox
|
||||||
|
//
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.AutoSize = true;
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.Location = new System.Drawing.Point(7, 131);
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.Name = "saveEpisodesToSeriesFolderCbox";
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.Size = new System.Drawing.Size(191, 19);
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.TabIndex = 3;
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.Text = "[Save Episodes To Series Folder]";
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
// tab2ImportLibrary
|
// tab2ImportLibrary
|
||||||
//
|
//
|
||||||
this.tab2ImportLibrary.Controls.Add(this.autoDownloadEpisodesCb);
|
this.tab2ImportLibrary.Controls.Add(this.autoDownloadEpisodesCb);
|
||||||
@ -508,9 +523,9 @@
|
|||||||
this.chapterFileTemplateLbl.AutoSize = true;
|
this.chapterFileTemplateLbl.AutoSize = true;
|
||||||
this.chapterFileTemplateLbl.Location = new System.Drawing.Point(6, 107);
|
this.chapterFileTemplateLbl.Location = new System.Drawing.Point(6, 107);
|
||||||
this.chapterFileTemplateLbl.Name = "chapterFileTemplateLbl";
|
this.chapterFileTemplateLbl.Name = "chapterFileTemplateLbl";
|
||||||
this.chapterFileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
this.chapterFileTemplateLbl.Size = new System.Drawing.Size(132, 15);
|
||||||
this.chapterFileTemplateLbl.TabIndex = 6;
|
this.chapterFileTemplateLbl.TabIndex = 6;
|
||||||
this.chapterFileTemplateLbl.Text = "[folder template desc]";
|
this.chapterFileTemplateLbl.Text = "[chapter template desc]";
|
||||||
//
|
//
|
||||||
// fileTemplateBtn
|
// fileTemplateBtn
|
||||||
//
|
//
|
||||||
@ -538,9 +553,9 @@
|
|||||||
this.fileTemplateLbl.AutoSize = true;
|
this.fileTemplateLbl.AutoSize = true;
|
||||||
this.fileTemplateLbl.Location = new System.Drawing.Point(6, 63);
|
this.fileTemplateLbl.Location = new System.Drawing.Point(6, 63);
|
||||||
this.fileTemplateLbl.Name = "fileTemplateLbl";
|
this.fileTemplateLbl.Name = "fileTemplateLbl";
|
||||||
this.fileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
this.fileTemplateLbl.Size = new System.Drawing.Size(108, 15);
|
||||||
this.fileTemplateLbl.TabIndex = 3;
|
this.fileTemplateLbl.TabIndex = 3;
|
||||||
this.fileTemplateLbl.Text = "[folder template desc]";
|
this.fileTemplateLbl.Text = "[file template desc]";
|
||||||
//
|
//
|
||||||
// folderTemplateBtn
|
// folderTemplateBtn
|
||||||
//
|
//
|
||||||
@ -574,6 +589,7 @@
|
|||||||
//
|
//
|
||||||
// tab4AudioFileOptions
|
// tab4AudioFileOptions
|
||||||
//
|
//
|
||||||
|
this.tab4AudioFileOptions.Controls.Add(this.chapterTitleTemplateGb);
|
||||||
this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb);
|
this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb);
|
||||||
this.tab4AudioFileOptions.Controls.Add(this.convertLossyRb);
|
this.tab4AudioFileOptions.Controls.Add(this.convertLossyRb);
|
||||||
this.tab4AudioFileOptions.Controls.Add(this.stripAudibleBrandingCbox);
|
this.tab4AudioFileOptions.Controls.Add(this.stripAudibleBrandingCbox);
|
||||||
@ -592,6 +608,38 @@
|
|||||||
this.tab4AudioFileOptions.Text = "Audio File Options";
|
this.tab4AudioFileOptions.Text = "Audio File Options";
|
||||||
this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
|
// chapterTitleTemplateGb
|
||||||
|
//
|
||||||
|
this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateBtn);
|
||||||
|
this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateTb);
|
||||||
|
this.chapterTitleTemplateGb.Location = new System.Drawing.Point(6, 335);
|
||||||
|
this.chapterTitleTemplateGb.Name = "chapterTitleTemplateGb";
|
||||||
|
this.chapterTitleTemplateGb.Size = new System.Drawing.Size(842, 54);
|
||||||
|
this.chapterTitleTemplateGb.TabIndex = 18;
|
||||||
|
this.chapterTitleTemplateGb.TabStop = false;
|
||||||
|
this.chapterTitleTemplateGb.Text = "[chapter title template desc]";
|
||||||
|
//
|
||||||
|
// chapterTitleTemplateBtn
|
||||||
|
//
|
||||||
|
this.chapterTitleTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.chapterTitleTemplateBtn.Location = new System.Drawing.Point(764, 22);
|
||||||
|
this.chapterTitleTemplateBtn.Name = "chapterTitleTemplateBtn";
|
||||||
|
this.chapterTitleTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||||
|
this.chapterTitleTemplateBtn.TabIndex = 17;
|
||||||
|
this.chapterTitleTemplateBtn.Text = "Edit...";
|
||||||
|
this.chapterTitleTemplateBtn.UseVisualStyleBackColor = true;
|
||||||
|
this.chapterTitleTemplateBtn.Click += new System.EventHandler(this.chapterTitleTemplateBtn_Click);
|
||||||
|
//
|
||||||
|
// chapterTitleTemplateTb
|
||||||
|
//
|
||||||
|
this.chapterTitleTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.chapterTitleTemplateTb.Location = new System.Drawing.Point(6, 22);
|
||||||
|
this.chapterTitleTemplateTb.Name = "chapterTitleTemplateTb";
|
||||||
|
this.chapterTitleTemplateTb.ReadOnly = true;
|
||||||
|
this.chapterTitleTemplateTb.Size = new System.Drawing.Size(752, 23);
|
||||||
|
this.chapterTitleTemplateTb.TabIndex = 16;
|
||||||
|
//
|
||||||
// lameOptionsGb
|
// lameOptionsGb
|
||||||
//
|
//
|
||||||
this.lameOptionsGb.Controls.Add(this.lameDownsampleMonoCbox);
|
this.lameOptionsGb.Controls.Add(this.lameDownsampleMonoCbox);
|
||||||
@ -599,9 +647,9 @@
|
|||||||
this.lameOptionsGb.Controls.Add(this.label1);
|
this.lameOptionsGb.Controls.Add(this.label1);
|
||||||
this.lameOptionsGb.Controls.Add(this.lameQualityGb);
|
this.lameOptionsGb.Controls.Add(this.lameQualityGb);
|
||||||
this.lameOptionsGb.Controls.Add(this.groupBox2);
|
this.lameOptionsGb.Controls.Add(this.groupBox2);
|
||||||
this.lameOptionsGb.Location = new System.Drawing.Point(415, 18);
|
this.lameOptionsGb.Location = new System.Drawing.Point(415, 6);
|
||||||
this.lameOptionsGb.Name = "lameOptionsGb";
|
this.lameOptionsGb.Name = "lameOptionsGb";
|
||||||
this.lameOptionsGb.Size = new System.Drawing.Size(433, 371);
|
this.lameOptionsGb.Size = new System.Drawing.Size(433, 323);
|
||||||
this.lameOptionsGb.TabIndex = 14;
|
this.lameOptionsGb.TabIndex = 14;
|
||||||
this.lameOptionsGb.TabStop = false;
|
this.lameOptionsGb.TabStop = false;
|
||||||
this.lameOptionsGb.Text = "Mp3 Encoding Options";
|
this.lameOptionsGb.Text = "Mp3 Encoding Options";
|
||||||
@ -629,7 +677,7 @@
|
|||||||
this.lameBitrateGb.Controls.Add(this.lameBitrateTb);
|
this.lameBitrateGb.Controls.Add(this.lameBitrateTb);
|
||||||
this.lameBitrateGb.Location = new System.Drawing.Point(6, 84);
|
this.lameBitrateGb.Location = new System.Drawing.Point(6, 84);
|
||||||
this.lameBitrateGb.Name = "lameBitrateGb";
|
this.lameBitrateGb.Name = "lameBitrateGb";
|
||||||
this.lameBitrateGb.Size = new System.Drawing.Size(421, 112);
|
this.lameBitrateGb.Size = new System.Drawing.Size(421, 101);
|
||||||
this.lameBitrateGb.TabIndex = 0;
|
this.lameBitrateGb.TabIndex = 0;
|
||||||
this.lameBitrateGb.TabStop = false;
|
this.lameBitrateGb.TabStop = false;
|
||||||
this.lameBitrateGb.Text = "Bitrate";
|
this.lameBitrateGb.Text = "Bitrate";
|
||||||
@ -637,7 +685,7 @@
|
|||||||
// LameMatchSourceBRCbox
|
// LameMatchSourceBRCbox
|
||||||
//
|
//
|
||||||
this.LameMatchSourceBRCbox.AutoSize = true;
|
this.LameMatchSourceBRCbox.AutoSize = true;
|
||||||
this.LameMatchSourceBRCbox.Location = new System.Drawing.Point(260, 87);
|
this.LameMatchSourceBRCbox.Location = new System.Drawing.Point(260, 77);
|
||||||
this.LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox";
|
this.LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox";
|
||||||
this.LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19);
|
this.LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19);
|
||||||
this.LameMatchSourceBRCbox.TabIndex = 3;
|
this.LameMatchSourceBRCbox.TabIndex = 3;
|
||||||
@ -648,7 +696,7 @@
|
|||||||
// lameConstantBitrateCbox
|
// lameConstantBitrateCbox
|
||||||
//
|
//
|
||||||
this.lameConstantBitrateCbox.AutoSize = true;
|
this.lameConstantBitrateCbox.AutoSize = true;
|
||||||
this.lameConstantBitrateCbox.Location = new System.Drawing.Point(6, 87);
|
this.lameConstantBitrateCbox.Location = new System.Drawing.Point(6, 77);
|
||||||
this.lameConstantBitrateCbox.Name = "lameConstantBitrateCbox";
|
this.lameConstantBitrateCbox.Name = "lameConstantBitrateCbox";
|
||||||
this.lameConstantBitrateCbox.Size = new System.Drawing.Size(216, 19);
|
this.lameConstantBitrateCbox.Size = new System.Drawing.Size(216, 19);
|
||||||
this.lameConstantBitrateCbox.TabIndex = 2;
|
this.lameConstantBitrateCbox.TabIndex = 2;
|
||||||
@ -734,7 +782,7 @@
|
|||||||
this.label1.AutoSize = true;
|
this.label1.AutoSize = true;
|
||||||
this.label1.Enabled = false;
|
this.label1.Enabled = false;
|
||||||
this.label1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Italic, System.Drawing.GraphicsUnit.Point);
|
this.label1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Italic, System.Drawing.GraphicsUnit.Point);
|
||||||
this.label1.Location = new System.Drawing.Point(6, 353);
|
this.label1.Location = new System.Drawing.Point(6, 298);
|
||||||
this.label1.Name = "label1";
|
this.label1.Name = "label1";
|
||||||
this.label1.Size = new System.Drawing.Size(172, 15);
|
this.label1.Size = new System.Drawing.Size(172, 15);
|
||||||
this.label1.TabIndex = 1;
|
this.label1.TabIndex = 1;
|
||||||
@ -755,7 +803,7 @@
|
|||||||
this.lameQualityGb.Controls.Add(this.label14);
|
this.lameQualityGb.Controls.Add(this.label14);
|
||||||
this.lameQualityGb.Controls.Add(this.label2);
|
this.lameQualityGb.Controls.Add(this.label2);
|
||||||
this.lameQualityGb.Controls.Add(this.lameVBRQualityTb);
|
this.lameQualityGb.Controls.Add(this.lameVBRQualityTb);
|
||||||
this.lameQualityGb.Location = new System.Drawing.Point(6, 202);
|
this.lameQualityGb.Location = new System.Drawing.Point(6, 186);
|
||||||
this.lameQualityGb.Name = "lameQualityGb";
|
this.lameQualityGb.Name = "lameQualityGb";
|
||||||
this.lameQualityGb.Size = new System.Drawing.Size(421, 109);
|
this.lameQualityGb.Size = new System.Drawing.Size(421, 109);
|
||||||
this.lameQualityGb.TabIndex = 0;
|
this.lameQualityGb.TabIndex = 0;
|
||||||
@ -963,16 +1011,6 @@
|
|||||||
this.createCueSheetCbox.UseVisualStyleBackColor = true;
|
this.createCueSheetCbox.UseVisualStyleBackColor = true;
|
||||||
this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||||
//
|
//
|
||||||
// saveEpisodesToSeriesFolderCbox
|
|
||||||
//
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.AutoSize = true;
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.Location = new System.Drawing.Point(7, 131);
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.Name = "saveEpisodesToSeriesFolderCbox";
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.Size = new System.Drawing.Size(191, 19);
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.TabIndex = 3;
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.Text = "[Save Episodes To Series Folder]";
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// SettingsDialog
|
// SettingsDialog
|
||||||
//
|
//
|
||||||
this.AcceptButton = this.saveBtn;
|
this.AcceptButton = this.saveBtn;
|
||||||
@ -1007,6 +1045,8 @@
|
|||||||
this.customFileNamingGb.PerformLayout();
|
this.customFileNamingGb.PerformLayout();
|
||||||
this.tab4AudioFileOptions.ResumeLayout(false);
|
this.tab4AudioFileOptions.ResumeLayout(false);
|
||||||
this.tab4AudioFileOptions.PerformLayout();
|
this.tab4AudioFileOptions.PerformLayout();
|
||||||
|
this.chapterTitleTemplateGb.ResumeLayout(false);
|
||||||
|
this.chapterTitleTemplateGb.PerformLayout();
|
||||||
this.lameOptionsGb.ResumeLayout(false);
|
this.lameOptionsGb.ResumeLayout(false);
|
||||||
this.lameOptionsGb.PerformLayout();
|
this.lameOptionsGb.PerformLayout();
|
||||||
this.lameBitrateGb.ResumeLayout(false);
|
this.lameBitrateGb.ResumeLayout(false);
|
||||||
@ -1098,5 +1138,8 @@
|
|||||||
private System.Windows.Forms.CheckBox downloadCoverArtCbox;
|
private System.Windows.Forms.CheckBox downloadCoverArtCbox;
|
||||||
private System.Windows.Forms.CheckBox autoDownloadEpisodesCb;
|
private System.Windows.Forms.CheckBox autoDownloadEpisodesCb;
|
||||||
private System.Windows.Forms.CheckBox saveEpisodesToSeriesFolderCbox;
|
private System.Windows.Forms.CheckBox saveEpisodesToSeriesFolderCbox;
|
||||||
|
private System.Windows.Forms.GroupBox chapterTitleTemplateGb;
|
||||||
|
private System.Windows.Forms.Button chapterTitleTemplateBtn;
|
||||||
|
private System.Windows.Forms.TextBox chapterTitleTemplateTb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Dinah.Core;
|
||||||
|
using LibationFileManager;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs
|
||||||
|
{
|
||||||
|
public partial class SettingsDialog
|
||||||
|
{
|
||||||
|
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
|
||||||
|
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
|
||||||
|
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
|
||||||
|
|
||||||
|
private void Load_DownloadDecrypt(Configuration config)
|
||||||
|
{
|
||||||
|
inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||||
|
badBookGb.Text = desc(nameof(config.BadBook));
|
||||||
|
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
|
||||||
|
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
|
||||||
|
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
||||||
|
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
||||||
|
|
||||||
|
inProgressSelectControl.SetDirectoryItems(new()
|
||||||
|
{
|
||||||
|
Configuration.KnownDirectories.WinTemp,
|
||||||
|
Configuration.KnownDirectories.UserProfile,
|
||||||
|
Configuration.KnownDirectories.AppDir,
|
||||||
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
Configuration.KnownDirectories.LibationFiles
|
||||||
|
}, Configuration.KnownDirectories.WinTemp);
|
||||||
|
inProgressSelectControl.SelectDirectory(config.InProgress);
|
||||||
|
|
||||||
|
var rb = config.BadBook switch
|
||||||
|
{
|
||||||
|
Configuration.BadBookAction.Ask => this.badBookAskRb,
|
||||||
|
Configuration.BadBookAction.Abort => this.badBookAbortRb,
|
||||||
|
Configuration.BadBookAction.Retry => this.badBookRetryRb,
|
||||||
|
Configuration.BadBookAction.Ignore => this.badBookIgnoreRb,
|
||||||
|
_ => this.badBookAskRb
|
||||||
|
};
|
||||||
|
rb.Checked = true;
|
||||||
|
|
||||||
|
folderTemplateLbl.Text = desc(nameof(config.FolderTemplate));
|
||||||
|
fileTemplateLbl.Text = desc(nameof(config.FileTemplate));
|
||||||
|
chapterFileTemplateLbl.Text = desc(nameof(config.ChapterFileTemplate));
|
||||||
|
folderTemplateTb.Text = config.FolderTemplate;
|
||||||
|
fileTemplateTb.Text = config.FileTemplate;
|
||||||
|
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_DownloadDecrypt(Configuration config)
|
||||||
|
{
|
||||||
|
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||||
|
|
||||||
|
config.BadBook
|
||||||
|
= badBookAskRb.Checked ? Configuration.BadBookAction.Ask
|
||||||
|
: badBookAbortRb.Checked ? Configuration.BadBookAction.Abort
|
||||||
|
: badBookRetryRb.Checked ? Configuration.BadBookAction.Retry
|
||||||
|
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
||||||
|
: Configuration.BadBookAction.Ask;
|
||||||
|
|
||||||
|
config.FolderTemplate = folderTemplateTb.Text;
|
||||||
|
config.FileTemplate = fileTemplateTb.Text;
|
||||||
|
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
using LibationFileManager;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs
|
||||||
|
{
|
||||||
|
public partial class SettingsDialog
|
||||||
|
{
|
||||||
|
private void Load_ImportLibrary(Configuration config)
|
||||||
|
{
|
||||||
|
this.autoScanCb.Text = desc(nameof(config.AutoScan));
|
||||||
|
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||||
|
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||||
|
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||||
|
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
||||||
|
|
||||||
|
autoScanCb.Checked = config.AutoScan;
|
||||||
|
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||||
|
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||||
|
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||||
|
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
||||||
|
}
|
||||||
|
private void Save_ImportLibrary(Configuration config)
|
||||||
|
{
|
||||||
|
config.AutoScan = autoScanCb.Checked;
|
||||||
|
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||||
|
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||||
|
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||||
|
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs
Normal file
93
Source/LibationWinForms/Dialogs/SettingsDialog.Important.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using Dinah.Core;
|
||||||
|
using FileManager;
|
||||||
|
using LibationFileManager;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs
|
||||||
|
{
|
||||||
|
public partial class SettingsDialog
|
||||||
|
{
|
||||||
|
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||||
|
|
||||||
|
private void Load_Important(Configuration config)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
loggingLevelCb.Items.Clear();
|
||||||
|
foreach (var level in Enum<Serilog.Events.LogEventLevel>.GetValues())
|
||||||
|
loggingLevelCb.Items.Add(level);
|
||||||
|
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||||
|
this.saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
|
||||||
|
|
||||||
|
booksSelectControl.SetSearchTitle("books location");
|
||||||
|
booksSelectControl.SetDirectoryItems(
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Configuration.KnownDirectories.UserProfile,
|
||||||
|
Configuration.KnownDirectories.AppDir,
|
||||||
|
Configuration.KnownDirectories.MyDocs
|
||||||
|
},
|
||||||
|
Configuration.KnownDirectories.UserProfile,
|
||||||
|
"Books");
|
||||||
|
booksSelectControl.SelectDirectory(config.Books);
|
||||||
|
|
||||||
|
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Important(Configuration config)
|
||||||
|
{
|
||||||
|
var newBooks = booksSelectControl.SelectedDirectory;
|
||||||
|
|
||||||
|
#region validation
|
||||||
|
static void validationError(string text, string caption)
|
||||||
|
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
if (string.IsNullOrWhiteSpace(newBooks))
|
||||||
|
{
|
||||||
|
validationError("Cannot set Books Location to blank", "Location is blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
||||||
|
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
|
||||||
|
{
|
||||||
|
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Templates.File.IsValid(fileTemplateTb.Text))
|
||||||
|
{
|
||||||
|
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
|
||||||
|
{
|
||||||
|
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
LongPath lonNewBooks = newBooks;
|
||||||
|
if (!Directory.Exists(lonNewBooks))
|
||||||
|
Directory.CreateDirectory(lonNewBooks);
|
||||||
|
|
||||||
|
config.Books = newBooks;
|
||||||
|
|
||||||
|
{
|
||||||
|
var logLevelOld = config.LogLevel;
|
||||||
|
var logLevelNew = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem;
|
||||||
|
|
||||||
|
config.LogLevel = logLevelNew;
|
||||||
|
|
||||||
|
// only warn if changed during this time. don't want to warn every time user happens to change settings while level is verbose
|
||||||
|
if (logLevelOld != logLevelNew)
|
||||||
|
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.IO;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Dinah.Core;
|
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs
|
namespace LibationWinForms.Dialogs
|
||||||
@ -24,111 +21,12 @@ namespace LibationWinForms.Dialogs
|
|||||||
if (this.DesignMode)
|
if (this.DesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
{
|
Load_Important(config);
|
||||||
loggingLevelCb.Items.Clear();
|
Load_ImportLibrary(config);
|
||||||
foreach (var level in Enum<Serilog.Events.LogEventLevel>.GetValues())
|
Load_DownloadDecrypt(config);
|
||||||
loggingLevelCb.Items.Add(level);
|
Load_AudioSettings(config);
|
||||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoScanCb.Text = desc(nameof(config.AutoScan));
|
|
||||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
|
||||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
|
||||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
|
||||||
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
|
||||||
|
|
||||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
|
||||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
|
||||||
this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup));
|
|
||||||
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
|
||||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
|
||||||
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
|
||||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
|
||||||
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
|
|
||||||
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
|
|
||||||
this.saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
|
|
||||||
|
|
||||||
booksSelectControl.SetSearchTitle("books location");
|
|
||||||
booksSelectControl.SetDirectoryItems(
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Configuration.KnownDirectories.UserProfile,
|
|
||||||
Configuration.KnownDirectories.AppDir,
|
|
||||||
Configuration.KnownDirectories.MyDocs
|
|
||||||
},
|
|
||||||
Configuration.KnownDirectories.UserProfile,
|
|
||||||
"Books");
|
|
||||||
booksSelectControl.SelectDirectory(config.Books);
|
|
||||||
|
|
||||||
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
|
||||||
|
|
||||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
|
||||||
createCueSheetCbox.Checked = config.CreateCueSheet;
|
|
||||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
|
||||||
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
|
||||||
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
|
||||||
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
|
|
||||||
convertLosslessRb.Checked = !config.DecryptToLossy;
|
|
||||||
convertLossyRb.Checked = config.DecryptToLossy;
|
|
||||||
|
|
||||||
lameTargetBitrateRb.Checked = config.LameTargetBitrate;
|
|
||||||
lameTargetQualityRb.Checked = !config.LameTargetBitrate;
|
|
||||||
lameDownsampleMonoCbox.Checked = config.LameDownsampleMono;
|
|
||||||
lameBitrateTb.Value = config.LameBitrate;
|
|
||||||
lameConstantBitrateCbox.Checked = config.LameConstantBitrate;
|
|
||||||
LameMatchSourceBRCbox.Checked = config.LameMatchSourceBR;
|
|
||||||
lameVBRQualityTb.Value = config.LameVBRQuality;
|
|
||||||
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
|
|
||||||
|
|
||||||
autoScanCb.Checked = config.AutoScan;
|
|
||||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
|
||||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
|
||||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
|
||||||
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
|
||||||
|
|
||||||
lameTargetRb_CheckedChanged(this, e);
|
|
||||||
LameMatchSourceBRCbox_CheckedChanged(this, e);
|
|
||||||
convertFormatRb_CheckedChanged(this, e);
|
|
||||||
allowLibationFixupCbox_CheckedChanged(this, e);
|
|
||||||
|
|
||||||
inProgressSelectControl.SetDirectoryItems(new()
|
|
||||||
{
|
|
||||||
Configuration.KnownDirectories.WinTemp,
|
|
||||||
Configuration.KnownDirectories.UserProfile,
|
|
||||||
Configuration.KnownDirectories.AppDir,
|
|
||||||
Configuration.KnownDirectories.MyDocs,
|
|
||||||
Configuration.KnownDirectories.LibationFiles
|
|
||||||
}, Configuration.KnownDirectories.WinTemp);
|
|
||||||
inProgressSelectControl.SelectDirectory(config.InProgress);
|
|
||||||
|
|
||||||
badBookGb.Text = desc(nameof(config.BadBook));
|
|
||||||
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
|
|
||||||
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
|
|
||||||
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
|
||||||
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
|
||||||
var rb = config.BadBook switch
|
|
||||||
{
|
|
||||||
Configuration.BadBookAction.Ask => this.badBookAskRb,
|
|
||||||
Configuration.BadBookAction.Abort => this.badBookAbortRb,
|
|
||||||
Configuration.BadBookAction.Retry => this.badBookRetryRb,
|
|
||||||
Configuration.BadBookAction.Ignore => this.badBookIgnoreRb,
|
|
||||||
_ => this.badBookAskRb
|
|
||||||
};
|
|
||||||
rb.Checked = true;
|
|
||||||
|
|
||||||
folderTemplateLbl.Text = desc(nameof(config.FolderTemplate));
|
|
||||||
fileTemplateLbl.Text = desc(nameof(config.FileTemplate));
|
|
||||||
chapterFileTemplateLbl.Text = desc(nameof(config.ChapterFileTemplate));
|
|
||||||
folderTemplateTb.Text = config.FolderTemplate;
|
|
||||||
fileTemplateTb.Text = config.FileTemplate;
|
|
||||||
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
|
|
||||||
|
|
||||||
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
|
|
||||||
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
|
|
||||||
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
|
|
||||||
private static void editTemplate(Templates template, TextBox textBox)
|
private static void editTemplate(Templates template, TextBox textBox)
|
||||||
{
|
{
|
||||||
var form = new EditTemplateDialog(template, textBox.Text);
|
var form = new EditTemplateDialog(template, textBox.Text);
|
||||||
@ -138,93 +36,10 @@ namespace LibationWinForms.Dialogs
|
|||||||
|
|
||||||
private void saveBtn_Click(object sender, EventArgs e)
|
private void saveBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var newBooks = booksSelectControl.SelectedDirectory;
|
Save_Important(config);
|
||||||
|
Save_ImportLibrary(config);
|
||||||
#region validation
|
Save_DownloadDecrypt(config);
|
||||||
static void validationError(string text, string caption)
|
Save_AudioSettings(config);
|
||||||
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
if (string.IsNullOrWhiteSpace(newBooks))
|
|
||||||
{
|
|
||||||
validationError("Cannot set Books Location to blank", "Location is blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsCustom)
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
|
||||||
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Templates.File.IsValid(fileTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsKnown)
|
|
||||||
Directory.CreateDirectory(newBooks);
|
|
||||||
|
|
||||||
config.Books = newBooks;
|
|
||||||
|
|
||||||
{
|
|
||||||
var logLevelOld = config.LogLevel;
|
|
||||||
var logLevelNew = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem;
|
|
||||||
|
|
||||||
config.LogLevel = logLevelNew;
|
|
||||||
|
|
||||||
// only warn if changed during this time. don't want to warn every time user happens to change settings while level is verbose
|
|
||||||
if (logLevelOld != logLevelNew)
|
|
||||||
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
|
|
||||||
|
|
||||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
|
||||||
config.CreateCueSheet = createCueSheetCbox.Checked;
|
|
||||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
|
||||||
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
|
||||||
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
|
||||||
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
|
|
||||||
config.DecryptToLossy = convertLossyRb.Checked;
|
|
||||||
|
|
||||||
config.LameTargetBitrate = lameTargetBitrateRb.Checked;
|
|
||||||
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
|
|
||||||
config.LameBitrate = lameBitrateTb.Value;
|
|
||||||
config.LameConstantBitrate = lameConstantBitrateCbox.Checked;
|
|
||||||
config.LameMatchSourceBR = LameMatchSourceBRCbox.Checked;
|
|
||||||
config.LameVBRQuality = lameVBRQualityTb.Value;
|
|
||||||
config.DownloadCoverArt = downloadCoverArtCbox.Checked;
|
|
||||||
|
|
||||||
config.AutoScan = autoScanCb.Checked;
|
|
||||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
|
||||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
|
||||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
|
||||||
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
|
||||||
|
|
||||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
|
||||||
|
|
||||||
config.BadBook
|
|
||||||
= badBookAskRb.Checked ? Configuration.BadBookAction.Ask
|
|
||||||
: badBookAbortRb.Checked ? Configuration.BadBookAction.Abort
|
|
||||||
: badBookRetryRb.Checked ? Configuration.BadBookAction.Retry
|
|
||||||
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
|
||||||
: Configuration.BadBookAction.Ask;
|
|
||||||
|
|
||||||
config.FolderTemplate = folderTemplateTb.Text;
|
|
||||||
config.FileTemplate = fileTemplateTb.Text;
|
|
||||||
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
|
||||||
|
|
||||||
this.DialogResult = DialogResult.OK;
|
this.DialogResult = DialogResult.OK;
|
||||||
this.Close();
|
this.Close();
|
||||||
@ -235,6 +50,5 @@ namespace LibationWinForms.Dialogs
|
|||||||
this.DialogResult = DialogResult.Cancel;
|
this.DialogResult = DialogResult.Cancel;
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
|
await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
|
||||||
.Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated)));
|
.Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product)));
|
||||||
}
|
}
|
||||||
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
// liberated: open explorer to file
|
// liberated: open explorer to file
|
||||||
var filePath = AudibleFileStorage.Audio.GetPath(e.Book.AudibleProductId);
|
var filePath = AudibleFileStorage.Audio.GetPath(e.Book.AudibleProductId);
|
||||||
if (!Go.To.File(filePath))
|
if (!Go.To.File(filePath?.ShortPathName))
|
||||||
{
|
{
|
||||||
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
||||||
MessageBox.Show($"File not found" + suffix);
|
MessageBox.Show($"File not found" + suffix);
|
||||||
|
|||||||
@ -37,13 +37,18 @@
|
|||||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Form1.*.cs">
|
<Compile Update="Form1.*.cs">
|
||||||
<DependentUpon>Form1.cs</DependentUpon>
|
<DependentUpon>Form1.cs</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Dialogs\SettingsDialog.*.cs">
|
||||||
|
<DependentUpon>SettingsDialog.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Properties\Resources.Designer.cs">
|
<Compile Update="Properties\Resources.Designer.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
|
|||||||
@ -138,15 +138,12 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Cancel()
|
public async Task CancelAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (CurrentProcessable is AudioDecodable audioDecodable)
|
if (CurrentProcessable is AudioDecodable audioDecodable)
|
||||||
{
|
await audioDecodable.CancelAsync();
|
||||||
//There's some threadding bug that causes this to hang if executed synchronously.
|
|
||||||
await Task.Run(audioDecodable.Cancel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -209,10 +209,10 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
toolStripProgressBar1.Value = Queue.Completed.Count;
|
toolStripProgressBar1.Value = Queue.Completed.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelAllBtn_Click(object sender, EventArgs e)
|
private async void cancelAllBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Queue.ClearQueue();
|
Queue.ClearQueue();
|
||||||
Queue.Current?.Cancel();
|
await Queue.Current?.CancelAsync();
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||||
UpdateAllControls();
|
UpdateAllControls();
|
||||||
}
|
}
|
||||||
@ -331,7 +331,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
ProcessBook item = Queue[queueIndex];
|
ProcessBook item = Queue[queueIndex];
|
||||||
if (buttonName == nameof(panelClicked.cancelBtn))
|
if (buttonName == nameof(panelClicked.cancelBtn))
|
||||||
{
|
{
|
||||||
await item.Cancel();
|
await item.CancelAsync();
|
||||||
Queue.RemoveQueued(item);
|
Queue.RemoveQueued(item);
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,12 +15,17 @@ namespace FileNamingTemplateTests
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void equiv_GetValidFilename()
|
public void equiv_GetValidFilename()
|
||||||
{
|
{
|
||||||
var expected = @"C:\foo\bar\my_ book LONG_1234567890_1234567890_1234567890_123 [ID123456].txt";
|
var sb = new System.Text.StringBuilder();
|
||||||
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
|
sb.Append('0', 300);
|
||||||
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
|
var longText = sb.ToString();
|
||||||
|
|
||||||
f1.Should().Be(expected);
|
var expectedOld = "C:\\foo\\bar\\my_ book 00000000000000000000000000000000000000000 [ID123456].txt";
|
||||||
f1.Should().Be(f2);
|
var expectedNew = "C:\\foo\\bar\\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt";
|
||||||
|
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
|
||||||
|
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
|
||||||
|
|
||||||
|
f1.Should().Be(expectedOld);
|
||||||
|
f2.Should().Be(expectedNew);
|
||||||
}
|
}
|
||||||
private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
|
private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||||
{
|
{
|
||||||
@ -58,7 +63,7 @@ namespace FileNamingTemplateTests
|
|||||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||||
fileNamingTemplate.AddParameterReplacement("title", filename);
|
fileNamingTemplate.AddParameterReplacement("title", filename);
|
||||||
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||||
return fileNamingTemplate.GetFilePath();
|
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -101,7 +106,7 @@ namespace FileNamingTemplateTests
|
|||||||
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
|
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
|
||||||
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||||
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
||||||
return fileNamingTemplate.GetFilePath();
|
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -109,7 +114,7 @@ namespace FileNamingTemplateTests
|
|||||||
{
|
{
|
||||||
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
|
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
|
||||||
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
||||||
fileNamingTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
|
fileNamingTemplate.GetFilePath().PathWithoutPrefix.Should().Be(@"\foo\slashes.txt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -17,26 +17,26 @@ namespace FileUtilityTests
|
|||||||
|
|
||||||
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
|
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow("http://test.com/a/b/c", @"http\test.com\a\b\c")]
|
[DataRow("http://test.com/a/b/c", @"http꞉\test.com\a\b\c")]
|
||||||
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
|
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
// empty replacement
|
// empty replacement
|
||||||
[DataRow("abc*abc.txt", "", "abcabc.txt")]
|
[DataRow("abc*abc.txt", "", "abc✱abc.txt")]
|
||||||
// non-empty replacement
|
// non-empty replacement
|
||||||
[DataRow("abc*abc.txt", "ZZZ", "abcZZZabc.txt")]
|
[DataRow("abc*abc.txt", "ZZZ", "abc✱abc.txt")]
|
||||||
// standardize slashes
|
// standardize slashes
|
||||||
[DataRow(@"a/b\c/d", "Z", @"a\b\c\d")]
|
[DataRow(@"a/b\c/d", "Z", @"a\b\c\d")]
|
||||||
// remove illegal chars
|
// remove illegal chars
|
||||||
[DataRow("a*?:z.txt", "Z", "aZZZz.txt")]
|
[DataRow("a*?:z.txt", "Z", "a✱?꞉z.txt")]
|
||||||
// retain drive letter path colon
|
// retain drive letter path colon
|
||||||
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
|
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
|
||||||
// replace all other colons
|
// replace all other colons
|
||||||
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
|
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\b꞉c\d.txt")]
|
||||||
// remove empty directories
|
// remove empty directories
|
||||||
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
|
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
|
||||||
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\ZZZfoo\ZZZidZZZ")]
|
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\“foo\<id>")]
|
||||||
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
|
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement).PathWithoutPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
@ -134,7 +134,7 @@ namespace FileUtilityTests
|
|||||||
// file end dots
|
// file end dots
|
||||||
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
|
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
|
||||||
public void Tests(string input, string expected)
|
public void Tests(string input, string expected)
|
||||||
=> FileUtility.GetValidFilename(input).Should().Be(expected);
|
=> FileUtility.GetValidFilename(input).PathWithoutPrefix.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
|
|||||||
@ -74,24 +74,28 @@ namespace TemplatesTests
|
|||||||
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
|
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
|
||||||
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
|
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
|
||||||
.GetFilePath()
|
.GetFilePath()
|
||||||
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void IfSeries_empty()
|
public void IfSeries_empty()
|
||||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
|
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
|
||||||
.GetFilePath()
|
.GetFilePath()
|
||||||
|
.PathWithoutPrefix
|
||||||
.Should().Be(@"C:\a\b\foobar.ext");
|
.Should().Be(@"C:\a\b\foobar.ext");
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void IfSeries_no_series()
|
public void IfSeries_no_series()
|
||||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||||
.GetFilePath()
|
.GetFilePath()
|
||||||
|
.PathWithoutPrefix
|
||||||
.Should().Be(@"C:\a\b\foobar.ext");
|
.Should().Be(@"C:\a\b\foobar.ext");
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void IfSeries_with_series()
|
public void IfSeries_with_series()
|
||||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||||
.GetFilePath()
|
.GetFilePath()
|
||||||
|
.PathWithoutPrefix
|
||||||
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
|
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user