mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-03 11:28:30 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1165f81203 | ||
|
|
19369a21ef | ||
|
|
611fb4d6d8 | ||
|
|
c77ec54035 | ||
|
|
c9c28c7826 | ||
|
|
30e2caaff5 | ||
|
|
fd56017af5 |
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>6.1.1.1</Version>
|
||||
<Version>6.1.5.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -46,25 +46,23 @@ namespace AppScaffolding
|
||||
return Configuration.Instance;
|
||||
}
|
||||
|
||||
public static void RunPostConfigMigrations()
|
||||
/// <summary>most migrations go in here</summary>
|
||||
public static void RunPostConfigMigrations(Configuration config)
|
||||
{
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
//
|
||||
// migrations go below here
|
||||
//
|
||||
|
||||
Migrations.migrate_to_v5_2_0__post_config(config);
|
||||
Migrations.migrate_to_v5_7_1(config);
|
||||
Migrations.migrate_to_v6_1_2(config);
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding()
|
||||
public static void RunPostMigrationScaffolding(Configuration config)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
@@ -329,5 +327,15 @@ namespace AppScaffolding
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
}
|
||||
|
||||
// add config.DownloadEpisodes , config.ImportEpisodes
|
||||
public static void migrate_to_v6_1_2(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||
config.ImportEpisodes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,13 @@ namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
||||
|
||||
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
//These are the minimum response groups required for the
|
||||
//library scanner to pass all validation and filtering.
|
||||
LibraryResponseGroups =
|
||||
var libraryResponseGroups =
|
||||
LibraryOptions.ResponseGroupOptions.ProductAttrs |
|
||||
LibraryOptions.ResponseGroupOptions.ProductDesc |
|
||||
LibraryOptions.ResponseGroupOptions.Relationships;
|
||||
@@ -33,7 +31,7 @@ namespace ApplicationServices
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts);
|
||||
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryResponseGroups);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = libraryItems.Count;
|
||||
@@ -68,7 +66,6 @@ namespace ApplicationServices
|
||||
}
|
||||
finally
|
||||
{
|
||||
LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
}
|
||||
@@ -85,7 +82,7 @@ namespace ApplicationServices
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts);
|
||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = importItems.Count;
|
||||
@@ -129,7 +126,7 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts)
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
foreach (var account in accounts)
|
||||
@@ -138,7 +135,7 @@ namespace ApplicationServices
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account));
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryResponseGroups));
|
||||
}
|
||||
|
||||
// import library in parallel
|
||||
@@ -147,7 +144,7 @@ namespace ApplicationServices
|
||||
return importItems;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account)
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
@@ -158,7 +155,7 @@ namespace ApplicationServices
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(LibraryResponseGroups);
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, FileManager.Configuration.Instance.ImportEpisodes);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace DtoImporterService
|
||||
|
||||
foreach (var s in requestedSeries)
|
||||
{
|
||||
var series = DbContext.Series.Local.SingleOrDefault(c => c.AudibleSeriesId == s.SeriesId);
|
||||
var series = DbContext.Series.Local.FirstOrDefault(c => c.AudibleSeriesId == s.SeriesId);
|
||||
if (series is null)
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace FileLiberator
|
||||
var dest
|
||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
? audioFileName
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||
|
||||
@@ -11,21 +11,40 @@ using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : DownloadableBase
|
||||
public class DownloadPdf : IProcessable
|
||||
{
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public event EventHandler<string> StreamingBegin;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PDF_Exists;
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
try
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
|
||||
return result;
|
||||
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
@@ -50,15 +69,26 @@ namespace FileLiberator
|
||||
|
||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
StreamingBegin?.Invoke(this, proposedDownloadFilePath);
|
||||
|
||||
var client = new HttpClient();
|
||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
||||
proposedDownloadFilePath,
|
||||
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
|
||||
try
|
||||
{
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
return actualDownloadedFilePath;
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
StatusUpdate?.Invoke(this, actualDownloadedFilePath);
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public abstract class DownloadableBase : IProcessable
|
||||
{
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public event EventHandler<string> StreamingBegin;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
|
||||
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
|
||||
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
return await ProcessItemAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
|
||||
|
||||
StreamingBegin?.Invoke(this, proposedDownloadFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await func(progress);
|
||||
StatusUpdate?.Invoke(this, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,12 @@ namespace FileLiberator
|
||||
{
|
||||
public static class IProcessableExt
|
||||
{
|
||||
//
|
||||
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
|
||||
// ProcessAsync() often does a lot with forms in the UI context
|
||||
//
|
||||
|
||||
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook => processable.Validate(libraryBook));
|
||||
=> library.Where(libraryBook =>
|
||||
processable.Validate(libraryBook)
|
||||
&& (libraryBook.Book.ContentType != ContentType.Episode || FileManager.Configuration.Instance.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
private static object bookDirectoryFilesLocker { get; } = new();
|
||||
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
#endregion
|
||||
|
||||
@@ -47,7 +48,7 @@ namespace FileManager
|
||||
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
}
|
||||
@@ -58,7 +59,8 @@ namespace FileManager
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
string regexPattern = $@"{productId}.*?\.({extAggr})$";
|
||||
var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase);
|
||||
|
||||
string firstOrNull;
|
||||
|
||||
if (StorageDirectory == BooksDirectory)
|
||||
@@ -66,7 +68,7 @@ namespace FileManager
|
||||
//If user changed the BooksDirectory, reinitialize.
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
lock (BookDirectoryFiles)
|
||||
lock (bookDirectoryFilesLocker)
|
||||
{
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
@@ -75,14 +77,14 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase);
|
||||
firstOrNull = BookDirectoryFiles.FindFile(regex);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
|
||||
.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
if (firstOrNull is null)
|
||||
|
||||
@@ -3,8 +3,6 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileManager
|
||||
@@ -25,6 +23,8 @@ namespace FileManager
|
||||
private FileSystemWatcher fileSystemWatcher { get; set; }
|
||||
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
||||
private Task backgroundScanner { get; set; }
|
||||
|
||||
private object fsCacheLocker { get; } = new();
|
||||
private List<string> fsCache { get; } = new();
|
||||
|
||||
public BackgroundFileSystem(string rootDirectory, string searchPattern, SearchOption searchOptions)
|
||||
@@ -36,17 +36,15 @@ namespace FileManager
|
||||
Init();
|
||||
}
|
||||
|
||||
public string FindFile(string regexPattern, RegexOptions options)
|
||||
public string FindFile(System.Text.RegularExpressions.Regex regex)
|
||||
{
|
||||
lock (fsCache)
|
||||
{
|
||||
return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options));
|
||||
}
|
||||
lock (fsCacheLocker)
|
||||
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
public void RefreshFiles()
|
||||
{
|
||||
lock (fsCache)
|
||||
lock (fsCacheLocker)
|
||||
{
|
||||
fsCache.Clear();
|
||||
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
||||
@@ -57,17 +55,19 @@ namespace FileManager
|
||||
{
|
||||
Stop();
|
||||
|
||||
lock (fsCache)
|
||||
lock (fsCacheLocker)
|
||||
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
||||
|
||||
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory);
|
||||
fileSystemWatcher.Created += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
fileSystemWatcher.Created += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Error += FileSystemWatcher_Error;
|
||||
fileSystemWatcher.IncludeSubdirectories = true;
|
||||
fileSystemWatcher.EnableRaisingEvents = true;
|
||||
|
||||
backgroundScanner = new Task(BackgroundScanner);
|
||||
backgroundScanner.Start();
|
||||
@@ -86,7 +86,7 @@ namespace FileManager
|
||||
//Dispose of directoryChangesEvents after backgroundScanner exists.
|
||||
directoryChangesEvents?.Dispose();
|
||||
|
||||
lock (fsCache)
|
||||
lock (fsCacheLocker)
|
||||
fsCache.Clear();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace FileManager
|
||||
{
|
||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
||||
{
|
||||
lock (fsCache)
|
||||
lock (fsCacheLocker)
|
||||
UpdateLocalCache(change);
|
||||
}
|
||||
}
|
||||
@@ -146,9 +146,7 @@ namespace FileManager
|
||||
private void AddUniqueFiles(IEnumerable<string> newFiles)
|
||||
{
|
||||
foreach (var file in newFiles)
|
||||
{
|
||||
AddUniqueFile(file);
|
||||
}
|
||||
}
|
||||
private void AddUniqueFile(string newFile)
|
||||
{
|
||||
|
||||
@@ -125,6 +125,20 @@ namespace FileManager
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||
}
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region known directories
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace FileManager
|
||||
// file max length = 255. dir max len = 247
|
||||
|
||||
// sanitize
|
||||
filename = GetAsciiTag(filename);
|
||||
filename = getAsciiTag(filename);
|
||||
// manage length
|
||||
if (filename.Length > 50)
|
||||
filename = filename.Substring(0, 50) + "[...]";
|
||||
@@ -35,7 +35,7 @@ namespace FileManager
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
public static string GetAsciiTag(string property)
|
||||
private static string getAsciiTag(string property)
|
||||
{
|
||||
if (property == null)
|
||||
return "";
|
||||
|
||||
@@ -43,11 +43,12 @@ namespace FileManager
|
||||
public static event EventHandler<PictureCachedEventArgs> PictureCached;
|
||||
|
||||
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
|
||||
private static object cacheLocker { get; } = new object();
|
||||
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
|
||||
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
|
||||
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
|
||||
{
|
||||
lock (cache)
|
||||
lock (cacheLocker)
|
||||
{
|
||||
if (cache.ContainsKey(def))
|
||||
return (false, cache[def]);
|
||||
@@ -67,7 +68,7 @@ namespace FileManager
|
||||
|
||||
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
||||
{
|
||||
lock (cache)
|
||||
lock (cacheLocker)
|
||||
{
|
||||
if (!cache.ContainsKey(def) || cache[def] == null)
|
||||
{
|
||||
@@ -104,7 +105,7 @@ namespace FileManager
|
||||
|
||||
var bytes = downloadBytes(def);
|
||||
saveFile(def, bytes);
|
||||
lock (cache)
|
||||
lock (cacheLocker)
|
||||
cache[def] = bytes;
|
||||
|
||||
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes });
|
||||
|
||||
@@ -106,16 +106,16 @@ namespace InternalUtilities
|
||||
// 2 retries == 3 total
|
||||
.RetryAsync(2);
|
||||
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS)
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, bool importEpisodes = true)
|
||||
{
|
||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
|
||||
// worse, this 1st dummy call doesn't seem to help:
|
||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||
return policy.ExecuteAsync(() => getItemsAsync(responseGroups));
|
||||
return policy.ExecuteAsync(() => getItemsAsync(responseGroups, importEpisodes));
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions.ResponseGroupOptions responseGroups)
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions.ResponseGroupOptions responseGroups, bool importEpisodes)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
#if DEBUG
|
||||
@@ -130,7 +130,8 @@ namespace InternalUtilities
|
||||
if (!items.Any())
|
||||
items = await Api.GetAllLibraryItemsAsync(responseGroups);
|
||||
|
||||
await manageEpisodesAsync(items);
|
||||
if (importEpisodes)
|
||||
await manageEpisodesAsync(items);
|
||||
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
|
||||
@@ -24,8 +24,8 @@ namespace LibationCli
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations();
|
||||
LibationScaffolding.RunPostMigrationScaffolding();
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate();
|
||||
|
||||
68
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
68
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -33,6 +33,8 @@
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
||||
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||
@@ -67,21 +69,21 @@
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 149);
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
|
||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
|
||||
this.inProgressDescLbl.TabIndex = 15;
|
||||
this.inProgressDescLbl.TabIndex = 18;
|
||||
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 445);
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 17;
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
@@ -90,11 +92,11 @@
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 445);
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 18;
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
@@ -104,6 +106,8 @@
|
||||
this.advancedSettingsGb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.badBookGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||
@@ -112,21 +116,41 @@
|
||||
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Name = "advancedSettingsGb";
|
||||
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 258);
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
|
||||
this.advancedSettingsGb.TabIndex = 6;
|
||||
this.advancedSettingsGb.TabStop = false;
|
||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// importEpisodesCb
|
||||
//
|
||||
this.importEpisodesCb.AutoSize = true;
|
||||
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
|
||||
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||
this.importEpisodesCb.TabIndex = 7;
|
||||
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
this.downloadEpisodesCb.AutoSize = true;
|
||||
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
|
||||
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
this.downloadEpisodesCb.TabIndex = 8;
|
||||
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookGb
|
||||
//
|
||||
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||
this.badBookGb.Location = new System.Drawing.Point(372, 22);
|
||||
this.badBookGb.Location = new System.Drawing.Point(372, 72);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(529, 124);
|
||||
this.badBookGb.TabIndex = 11;
|
||||
this.badBookGb.TabIndex = 13;
|
||||
this.badBookGb.TabStop = false;
|
||||
this.badBookGb.Text = "[bad book desc]";
|
||||
//
|
||||
@@ -136,7 +160,7 @@
|
||||
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||
this.badBookIgnoreRb.TabIndex = 15;
|
||||
this.badBookIgnoreRb.TabIndex = 17;
|
||||
this.badBookIgnoreRb.TabStop = true;
|
||||
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||
@@ -147,7 +171,7 @@
|
||||
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||
this.badBookRetryRb.TabIndex = 14;
|
||||
this.badBookRetryRb.TabIndex = 16;
|
||||
this.badBookRetryRb.TabStop = true;
|
||||
this.badBookRetryRb.Text = "[retry desc]";
|
||||
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||
@@ -158,7 +182,7 @@
|
||||
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||
this.badBookAbortRb.TabIndex = 13;
|
||||
this.badBookAbortRb.TabIndex = 15;
|
||||
this.badBookAbortRb.TabStop = true;
|
||||
this.badBookAbortRb.Text = "[abort desc]";
|
||||
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||
@@ -169,7 +193,7 @@
|
||||
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.badBookAskRb.Name = "badBookAskRb";
|
||||
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||
this.badBookAskRb.TabIndex = 12;
|
||||
this.badBookAskRb.TabIndex = 14;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
@@ -179,10 +203,10 @@
|
||||
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 22);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 7;
|
||||
this.decryptAndConvertGb.TabIndex = 9;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
@@ -194,7 +218,7 @@
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 8;
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
@@ -205,7 +229,7 @@
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 81);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 10;
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -216,7 +240,7 @@
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 56);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 9;
|
||||
this.convertLosslessRb.TabIndex = 11;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
@@ -225,10 +249,10 @@
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 197);
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
|
||||
this.inProgressSelectControl.TabIndex = 16;
|
||||
this.inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
@@ -286,7 +310,7 @@
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 488);
|
||||
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||
this.Controls.Add(this.logsBtn);
|
||||
this.Controls.Add(this.loggingLevelCb);
|
||||
this.Controls.Add(this.loggingLevelLbl);
|
||||
@@ -334,5 +358,7 @@
|
||||
private System.Windows.Forms.RadioButton badBookAbortRb;
|
||||
private System.Windows.Forms.RadioButton badBookAskRb;
|
||||
private System.Windows.Forms.RadioButton badBookIgnoreRb;
|
||||
private System.Windows.Forms.CheckBox downloadEpisodesCb;
|
||||
private System.Windows.Forms.CheckBox importEpisodesCb;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ namespace LibationWinForms.Dialogs
|
||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||
}
|
||||
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||
|
||||
@@ -41,6 +43,8 @@ namespace LibationWinForms.Dialogs
|
||||
"Books");
|
||||
booksSelectControl.SelectDirectory(config.Books);
|
||||
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
convertLosslessRb.Checked = !config.DecryptToLossy;
|
||||
convertLossyRb.Checked = config.DecryptToLossy;
|
||||
@@ -121,6 +125,8 @@ namespace LibationWinForms.Dialogs
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.DecryptToLossy = convertLossyRb.Checked;
|
||||
|
||||
|
||||
@@ -42,10 +42,11 @@ namespace LibationWinForms
|
||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
// do this as soon as possible (post-config)
|
||||
RunInstaller(config);
|
||||
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations();
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
|
||||
|
||||
// migrations which require Forms or are long-running
|
||||
RunWindowsOnlyMigrations(config);
|
||||
@@ -53,9 +54,10 @@ namespace LibationWinForms
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate();
|
||||
checkForUpdate();
|
||||
#endif
|
||||
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -72,8 +74,6 @@ namespace LibationWinForms
|
||||
return;
|
||||
}
|
||||
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding();
|
||||
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
@@ -143,8 +143,6 @@ namespace LibationWinForms
|
||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
|
||||
config.InProgress ??= Configuration.WinTemp;
|
||||
config.AllowLibationFixup = true;
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
@@ -158,6 +156,7 @@ namespace LibationWinForms
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
/// <summary>migrations which require Forms or are long-running</summary>
|
||||
private static void RunWindowsOnlyMigrations(Configuration config)
|
||||
{
|
||||
// only supported in winforms. don't move to app scaffolding
|
||||
@@ -170,9 +169,6 @@ namespace LibationWinForms
|
||||
#region migrate to v5.0.0 re-register device if device info not in settings
|
||||
private static void migrate_to_v5_0_0(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||
return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user