mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 09:58:43 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e89386173 | ||
|
|
7685613e8c | ||
|
|
727d1479bb | ||
|
|
bb46021f20 | ||
|
|
c45e6d526c | ||
|
|
a72c3f069b | ||
|
|
1fcacb9cfb | ||
|
|
a3542c53e2 | ||
|
|
9e44a95ba2 | ||
|
|
204e77008b | ||
|
|
621fb68cd8 | ||
|
|
0c265a9010 | ||
|
|
d4fbb03577 | ||
|
|
69a7ab5b0c | ||
|
|
53a46b5dfc | ||
|
|
fb3126b0c6 | ||
|
|
5c6b5c0af2 | ||
|
|
8de8e50829 | ||
|
|
5d15d6c2c7 | ||
|
|
85c18c8334 | ||
|
|
9de85b649b | ||
|
|
3c1db55a95 | ||
|
|
4e6011711a | ||
|
|
1440b3fcf6 | ||
|
|
f2f0725c68 | ||
|
|
75f1d987fc | ||
|
|
de8589fb84 | ||
|
|
54ceba816a | ||
|
|
05d52e64e5 | ||
|
|
5c6bf300c6 | ||
|
|
10ff95161b | ||
|
|
112671cf9f | ||
|
|
1a37b2346e | ||
|
|
54cceba4e3 | ||
|
|
1502936cd0 | ||
|
|
f06b04ede4 | ||
|
|
406aea6ead | ||
|
|
5f8c40962a | ||
|
|
a77405c632 | ||
|
|
fdff31b69f | ||
|
|
f5e1667368 | ||
|
|
af81367b46 | ||
|
|
cd418e877d | ||
|
|
b6c9a82c68 | ||
|
|
efca1f9c1d | ||
|
|
ca14db79b9 | ||
|
|
9d00da006c | ||
|
|
b479096fc2 | ||
|
|
ad09d36588 | ||
|
|
1a9c0188a4 | ||
|
|
ca75b55da4 | ||
|
|
285b1e7b45 | ||
|
|
6912a499d0 | ||
|
|
4e70365150 | ||
|
|
811a95aedf | ||
|
|
20971124ab | ||
|
|
fa66a361dc | ||
|
|
61d7f5a5cb | ||
|
|
f8c788297e | ||
|
|
79e5545fd3 | ||
|
|
b4def2e2d6 | ||
|
|
281d615649 | ||
|
|
c2c6a31716 | ||
|
|
391f1f387b | ||
|
|
206890b8f3 | ||
|
|
9aa31338d6 | ||
|
|
35fe3ae786 | ||
|
|
b6fe3ae009 | ||
|
|
6ba8c0ca91 | ||
|
|
01de928b7a | ||
|
|
0a54f8d881 | ||
|
|
d31121e307 | ||
|
|
b86bd76726 | ||
|
|
c49edbc77b | ||
|
|
1ba54a74af |
@@ -2,6 +2,7 @@
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.IO;
|
||||
@@ -20,8 +21,8 @@ namespace AaxDecrypter
|
||||
public event EventHandler<int> DecryptProgressUpdate;
|
||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
|
||||
public string OutputFileName { get; private set; }
|
||||
|
||||
private string outputFileName { get; }
|
||||
private string cacheDir { get; }
|
||||
private DownloadLicense downloadLicense { get; }
|
||||
private AaxFile aaxFile;
|
||||
@@ -31,18 +32,19 @@ namespace AaxDecrypter
|
||||
private StepSequence steps { get; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
private bool isCanceled { get; set; }
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(OutputFileName) + ".json");
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
|
||||
|
||||
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||
OutputFileName = outFileName;
|
||||
var outDir = Path.GetDirectoryName(OutputFileName);
|
||||
outputFileName = outFileName;
|
||||
|
||||
var outDir = Path.GetDirectoryName(outputFileName);
|
||||
if (!Directory.Exists(outDir))
|
||||
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
|
||||
if (File.Exists(OutputFileName))
|
||||
File.Delete(OutputFileName);
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
|
||||
@@ -123,10 +125,12 @@ namespace AaxDecrypter
|
||||
}
|
||||
private NetworkFileStreamPersister NewNetworkFilePersister()
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection();
|
||||
headers.Add("User-Agent", downloadLicense.UserAgent);
|
||||
var headers = new System.Net.WebHeaderCollection
|
||||
{
|
||||
{ "User-Agent", downloadLicense.UserAgent }
|
||||
};
|
||||
|
||||
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
|
||||
@@ -135,10 +139,10 @@ namespace AaxDecrypter
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
if (File.Exists(OutputFileName))
|
||||
FileExt.SafeDelete(OutputFileName);
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
|
||||
FileStream outFile = File.OpenWrite(OutputFileName);
|
||||
FileStream outFile = File.OpenWrite(outputFileName);
|
||||
|
||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
||||
|
||||
@@ -157,7 +161,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
//This handles a special case where the aaxc file doesn't contain cover art and
|
||||
//Libation downloaded it instead (Animal Farm). Currently only works for Mp4a files.
|
||||
using var decryptedBook = new Mp4File(OutputFileName, FileAccess.ReadWrite);
|
||||
using var decryptedBook = new Mp4File(outputFileName, FileAccess.ReadWrite);
|
||||
decryptedBook.AppleTags?.SetCoverArt(coverArt);
|
||||
decryptedBook.Save();
|
||||
decryptedBook.Close();
|
||||
@@ -189,7 +193,7 @@ namespace AaxDecrypter
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(OutputFileName), downloadLicense.ChapterInfo));
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -203,7 +207,7 @@ namespace AaxDecrypter
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace AaxDecrypter
|
||||
_networkStream = response.GetResponseStream();
|
||||
|
||||
//Download the file in the background.
|
||||
Thread downloadThread = new Thread(() => DownloadFile());
|
||||
Thread downloadThread = new Thread(() => DownloadFile()) { IsBackground = true };
|
||||
downloadThread.Start();
|
||||
|
||||
hasBegunDownloading = true;
|
||||
|
||||
@@ -11,8 +11,16 @@ using Serilog;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
// subtly different from DataLayer.LiberatedStatus
|
||||
// - DataLayer.LiberatedStatus: has no concept of partially downloaded
|
||||
// - ApplicationServices.LiberatedState: has no concept of Error/skipped
|
||||
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
|
||||
|
||||
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
|
||||
|
||||
public static class LibraryCommands
|
||||
{
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
|
||||
{
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
@@ -41,7 +49,8 @@ namespace ApplicationServices
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new {
|
||||
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new
|
||||
{
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
@@ -99,13 +108,23 @@ namespace ApplicationServices
|
||||
|
||||
return newCount;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static int UpdateTags(this LibationContext context, Book book, string newTags)
|
||||
#region Update book details
|
||||
public static int UpdateTags(Book book, string newTags)
|
||||
{
|
||||
try
|
||||
{
|
||||
book.UserDefinedItem.Tags = newTags;
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = book.UserDefinedItem;
|
||||
|
||||
if (udi.Tags == newTags)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.Tags = newTags;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
if (qtyChanges > 0)
|
||||
@@ -119,5 +138,99 @@ namespace ApplicationServices
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus, string finalAudioPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = libraryBook.Book.UserDefinedItem;
|
||||
|
||||
if (udi.BookStatus == liberatedStatus && udi.BookLocation == finalAudioPath)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.BookStatus = liberatedStatus;
|
||||
udi.BookLocation = finalAudioPath;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error updating tags");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = libraryBook.Book.UserDefinedItem;
|
||||
|
||||
if (udi.PdfStatus == liberatedStatus)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.PdfStatus = liberatedStatus;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error updating tags");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public static LiberatedState Liberated_Status(Book book)
|
||||
=> TransitionalFileLocator.Audio_Exists(book) ? LiberatedState.Liberated
|
||||
: TransitionalFileLocator.AAXC_Exists(book) ? LiberatedState.PartialDownload
|
||||
: LiberatedState.NotDownloaded;
|
||||
|
||||
public static PdfState Pdf_Status(Book book)
|
||||
=> !book.Supplements.Any() ? PdfState.NoPdf
|
||||
: TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded
|
||||
: PdfState.NotDownloaded;
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
|
||||
public static LibraryStats GetCounts()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
|
||||
|
||||
var results = libraryBooks
|
||||
.AsParallel()
|
||||
.Select(lb => Liberated_Status(lb.Book))
|
||||
.ToList();
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedState.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedState.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedState.NotDownloaded);
|
||||
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
|
||||
|
||||
var boolResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.Supplements.Any())
|
||||
.Select(lb => Pdf_Status(lb.Book))
|
||||
.ToList();
|
||||
var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded);
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, pdfsDownloaded, pdfsNotDownloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,15 @@ namespace ApplicationServices
|
||||
|
||||
[Name("My Libation Tags")]
|
||||
public string MyLibationTags { get; set; }
|
||||
|
||||
[Name("Book Liberation Status")]
|
||||
public string BookStatus { get; set; }
|
||||
|
||||
[Name("Book File Location")]
|
||||
public string BookLocation { get; set; }
|
||||
|
||||
[Name("PDF Liberation Status")]
|
||||
public string PdfStatus { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
@@ -116,7 +125,10 @@ namespace ApplicationServices
|
||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
BookLocation = a.Book.UserDefinedItem.BookLocation,
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
@@ -187,7 +199,10 @@ namespace ApplicationServices
|
||||
nameof (ExportDto.MyRatingOverall),
|
||||
nameof (ExportDto.MyRatingPerformance),
|
||||
nameof (ExportDto.MyRatingStory),
|
||||
nameof (ExportDto.MyLibationTags)
|
||||
nameof (ExportDto.MyLibationTags),
|
||||
nameof (ExportDto.BookStatus),
|
||||
nameof (ExportDto.BookLocation),
|
||||
nameof (ExportDto.PdfStatus)
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
@@ -249,6 +264,9 @@ namespace ApplicationServices
|
||||
col = createCell(row, col, dto.MyRatingStory);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
|
||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.BookLocation);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace ApplicationServices
|
||||
{
|
||||
public static class SearchEngineCommands
|
||||
{
|
||||
public static void FullReIndex()
|
||||
public static void FullReIndex(SearchEngine engine = null)
|
||||
{
|
||||
var engine = new SearchEngine(DbContexts.GetContext());
|
||||
engine.CreateNewIndex();
|
||||
engine ??= new SearchEngine();
|
||||
engine.CreateNewIndex(DbContexts.GetContext());
|
||||
}
|
||||
|
||||
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
|
||||
@@ -21,34 +21,34 @@ namespace ApplicationServices
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
|
||||
);
|
||||
|
||||
public static void UpdateIsLiberated(Book book) => performSearchEngineAction_safe(e =>
|
||||
e.UpdateIsLiberated(book.AudibleProductId)
|
||||
public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
|
||||
e.UpdateLiberatedStatus(book)
|
||||
);
|
||||
|
||||
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
|
||||
{
|
||||
var engine = new SearchEngine(DbContexts.GetContext());
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
action(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
FullReIndex(engine);
|
||||
action(engine);
|
||||
}
|
||||
}
|
||||
|
||||
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> action)
|
||||
{
|
||||
var engine = new SearchEngine(DbContexts.GetContext());
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
return action(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
FullReIndex(engine);
|
||||
return action(engine);
|
||||
}
|
||||
}
|
||||
|
||||
46
ApplicationServices/TransitionalFileLocator.cs
Normal file
46
ApplicationServices/TransitionalFileLocator.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class TransitionalFileLocator
|
||||
{
|
||||
public static string Audio_GetPath(Book book)
|
||||
{
|
||||
var loc = book?.UserDefinedItem?.BookLocation ?? "";
|
||||
if (File.Exists(loc))
|
||||
return loc;
|
||||
|
||||
return AudibleFileStorage.Audio.GetPath(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool PDF_Exists(Book book)
|
||||
{
|
||||
var status = book?.UserDefinedItem?.PdfStatus;
|
||||
if (status.HasValue && status.Value == LiberatedStatus.Liberated)
|
||||
return true;
|
||||
|
||||
return AudibleFileStorage.PDF.Exists(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool Audio_Exists(Book book)
|
||||
{
|
||||
var status = book?.UserDefinedItem?.BookStatus;
|
||||
// true since Error == libhack
|
||||
if (status.HasValue && status.Value != LiberatedStatus.NotLiberated)
|
||||
return true;
|
||||
|
||||
return AudibleFileStorage.Audio.Exists(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool AAXC_Exists(Book book)
|
||||
{
|
||||
// this one will actually stay the same. centralizing helps with organization in the interim though
|
||||
return AudibleFileStorage.AAXC.Exists(book.AudibleProductId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,8 @@ namespace DataLayer
|
||||
Category = category;
|
||||
|
||||
// simple assigns
|
||||
Title = title;
|
||||
Description = description;
|
||||
Title = title.Trim();
|
||||
Description = description.Trim();
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
|
||||
// assigns with biz logic
|
||||
@@ -220,8 +220,11 @@ namespace DataLayer
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
|
||||
if (!_supplements.Any(s => url.EqualsInsensitive(url)))
|
||||
_supplements.Add(new Supplement(this, url));
|
||||
if (_supplements.Any(s => url.EqualsInsensitive(url)))
|
||||
return;
|
||||
|
||||
_supplements.Add(new Supplement(this, url));
|
||||
UserDefinedItem.PdfStatus ??= LiberatedStatus.NotLiberated;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||
|
||||
public class BookContributor
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace DataLayer
|
||||
{
|
||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||
}
|
||||
@@ -6,6 +6,17 @@ using Dinah.Core;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not track in-process state. In-process state is determined by the presence of temp file.
|
||||
/// </summary>
|
||||
public enum LiberatedStatus
|
||||
{
|
||||
NotLiberated = 0,
|
||||
Liberated = 1,
|
||||
/// <summary>Error occurred during liberation. Don't retry</summary>
|
||||
Error = 2
|
||||
}
|
||||
|
||||
public class UserDefinedItem
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
@@ -22,6 +33,7 @@ namespace DataLayer
|
||||
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
#region Tags
|
||||
private string _tags = "";
|
||||
public string Tags
|
||||
{
|
||||
@@ -71,14 +83,23 @@ namespace DataLayer
|
||||
return string.Join(" ", unique);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Rating
|
||||
// owned: not an optional one-to-one
|
||||
/// <summary>The user's individual book rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
#region LiberatedStatuses and book file location
|
||||
public LiberatedStatus BookStatus { get; set; }
|
||||
public string BookLocation { get; set; }
|
||||
public LiberatedStatus? PdfStatus { get; set; }
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
}
|
||||
}
|
||||
|
||||
390
DataLayer/Migrations/20210727180408_AddLiberatedStatus.Designer.cs
generated
Normal file
390
DataLayer/Migrations/20210727180408_AddLiberatedStatus.Designer.cs
generated
Normal file
@@ -0,0 +1,390 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210727180408_AddLiberatedStatus")]
|
||||
partial class AddLiberatedStatus
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("BookLocation")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
44
DataLayer/Migrations/20210727180408_AddLiberatedStatus.cs
Normal file
44
DataLayer/Migrations/20210727180408_AddLiberatedStatus.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class AddLiberatedStatus : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookStatus",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PdfStatus",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +253,15 @@ namespace DataLayer.Migrations
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("BookLocation")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
// only library importing should directly query Book. All else should use LibraryBook
|
||||
public static class BookQueries
|
||||
{
|
||||
public static Book GetBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.Books
|
||||
.AsNoTracking()
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetBook(productId);
|
||||
|
||||
public static Book GetBook(this IQueryable<Book> books, string productId)
|
||||
@@ -25,6 +27,7 @@ namespace DataLayer
|
||||
.GetBooks()
|
||||
.Where(predicate);
|
||||
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<Book> GetBooks(this IQueryable<Book> books)
|
||||
=> books
|
||||
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
|
||||
|
||||
@@ -4,27 +4,35 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
// only library importing should directly query Book. All else should use LibraryBook
|
||||
public static class LibraryQueries
|
||||
{
|
||||
public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
||||
=> context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
//// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid
|
||||
//public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
||||
// => context
|
||||
// .Library
|
||||
// .GetLibrary()
|
||||
// .ToList();
|
||||
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
|
||||
=> context
|
||||
.Library
|
||||
.AsNoTracking()
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
|
||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.Library
|
||||
.AsNoTracking()
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetLibraryBook(productId);
|
||||
|
||||
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
|
||||
=> library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)
|
||||
=> library
|
||||
@@ -32,10 +40,5 @@ namespace DataLayer
|
||||
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
|
||||
|
||||
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
|
||||
=> library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(le => le.Book.AudibleProductId == productId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,6 @@ namespace DtoImporterService
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
//Add any subtitle after the title title.
|
||||
var title = item.Title + (!string.IsNullOrWhiteSpace(item.Subtitle) ? $": {item.Subtitle}" : "");
|
||||
|
||||
// absence of authors is very rare, but possible
|
||||
if (!item.Authors?.Any() ?? true)
|
||||
item.Authors = new[] { new Person { Name = "", Asin = null } };
|
||||
@@ -105,7 +102,7 @@ namespace DtoImporterService
|
||||
|
||||
var book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
title,
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
authors,
|
||||
|
||||
@@ -71,7 +71,8 @@ namespace DtoImporterService
|
||||
|
||||
foreach (var p in people)
|
||||
{
|
||||
var person = DbContext.Contributors.Local.SingleOrDefault(c => c.Name == p.Name);
|
||||
// Should be 'Single' not 'First'. A user had a duplicate get in somehow though so I'm now using 'First' defensively
|
||||
var person = DbContext.Contributors.Local.FirstOrDefault(c => c.Name == p.Name);
|
||||
if (person == null)
|
||||
{
|
||||
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
|
||||
|
||||
@@ -20,11 +20,11 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
|
||||
public DownloadDecryptBook DownloadDecryptBook { get; } = new DownloadDecryptBook();
|
||||
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
@@ -35,7 +35,7 @@ namespace FileLiberator
|
||||
try
|
||||
{
|
||||
{
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
var statusHandler = await DownloadDecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
98
FileLiberator/ConvertToMp3.cs
Normal file
98
FileLiberator/ConvertToMp3.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using AAXClean;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.IO;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class ConvertToMp3 : IDecryptable
|
||||
{
|
||||
public event EventHandler<string> DecryptBegin;
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
public event EventHandler<int> UpdateProgress;
|
||||
public event EventHandler<TimeSpan> UpdateRemainingTime;
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
|
||||
private Mp4File m4bBook;
|
||||
|
||||
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
||||
|
||||
public void Cancel() => m4bBook?.Cancel();
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
{
|
||||
var path = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
|
||||
}
|
||||
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
DecryptBegin?.Invoke(this, $"Begin converting {libraryBook} to mp3");
|
||||
|
||||
try
|
||||
{
|
||||
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
|
||||
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||
|
||||
TitleDiscovered?.Invoke(this, m4bBook.AppleTags.Title);
|
||||
AuthorsDiscovered?.Invoke(this, m4bBook.AppleTags.FirstAuthor);
|
||||
NarratorsDiscovered?.Invoke(this, m4bBook.AppleTags.Narrator);
|
||||
CoverImageFilepathDiscovered?.Invoke(this, m4bBook.AppleTags.Cover);
|
||||
|
||||
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
||||
|
||||
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File));
|
||||
m4bBook.InputStream.Close();
|
||||
mp3File.Close();
|
||||
|
||||
var mp3Path = Mp3FileName(m4bPath);
|
||||
|
||||
FileExt.SafeMove(mp3File.Name, mp3Path);
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
|
||||
if (result == ConversionResult.Failed)
|
||||
statusHandler.AddError("Conversion failed");
|
||||
|
||||
return statusHandler;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DecryptCompleted?.Invoke(this, $"Completed converting to mp3: {libraryBook.Book.Title}");
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var duration = m4bBook.Duration;
|
||||
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
UpdateRemainingTime?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
|
||||
UpdateProgress?.Invoke(this, (int)progressPercent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -35,7 +34,7 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
@@ -47,10 +46,13 @@ namespace FileLiberator
|
||||
// moves files and returns dest dir
|
||||
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
|
||||
if (!finalAudioExists)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
// only need to update if success. if failure, it will remain at 0 == NotLiberated
|
||||
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated, outputAudioFilename);
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
@@ -73,10 +75,10 @@ namespace FileLiberator
|
||||
|
||||
var aaxcDecryptDlLic = new DownloadLicense
|
||||
(
|
||||
contentLic.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic.Voucher?.Key,
|
||||
contentLic.Voucher?.Iv,
|
||||
Resources.UserAgent
|
||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic?.Voucher?.Key,
|
||||
contentLic?.Voucher?.Iv,
|
||||
"Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0"
|
||||
);
|
||||
|
||||
if (Configuration.Instance.AllowLibationFixup)
|
||||
@@ -97,10 +99,10 @@ namespace FileLiberator
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
var proposedOutputFile = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{extension}");
|
||||
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{extension}");
|
||||
|
||||
|
||||
aaxcDownloader = new AaxcDownloadConverter(proposedOutputFile, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" };
|
||||
aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" };
|
||||
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining);
|
||||
aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
@@ -113,7 +115,7 @@ namespace FileLiberator
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
return aaxcDownloader.OutputFileName;
|
||||
return outFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -170,6 +172,8 @@ namespace FileLiberator
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
@@ -215,7 +219,7 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
// frustratingly copy pasta from DownloadableBase and DownloadPdf
|
||||
// currently only used to download the .zip flies for upgrade
|
||||
public class DownloadFile : IDownloadable
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,24 +15,24 @@ namespace FileLiberator
|
||||
{
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId);
|
||||
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book);
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
var result = verifyDownload(libraryBook);
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var existingPath = Path.GetDirectoryName(ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
|
||||
if (existingPath != null)
|
||||
@@ -44,6 +46,9 @@ namespace FileLiberator
|
||||
return full;
|
||||
}
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
|
||||
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
@@ -55,7 +60,9 @@ namespace FileLiberator
|
||||
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
|
||||
}
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
@@ -22,16 +23,6 @@ namespace FileLiberator
|
||||
.GetLibrary_Flat_NoTracking()
|
||||
.Where(libraryBook => processable.Validate(libraryBook));
|
||||
|
||||
public static LibraryBook GetSingleLibraryBook(string productId)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBook = context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
return libraryBook;
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
if (!processable.Validate(libraryBook))
|
||||
|
||||
@@ -8,19 +8,11 @@ using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
// could add images here, but for now images are stored in a well-known location
|
||||
public enum FileType { Unknown, Audio, AAXC, PDF }
|
||||
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
public abstract string[] Extensions { get; }
|
||||
protected abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
#region static
|
||||
@@ -41,6 +33,8 @@ namespace FileManager
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
private static BackgroundFileSystem BookDirectoryFiles { get; } = new BackgroundFileSystem();
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
@@ -53,7 +47,12 @@ namespace FileManager
|
||||
{
|
||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
BookDirectoryFiles.RefreshFiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example for full books:
|
||||
@@ -63,16 +62,31 @@ namespace FileManager
|
||||
/// </summary>
|
||||
public bool Exists(string productId) => GetPath(productId) != null;
|
||||
|
||||
public string GetPath(string productId)
|
||||
public string GetPath(string productId)
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
var firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
|
||||
string storageDir = StorageDirectory;
|
||||
string regexPattern = $@"{productId}.*?\.({extAggr})$";
|
||||
string firstOrNull;
|
||||
|
||||
if (storageDir == BooksDirectory)
|
||||
{
|
||||
//If user changed the BooksDirectory, reinitialize.
|
||||
if (storageDir != BookDirectoryFiles.RootDirectory)
|
||||
BookDirectoryFiles.Init(storageDir, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
|
||||
}
|
||||
|
||||
if (firstOrNull is null)
|
||||
return null;
|
||||
@@ -102,7 +116,7 @@ namespace FileManager
|
||||
{
|
||||
public const string SKIP_FILE_EXT = "libhack";
|
||||
|
||||
public override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
@@ -125,7 +139,7 @@ namespace FileManager
|
||||
|
||||
public class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "aaxc" };
|
||||
protected override string[] Extensions { get; } = new[] { "aaxc" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
@@ -137,7 +151,7 @@ namespace FileManager
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
protected override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
|
||||
142
FileManager/BackgroundFileSystem.cs
Normal file
142
FileManager/BackgroundFileSystem.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
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
|
||||
{
|
||||
class BackgroundFileSystem
|
||||
{
|
||||
public string RootDirectory { get; private set; }
|
||||
public string SearchPattern { get; private set; }
|
||||
public SearchOption SearchOption { get; private set; }
|
||||
|
||||
private FileSystemWatcher fileSystemWatcher { get; set; }
|
||||
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
||||
private Task backgroundScanner { get; set; }
|
||||
private List<string> fsCache { get; set; }
|
||||
|
||||
public string FindFile(string regexPattern, RegexOptions options)
|
||||
{
|
||||
if (fsCache is null) return null;
|
||||
|
||||
lock (fsCache)
|
||||
{
|
||||
return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options));
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshFiles()
|
||||
{
|
||||
if (fsCache is null) return;
|
||||
|
||||
lock (fsCache)
|
||||
{
|
||||
fsCache.Clear();
|
||||
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(string rootDirectory, string searchPattern, SearchOption searchOptions)
|
||||
{
|
||||
RootDirectory = rootDirectory;
|
||||
SearchPattern = searchPattern;
|
||||
SearchOption = searchOptions;
|
||||
|
||||
//Calling CompleteAdding() will cause background scanner to terminate.
|
||||
directoryChangesEvents?.CompleteAdding();
|
||||
fsCache?.Clear();
|
||||
directoryChangesEvents?.Dispose();
|
||||
fileSystemWatcher?.Dispose();
|
||||
|
||||
fsCache = Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption).ToList();
|
||||
|
||||
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory);
|
||||
fileSystemWatcher.Created += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
|
||||
fileSystemWatcher.Error += FileSystemWatcher_Error;
|
||||
fileSystemWatcher.IncludeSubdirectories = true;
|
||||
fileSystemWatcher.EnableRaisingEvents = true;
|
||||
|
||||
//Wait for background scanner to terminate before reinitializing.
|
||||
backgroundScanner?.Wait();
|
||||
backgroundScanner = new Task(BackgroundScanner);
|
||||
backgroundScanner.Start();
|
||||
}
|
||||
|
||||
private void AddUniqueFiles(IEnumerable<string> newFiles)
|
||||
{
|
||||
foreach (var file in newFiles)
|
||||
{
|
||||
AddUniqueFile(file);
|
||||
}
|
||||
}
|
||||
private void AddUniqueFile(string newFile)
|
||||
{
|
||||
if (!fsCache.Contains(newFile))
|
||||
fsCache.Add(newFile);
|
||||
}
|
||||
|
||||
private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
|
||||
{
|
||||
Init(RootDirectory, SearchPattern, SearchOption);
|
||||
}
|
||||
|
||||
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
directoryChangesEvents.Add(e);
|
||||
}
|
||||
|
||||
#region Background Thread
|
||||
private void BackgroundScanner()
|
||||
{
|
||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
||||
UpdateLocalCache(change);
|
||||
}
|
||||
|
||||
private void UpdateLocalCache(FileSystemEventArgs change)
|
||||
{
|
||||
lock (fsCache)
|
||||
{
|
||||
if (change.ChangeType == WatcherChangeTypes.Deleted)
|
||||
{
|
||||
RemovePath(change.FullPath);
|
||||
}
|
||||
else if (change.ChangeType == WatcherChangeTypes.Created)
|
||||
{
|
||||
AddPath(change.FullPath);
|
||||
}
|
||||
else if (change.ChangeType == WatcherChangeTypes.Renamed)
|
||||
{
|
||||
var renameChange = change as RenamedEventArgs;
|
||||
|
||||
RemovePath(renameChange.OldFullPath);
|
||||
AddPath(renameChange.FullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemovePath(string path)
|
||||
{
|
||||
var pathsToRemove = fsCache.Where(p => p.StartsWith(path)).ToArray();
|
||||
|
||||
foreach (var p in pathsToRemove)
|
||||
fsCache.Remove(p);
|
||||
}
|
||||
|
||||
private void AddPath(string path)
|
||||
{
|
||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||
AddUniqueFiles(Directory.EnumerateFiles(path, SearchPattern, SearchOption));
|
||||
else
|
||||
AddUniqueFile(path);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@ using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
@@ -34,28 +38,6 @@ namespace FileManager
|
||||
|
||||
#region persistent configuration settings/values
|
||||
|
||||
#region // properties to test reflection
|
||||
/*
|
||||
// field should NOT be populated
|
||||
public string TestField;
|
||||
// int should NOT be populated
|
||||
public int TestInt { get; set; }
|
||||
// read-only should NOT be populated
|
||||
public string TestGet { get; } // get only: should NOT get auto-populated
|
||||
// set-only should NOT be populated
|
||||
public string TestSet { private get; set; }
|
||||
|
||||
// get and set: SHOULD be auto-populated
|
||||
public string TestGetSet { get; set; }
|
||||
*/
|
||||
#endregion
|
||||
|
||||
// settings will be persisted when all are true
|
||||
// - property (not field)
|
||||
// - string
|
||||
// - public getter
|
||||
// - public setter
|
||||
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
@@ -65,8 +47,12 @@ namespace FileManager
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.Set(propertyName, newValue);
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue) => persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
/// <returns>Value was changed</returns>
|
||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
=> persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
@@ -83,11 +69,43 @@ namespace FileManager
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
#region MainForm: X, Y, Width, Height, MainFormIsMaximized
|
||||
public int MainFormX
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormX));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormX), value);
|
||||
}
|
||||
|
||||
public int MainFormY
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormY));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormY), value);
|
||||
}
|
||||
|
||||
public int MainFormWidth
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormWidth));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormWidth), value);
|
||||
}
|
||||
|
||||
public int MainFormHeight
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormHeight));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormHeight), value);
|
||||
}
|
||||
|
||||
public bool MainFormIsMaximized
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MainFormIsMaximized));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormIsMaximized), value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(Books));
|
||||
set => persistentDictionary.Set(nameof(Books), value);
|
||||
set => persistentDictionary.SetString(nameof(Books), value);
|
||||
}
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
@@ -95,21 +113,21 @@ namespace FileManager
|
||||
public string InProgress
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(InProgress));
|
||||
set => persistentDictionary.Set(nameof(InProgress), value);
|
||||
set => persistentDictionary.SetString(nameof(InProgress), value);
|
||||
}
|
||||
|
||||
[Description("Allow Libation for fix up audiobook metadata?")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.Set(nameof(AllowLibationFixup), value);
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.Set(nameof(DecryptToLossy), value);
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -167,6 +185,63 @@ namespace FileManager
|
||||
// since it's a list, order matters and non-LibationFiles will be returned first
|
||||
var dirFunc = directoryOptionsPaths.FirstOrDefault(dirFunc => dirFunc.getPathFunc() == directory);
|
||||
return dirFunc == default ? KnownDirectories.None : dirFunc.directory;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region logging
|
||||
private IConfigurationRoot configuration;
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
//// with code. also persists to Settings.json
|
||||
//SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
|
||||
//// hack which achieves the same, in memory only
|
||||
//configuration["Serilog:WriteTo:1:Args:path"] = logPath;
|
||||
|
||||
configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
[Description("The importance of a log event")]
|
||||
public LogEventLevel LogLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
var logLevelEnum = Enum<LogEventLevel>.Parse(logLevelStr);
|
||||
return logLevelEnum;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LogEventLevel.Information;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
Log.Logger.Information("LogLevel.set attempt. No change");
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.Reload();
|
||||
|
||||
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
|
||||
{
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled()
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -188,12 +263,19 @@ namespace FileManager
|
||||
if (libationFilesPathCache is not null)
|
||||
return libationFilesPathCache;
|
||||
|
||||
// must write here before SettingsFilePath in next step reads cache
|
||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||
libationFilesPathCache = getLiberationFilesSettingFromJson();
|
||||
|
||||
// load json values into memory. create settings if not exists
|
||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
// Config init in Program.ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||
bool settingWasChanged = SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
|
||||
return libationFilesPathCache;
|
||||
}
|
||||
}
|
||||
@@ -235,34 +317,27 @@ namespace FileManager
|
||||
return valueFinal;
|
||||
}
|
||||
|
||||
public bool TrySetLibationFiles(string directory)
|
||||
public void SetLibationFiles(string directory)
|
||||
{
|
||||
// this is WRONG. need to MOVE settings; not DELETE them
|
||||
|
||||
//// if moving from default, delete old settings file and dir (if empty)
|
||||
//if (LibationFiles.EqualsInsensitive(AppDir))
|
||||
//{
|
||||
// File.Delete(SettingsFilePath);
|
||||
// System.Threading.Thread.Sleep(100);
|
||||
// if (!Directory.EnumerateDirectories(AppDir).Any() && !Directory.EnumerateFiles(AppDir).Any())
|
||||
// Directory.Delete(AppDir);
|
||||
//}
|
||||
|
||||
|
||||
libationFilesPathCache = null;
|
||||
|
||||
|
||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = directory;
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents != endingContents)
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
|
||||
return true;
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -16,16 +16,16 @@ namespace FileManager
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
|
||||
static FilePathCache()
|
||||
{
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (File.Exists(JsonFile))
|
||||
if (File.Exists(jsonFile))
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ namespace FileManager
|
||||
private static void save()
|
||||
{
|
||||
// create json if not exists
|
||||
static void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
|
||||
static void resave() => File.WriteAllText(jsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace FileManager
|
||||
return stringCache[propertyName];
|
||||
}
|
||||
|
||||
public T Get<T>(string propertyName)
|
||||
public T GetNonString<T>(string propertyName)
|
||||
{
|
||||
var obj = GetObject(propertyName);
|
||||
if (obj is null) return default;
|
||||
@@ -68,10 +68,32 @@ namespace FileManager
|
||||
return objectCache[propertyName];
|
||||
}
|
||||
|
||||
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
|
||||
public string GetStringFromJsonPath(string jsonPath)
|
||||
{
|
||||
if (!stringCache.ContainsKey(jsonPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var jObject = readFile();
|
||||
var token = jObject.SelectToken(jsonPath);
|
||||
if (token is null)
|
||||
return null;
|
||||
stringCache[jsonPath] = (string)token;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return stringCache[jsonPath];
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
|
||||
|
||||
private object locker { get; } = new object();
|
||||
public void Set(string propertyName, string newValue)
|
||||
public void SetString(string propertyName, string newValue)
|
||||
{
|
||||
// only do this check in string cache, NOT object cache
|
||||
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
|
||||
@@ -83,7 +105,7 @@ namespace FileManager
|
||||
writeFile(propertyName, newValue);
|
||||
}
|
||||
|
||||
public void Set(string propertyName, object newValue)
|
||||
public void SetNonString(string propertyName, object newValue)
|
||||
{
|
||||
// set cache
|
||||
objectCache[propertyName] = newValue;
|
||||
@@ -97,19 +119,6 @@ namespace FileManager
|
||||
if (IsReadOnly)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var str = newValue?.ToString();
|
||||
var formattedValue
|
||||
= str is null ? "[null]"
|
||||
: string.IsNullOrEmpty(str) ? "[empty]"
|
||||
: string.IsNullOrWhiteSpace(str) ? $"[whitespace. Length={str.Length}]"
|
||||
: str.Length > 100 ? $"[Length={str.Length}] {str[0..50]}...{str[^50..^0]}"
|
||||
: str;
|
||||
Serilog.Log.Logger.Information($"Config changed. {propertyName}={formattedValue}");
|
||||
}
|
||||
catch { }
|
||||
|
||||
// write new setting to file
|
||||
lock (locker)
|
||||
{
|
||||
@@ -119,31 +128,80 @@ namespace FileManager
|
||||
jObject[propertyName] = newValue;
|
||||
var endContents = JsonConvert.SerializeObject(jObject, Formatting.Indented);
|
||||
|
||||
if (startContents != endContents)
|
||||
File.WriteAllText(Filepath, endContents);
|
||||
if (startContents == endContents)
|
||||
return;
|
||||
|
||||
File.WriteAllText(Filepath, endContents);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var str = formatValueForLog(newValue?.ToString());
|
||||
Serilog.Log.Logger.Information("Config changed. {@DebugInfo}", new { propertyName, newValue = str });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// special case: no caching. no logging
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue)
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
/// <returns>Value was changed</returns>
|
||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
if (IsReadOnly)
|
||||
return;
|
||||
return false;
|
||||
|
||||
var path = $"{jsonPath}.{propertyName}";
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
var jObject = readFile();
|
||||
var token = jObject.SelectToken(jsonPath);
|
||||
var oldValue = (string)token[propertyName];
|
||||
// only do this check in string cache, NOT object cache
|
||||
if (stringCache.ContainsKey(path) && stringCache[path] == newValue)
|
||||
return false;
|
||||
|
||||
if (oldValue != newValue)
|
||||
// set cache
|
||||
stringCache[path] = newValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var jObject = readFile();
|
||||
var token = jObject.SelectToken(jsonPath);
|
||||
if (token is null || token[propertyName] is null)
|
||||
return false;
|
||||
|
||||
var oldValue = token.Value<string>(propertyName);
|
||||
if (oldValue == newValue)
|
||||
return false;
|
||||
|
||||
token[propertyName] = newValue;
|
||||
File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
catch (Exception exDebug)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!suppressLogging)
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = formatValueForLog(newValue?.ToString());
|
||||
Serilog.Log.Logger.Information("Config changed. {@DebugInfo}", new { jsonPath, propertyName, newValue = str });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string formatValueForLog(string value)
|
||||
=> value is null ? "[null]"
|
||||
: string.IsNullOrEmpty(value) ? "[empty]"
|
||||
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
|
||||
: value.Length > 100 ? $"[Length={value.Length}] {value[0..50]}...{value[^50..^0]}"
|
||||
: value;
|
||||
|
||||
private JObject readFile()
|
||||
{
|
||||
var settingsJsonContents = File.ReadAllText(Filepath);
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace FileManager
|
||||
{
|
||||
ensureCache();
|
||||
|
||||
if (!tagsCollection.Any())
|
||||
return;
|
||||
|
||||
// on initial reload, there's a huge benefit to adding to cache individually then updating the file only once
|
||||
foreach ((string productId, string tags) in tagsCollection)
|
||||
cache[productId] = tags;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>5.2.0.1</Version>
|
||||
<Version>5.4.9.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
@@ -18,9 +21,16 @@ namespace LibationLauncher
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
static extern bool AllocConsole();
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
//// uncomment to see Console. MUST be called before anything writes to Console. Might only work from VS
|
||||
//AllocConsole();
|
||||
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
@@ -44,11 +54,12 @@ namespace LibationLauncher
|
||||
|
||||
migrate_to_v5_0_0(config);
|
||||
migrate_to_v5_2_0__post_config(config);
|
||||
//migrate_to_v5_4_1(config);// comment out until after vacation
|
||||
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
checkForUpdate(config);
|
||||
logStartupState(config);
|
||||
checkForUpdate(config);
|
||||
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
@@ -74,7 +85,7 @@ namespace LibationLauncher
|
||||
// check for existing settigns in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.TrySetLibationFiles(defaultLibationFilesDir);
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
@@ -87,7 +98,7 @@ namespace LibationLauncher
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
config.TrySetLibationFiles(defaultLibationFilesDir);
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@@ -98,7 +109,7 @@ namespace LibationLauncher
|
||||
return;
|
||||
}
|
||||
|
||||
config.TrySetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
@@ -134,7 +145,7 @@ namespace LibationLauncher
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
#region migrate_to_v5_0_0 re-register device if device info not in settings
|
||||
#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)))
|
||||
@@ -222,6 +233,72 @@ namespace LibationLauncher
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate to v5.4.1 see comment
|
||||
// this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually
|
||||
// implement the portion which removes FilePaths.json, at which time this method will be a proper migration
|
||||
//
|
||||
// I'm iterating through safe steps toward getting rid of the live scanner except to track audiobook files as a convenience
|
||||
// such as clicking the stop light to open its location. live scanning will be replaced with state tracking in the database.
|
||||
|
||||
// FilePaths.json => db. long running. fire and forget
|
||||
private static void migrate_to_v5_4_1(Configuration config)
|
||||
=> new System.Threading.Thread(() => migrate_to_v5_4_1_thread(config)) { IsBackground = true }.Start();
|
||||
private static void migrate_to_v5_4_1_thread(Configuration config)
|
||||
{
|
||||
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
|
||||
if (!File.Exists(filePaths))
|
||||
return;
|
||||
|
||||
using var context = ApplicationServices.DbContexts.GetContext();
|
||||
context.Books.Load();
|
||||
|
||||
var jArr = JArray.Parse(File.ReadAllText(filePaths));
|
||||
|
||||
foreach (var jToken in jArr)
|
||||
{
|
||||
var asinToken = jToken["Id"];
|
||||
var fileTypeToken = jToken["FileType"];
|
||||
var pathToken = jToken["Path"];
|
||||
if (asinToken is null || fileTypeToken is null || pathToken is null ||
|
||||
asinToken.Type != JTokenType.String || fileTypeToken.Type != JTokenType.Integer || pathToken.Type != JTokenType.String)
|
||||
continue;
|
||||
|
||||
var asin = asinToken.Value<string>();
|
||||
var fileType = (FileType)fileTypeToken.Value<int>();
|
||||
var path = pathToken.Value<string>();
|
||||
|
||||
if (fileType == FileType.Unknown || fileType == FileType.AAXC)
|
||||
continue;
|
||||
|
||||
var book = context.Books.Local.FirstOrDefault(b => b.AudibleProductId == asin);
|
||||
if (book is null)
|
||||
continue;
|
||||
|
||||
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
||||
if (fileType == FileType.PDF)
|
||||
book.UserDefinedItem.PdfStatus = LiberatedStatus.Liberated;
|
||||
|
||||
if (fileType == FileType.Audio)
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
book.UserDefinedItem.BookLocation = path;
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
|
||||
}
|
||||
debugStopwatch.Stop();
|
||||
var debugTotal = debugStopwatch.Elapsed;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
{
|
||||
if (config.GetObject("Serilog") != null)
|
||||
@@ -257,7 +334,7 @@ namespace LibationLauncher
|
||||
new JObject
|
||||
{
|
||||
// for this sink to work, a path must be provided. we override this below
|
||||
{ "path", Path.Combine(Configuration.Instance.LibationFiles, "_Log.log") },
|
||||
{ "path", Path.Combine(config.LibationFiles, "_Log.log") },
|
||||
{ "rollingInterval", "Month" },
|
||||
// Serilog template formatting examples
|
||||
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||
@@ -276,95 +353,24 @@ namespace LibationLauncher
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
}
|
||||
|
||||
// to restore original: Console.SetOut(origOut);
|
||||
private static TextWriter origOut { get; } = Console.Out;
|
||||
|
||||
private static void configureLogging(Configuration config)
|
||||
{
|
||||
// override path. always use current libation files
|
||||
var logPath = Path.Combine(Configuration.Instance.LibationFiles, "Log.log");
|
||||
config.SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath);
|
||||
config.ConfigureLogging();
|
||||
|
||||
//// hack which achieves the same
|
||||
//configuration["Serilog:WriteTo:1:Args:path"] = logPath;
|
||||
|
||||
// CONFIGURATION-DRIVEN (json)
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(config.SettingsFilePath)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
|
||||
//// MANUAL HARD CODED
|
||||
//Log.Logger = new LoggerConfiguration()
|
||||
// // requires: using Dinah.Core.Logging;
|
||||
// .Enrich.WithCaller()
|
||||
// .MinimumLevel.Information()
|
||||
// .WriteTo.File(logPath,
|
||||
// rollingInterval: RollingInterval.Month,
|
||||
// outputTemplate: code_outputTemplate)
|
||||
// .CreateLogger();
|
||||
// Fwd Console to serilog.
|
||||
// Serilog also writes to Console (should probably change this) so it might be asking for trouble.
|
||||
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
|
||||
// Empirical testing so far has shown no issues.
|
||||
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
|
||||
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
private static void checkForUpdate(Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
if (!Version.TryParse(latestVersionString, out var latestRelease))
|
||||
return;
|
||||
|
||||
// we're up to date
|
||||
if (latestRelease <= BuildVersion)
|
||||
return;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
var zipUrl = zip?.BrowserDownloadUrl;
|
||||
if (zipUrl is null)
|
||||
{
|
||||
MessageBox.Show(latest.HtmlUrl, "New version available");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"New version available @ {latest.HtmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
using var fileSelector = new SaveFileDialog { FileName = zip.Name, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
var selectedPath = fileSelector.FileName;
|
||||
|
||||
try
|
||||
{
|
||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex, "Error downloading update");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex, "Error checking for update");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Error(Exception ex, string message)
|
||||
{
|
||||
Log.Logger.Error(ex, message);
|
||||
MessageBox.Show($"{message}\r\nSee log for details");
|
||||
}
|
||||
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
// begin logging session with a form feed
|
||||
@@ -372,6 +378,11 @@ namespace LibationLauncher
|
||||
Log.Logger.Information("Begin Libation. {@DebugInfo}", new
|
||||
{
|
||||
Version = BuildVersion.ToString(),
|
||||
#if DEBUG
|
||||
Mode = "Debug",
|
||||
#else
|
||||
Mode = "Release",
|
||||
#endif
|
||||
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
@@ -392,19 +403,92 @@ namespace LibationLauncher
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
||||
});
|
||||
|
||||
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
|
||||
if (Log.Logger.IsVerboseEnabled())
|
||||
MessageBox.Show(@"
|
||||
Warning: verbose logging is enabled.
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
This should be used for debugging only. It creates many
|
||||
more logs and debug files, neither of which are as
|
||||
strictly anonomous.
|
||||
private static void checkForUpdate(Configuration config)
|
||||
{
|
||||
string zipUrl;
|
||||
string selectedPath;
|
||||
|
||||
When you are finished debugging, it's highly recommended
|
||||
to set your debug MinimumLevel to Information and restart
|
||||
Libation.
|
||||
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
try
|
||||
{
|
||||
// timed out
|
||||
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
if (latest is null)
|
||||
return;
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
if (!Version.TryParse(latestVersionString, out var latestRelease))
|
||||
return;
|
||||
|
||||
// we're up to date
|
||||
if (latestRelease <= BuildVersion)
|
||||
return;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
zipUrl = zip?.BrowserDownloadUrl;
|
||||
|
||||
Log.Logger.Information("Update available: {@DebugInfo}", new {
|
||||
latestRelease = latestRelease.ToString(),
|
||||
latest.HtmlUrl,
|
||||
zipUrl
|
||||
});
|
||||
|
||||
if (zipUrl is null)
|
||||
{
|
||||
MessageBox.Show(latest.HtmlUrl, "New version available");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"New version available @ {latest.HtmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
using var fileSelector = new SaveFileDialog { FileName = zip.Name, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
selectedPath = fileSelector.FileName;
|
||||
}
|
||||
catch (AggregateException aggEx)
|
||||
{
|
||||
Log.Logger.Error(aggEx, "Checking for new version too often");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error checking for update", "Error checking for update", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error downloading update", "Error downloading update", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Octokit.Release getLatestRelease(TimeSpan timeout)
|
||||
{
|
||||
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
|
||||
if (task.Wait(timeout))
|
||||
return task.Result;
|
||||
|
||||
Log.Logger.Information("Timed out");
|
||||
return null;
|
||||
}
|
||||
private static Octokit.Release getLatestRelease()
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
return latest;
|
||||
}
|
||||
|
||||
private static Version BuildVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
|
||||
@@ -18,8 +18,6 @@ namespace LibationSearchEngine
|
||||
{
|
||||
public const Lucene.Net.Util.Version Version = Lucene.Net.Util.Version.LUCENE_30;
|
||||
|
||||
private LibationContext context { get; }
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
|
||||
@@ -32,8 +30,6 @@ namespace LibationSearchEngine
|
||||
// the workaround which allows displaying all books when query is empty
|
||||
public const string ALL_QUERY = "*:*";
|
||||
|
||||
public SearchEngine(LibationContext context) => this.context = context;
|
||||
|
||||
#region index rules
|
||||
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
|
||||
@@ -130,8 +126,9 @@ namespace LibationSearchEngine
|
||||
["Abridged"] = lb => lb.Book.IsAbridged,
|
||||
|
||||
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
|
||||
["IsLiberated"] = lb => isLiberated(lb.Book.AudibleProductId),
|
||||
["Liberated"] = lb => isLiberated(lb.Book.AudibleProductId),
|
||||
["IsLiberated"] = lb => isLiberated(lb.Book),
|
||||
["Liberated"] = lb => isLiberated(lb.Book),
|
||||
["LiberatedError"] = lb => liberatedError(lb.Book),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -142,7 +139,10 @@ namespace LibationSearchEngine
|
||||
return authors.Intersect(narrators).Any();
|
||||
}
|
||||
|
||||
private static bool isLiberated(string id) => AudibleFileStorage.Audio.Exists(id);
|
||||
private static bool isLiberated(Book book)
|
||||
=> book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated
|
||||
|| AudibleFileStorage.Audio.Exists(book.AudibleProductId);
|
||||
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
|
||||
|
||||
// use these common fields in the "all" default search field
|
||||
private static IEnumerable<Func<LibraryBook, string>> allFieldIndexRules { get; }
|
||||
@@ -198,11 +198,10 @@ namespace LibationSearchEngine
|
||||
/// create new. ie: full re-index
|
||||
/// </summary>
|
||||
/// <param name="overwrite"></param>
|
||||
public void CreateNewIndex(bool overwrite = true)
|
||||
public void CreateNewIndex(LibationContext context, bool overwrite = true)
|
||||
{
|
||||
// 300 products
|
||||
// 1st run after app is started: 400ms
|
||||
// subsequent runs: 200ms
|
||||
// 300 titles: 200- 400 ms
|
||||
// 1021 titles: 1777-2250 ms
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var stamps = new List<long>();
|
||||
void log() => stamps.Add(sw.ElapsedMilliseconds);
|
||||
@@ -232,7 +231,7 @@ namespace LibationSearchEngine
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(string productId)
|
||||
public void UpdateBook(LibationContext context, string productId)
|
||||
{
|
||||
var libraryBook = context.GetLibraryBook_Flat_NoTracking(productId);
|
||||
var term = new Term(_ID_, productId);
|
||||
@@ -301,18 +300,22 @@ namespace LibationSearchEngine
|
||||
});
|
||||
|
||||
// update single document entry
|
||||
public void UpdateIsLiberated(string productId)
|
||||
public void UpdateLiberatedStatus(Book book)
|
||||
=> updateDocument(
|
||||
productId,
|
||||
book.AudibleProductId,
|
||||
d =>
|
||||
{
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
var v = isLiberated(productId);
|
||||
var v1 = isLiberated(book);
|
||||
d.RemoveField("IsLiberated");
|
||||
d.AddBool("IsLiberated", v);
|
||||
d.AddBool("IsLiberated", v1);
|
||||
d.RemoveField("Liberated");
|
||||
d.AddBool("Liberated", v);
|
||||
d.AddBool("Liberated", v1);
|
||||
|
||||
var v2 = liberatedError(book);
|
||||
d.RemoveField("LiberatedError");
|
||||
d.AddBool("LiberatedError", v2);
|
||||
});
|
||||
|
||||
private static void updateDocument(string productId, Action<Document> action)
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
this.pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
this.bookInfoLbl = new System.Windows.Forms.Label();
|
||||
this.progressBar1 = new System.Windows.Forms.ProgressBar();
|
||||
this.rtbLog = new System.Windows.Forms.RichTextBox();
|
||||
this.remainingTimeLbl = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
@@ -60,28 +59,16 @@
|
||||
//
|
||||
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.progressBar1.Location = new System.Drawing.Point(14, 607);
|
||||
this.progressBar1.Location = new System.Drawing.Point(14, 143);
|
||||
this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.progressBar1.Name = "progressBar1";
|
||||
this.progressBar1.Size = new System.Drawing.Size(611, 27);
|
||||
this.progressBar1.TabIndex = 2;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
this.rtbLog.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.rtbLog.Location = new System.Drawing.Point(14, 136);
|
||||
this.rtbLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.rtbLog.Name = "rtbLog";
|
||||
this.rtbLog.Size = new System.Drawing.Size(678, 463);
|
||||
this.rtbLog.TabIndex = 1;
|
||||
this.rtbLog.Text = "";
|
||||
//
|
||||
// remainingTimeLbl
|
||||
//
|
||||
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 607);
|
||||
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 143);
|
||||
this.remainingTimeLbl.Name = "remainingTimeLbl";
|
||||
this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31);
|
||||
this.remainingTimeLbl.TabIndex = 3;
|
||||
@@ -92,9 +79,8 @@
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(707, 647);
|
||||
this.ClientSize = new System.Drawing.Size(707, 183);
|
||||
this.Controls.Add(this.remainingTimeLbl);
|
||||
this.Controls.Add(this.rtbLog);
|
||||
this.Controls.Add(this.progressBar1);
|
||||
this.Controls.Add(this.bookInfoLbl);
|
||||
this.Controls.Add(this.pictureBox1);
|
||||
@@ -102,8 +88,6 @@
|
||||
this.Name = "DecryptForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "DecryptForm";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DecryptForm_FormClosing);
|
||||
this.Load += new System.EventHandler(this.DecryptForm_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
@@ -115,7 +99,6 @@
|
||||
private System.Windows.Forms.PictureBox pictureBox1;
|
||||
private System.Windows.Forms.Label bookInfoLbl;
|
||||
private System.Windows.Forms.ProgressBar progressBar1;
|
||||
private System.Windows.Forms.RichTextBox rtbLog;
|
||||
private System.Windows.Forms.Label remainingTimeLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,21 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Drawing;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
public partial class DecryptForm : Form
|
||||
{
|
||||
public DecryptForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
System.IO.TextWriter origOut { get; } = Console.Out;
|
||||
private void DecryptForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
// redirect Console.WriteLine to console, textbox
|
||||
var multiLogger = new MultiTextWriter(
|
||||
origOut,
|
||||
new RichTextBoxTextWriter(this.rtbLog),
|
||||
new SerilogTextWriter());
|
||||
Console.SetOut(multiLogger);
|
||||
}
|
||||
|
||||
private void DecryptForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
// restore original
|
||||
Console.SetOut(origOut);
|
||||
}
|
||||
public DecryptForm() => InitializeComponent();
|
||||
|
||||
// book info
|
||||
string title;
|
||||
string authorNames;
|
||||
string narratorNames;
|
||||
private string title;
|
||||
private string authorNames;
|
||||
private string narratorNames;
|
||||
|
||||
public void SetTitle(string title)
|
||||
public void SetTitle(string actionName, string title)
|
||||
{
|
||||
this.UIThread(() => this.Text = " Decrypting " + title);
|
||||
this.UIThread(() => this.Text = actionName + " " + title);
|
||||
this.title = title;
|
||||
updateBookInfo();
|
||||
}
|
||||
@@ -62,15 +40,15 @@ namespace LibationWinForms.BookLiberation
|
||||
public void UpdateProgress(int percentage)
|
||||
{
|
||||
if (percentage == 0)
|
||||
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec");
|
||||
|
||||
updateRemainingTime(0);
|
||||
else
|
||||
progressBar1.UIThread(() => progressBar1.Value = percentage);
|
||||
}
|
||||
|
||||
public void UpdateRemainingTime(TimeSpan remaining)
|
||||
{
|
||||
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{(int)remaining.TotalSeconds} sec");
|
||||
}
|
||||
=> updateRemainingTime((int)remaining.TotalSeconds);
|
||||
|
||||
private void updateRemainingTime(int remaining)
|
||||
=> remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using FileLiberator;
|
||||
@@ -50,15 +51,14 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public static class ProcessorAutomationController
|
||||
{
|
||||
public static async Task BackupSingleBookAsync(string productId, EventHandler<LibraryBook> completedAction = null)
|
||||
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupSingleBookAsync) + " {@DebugInfo}", new { productId });
|
||||
Serilog.Log.Logger.Information("Begin backup single {@DebugInfo}", new { libraryBook?.Book?.AudibleProductId });
|
||||
|
||||
var backupBook = getWiredUpBackupBook(completedAction);
|
||||
|
||||
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
|
||||
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
|
||||
// continue even if libraryBook is null. we'll display even that in the processing box
|
||||
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
|
||||
|
||||
@@ -79,33 +79,47 @@ namespace LibationWinForms.BookLiberation
|
||||
unsubscribeEvents();
|
||||
}
|
||||
|
||||
public static async Task ConvertAllBooksAsync()
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
|
||||
|
||||
var convertBook = new ConvertToMp3();
|
||||
convertBook.Begin += (_, l) => wireUpEvents(convertBook, l, "Converting");
|
||||
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
|
||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
||||
void convertBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Begin: {libraryBook.Book}");
|
||||
void convertBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
convertBook.Begin += convertBookBegin;
|
||||
convertBook.StatusUpdate += statusUpdate;
|
||||
convertBook.Completed += convertBookCompleted;
|
||||
|
||||
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
|
||||
|
||||
convertBook.Begin -= convertBookBegin;
|
||||
convertBook.StatusUpdate -= statusUpdate;
|
||||
convertBook.Completed -= convertBookCompleted;
|
||||
}
|
||||
|
||||
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
|
||||
{
|
||||
var backupBook = new BackupBook();
|
||||
|
||||
backupBook.DecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DecryptBook, l);
|
||||
backupBook.DownloadDecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DownloadDecryptBook, l);
|
||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
|
||||
|
||||
// must occur before completedAction. A common use case is:
|
||||
// - filter by -liberated
|
||||
// - liberate only that book
|
||||
// completedAction is to refresh grid
|
||||
// - want to see that book disappear from grid
|
||||
// also for this to work, updateIsLiberated can NOT be async
|
||||
backupBook.DecryptBook.Completed += updateIsLiberated;
|
||||
backupBook.DownloadPdf.Completed += updateIsLiberated;
|
||||
|
||||
if (completedAction != null)
|
||||
{
|
||||
backupBook.DecryptBook.Completed += completedAction;
|
||||
backupBook.DownloadDecryptBook.Completed += completedAction;
|
||||
backupBook.DownloadPdf.Completed += completedAction;
|
||||
}
|
||||
|
||||
return backupBook;
|
||||
}
|
||||
|
||||
private static void updateIsLiberated(object sender, LibraryBook e) => ApplicationServices.SearchEngineCommands.UpdateIsLiberated(e.Book);
|
||||
|
||||
private static (Action unsubscribeEvents, LogMe) attachToBackupsForm(BackupBook backupBook, AutomatedBackupsForm automatedBackupsForm = null)
|
||||
{
|
||||
#region create logger
|
||||
@@ -123,9 +137,9 @@ namespace LibationWinForms.BookLiberation
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
backupBook.DecryptBook.Begin += decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate += statusUpdate;
|
||||
backupBook.DecryptBook.Completed += decryptBookCompleted;
|
||||
backupBook.DownloadDecryptBook.Begin += decryptBookBegin;
|
||||
backupBook.DownloadDecryptBook.StatusUpdate += statusUpdate;
|
||||
backupBook.DownloadDecryptBook.Completed += decryptBookCompleted;
|
||||
backupBook.DownloadPdf.Begin += downloadPdfBegin;
|
||||
backupBook.DownloadPdf.StatusUpdate += statusUpdate;
|
||||
backupBook.DownloadPdf.Completed += downloadPdfCompleted;
|
||||
@@ -135,9 +149,9 @@ namespace LibationWinForms.BookLiberation
|
||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
||||
Action unsubscribe = () =>
|
||||
{
|
||||
backupBook.DecryptBook.Begin -= decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DecryptBook.Completed -= decryptBookCompleted;
|
||||
backupBook.DownloadDecryptBook.Begin -= decryptBookBegin;
|
||||
backupBook.DownloadDecryptBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DownloadDecryptBook.Completed -= decryptBookCompleted;
|
||||
backupBook.DownloadPdf.Begin -= downloadPdfBegin;
|
||||
backupBook.DownloadPdf.StatusUpdate -= statusUpdate;
|
||||
backupBook.DownloadPdf.Completed -= downloadPdfCompleted;
|
||||
@@ -175,7 +189,8 @@ namespace LibationWinForms.BookLiberation
|
||||
downloadDialog.UpdateFilename(destination);
|
||||
downloadDialog.Show();
|
||||
|
||||
new System.Threading.Thread(() => {
|
||||
new System.Threading.Thread(() =>
|
||||
{
|
||||
var downloadFile = new DownloadFile();
|
||||
|
||||
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.UIThread(() =>
|
||||
@@ -189,7 +204,9 @@ namespace LibationWinForms.BookLiberation
|
||||
});
|
||||
|
||||
downloadFile.PerformDownloadFileAsync(url, destination).GetAwaiter().GetResult();
|
||||
}).Start();
|
||||
})
|
||||
{ IsBackground = true }
|
||||
.Start();
|
||||
}
|
||||
|
||||
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
|
||||
@@ -257,14 +274,14 @@ namespace LibationWinForms.BookLiberation
|
||||
}
|
||||
|
||||
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
|
||||
private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook)
|
||||
private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook, string actionName = "Decrypting")
|
||||
{
|
||||
#region create form
|
||||
var decryptDialog = new DecryptForm();
|
||||
#endregion
|
||||
|
||||
#region Set initially displayed book properties from library info.
|
||||
decryptDialog.SetTitle(libraryBook.Book.Title);
|
||||
decryptDialog.SetTitle(actionName, libraryBook.Book.Title);
|
||||
decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors));
|
||||
decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames));
|
||||
decryptDialog.SetCoverImage(
|
||||
@@ -277,7 +294,7 @@ namespace LibationWinForms.BookLiberation
|
||||
#region define how model actions will affect form behavior
|
||||
void decryptBegin(object _, string __) => decryptDialog.Show();
|
||||
|
||||
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(title);
|
||||
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(actionName, title);
|
||||
void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors);
|
||||
void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators);
|
||||
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes));
|
||||
@@ -440,13 +457,33 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||
|
||||
var dialogResult = MessageBox.Show(SkipDialogText, "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
|
||||
string details;
|
||||
try
|
||||
{
|
||||
static string trunc(string str)
|
||||
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
|
||||
: (str.Length > 50) ? $"{str.Truncate(47)}..."
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.Title}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames)}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames)}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
details = "[Error retrieving details]";
|
||||
}
|
||||
|
||||
var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
|
||||
|
||||
if (dialogResult == DialogResult.Abort)
|
||||
return false;
|
||||
|
||||
if (dialogResult == CreateSkipFileResult)
|
||||
{
|
||||
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
|
||||
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
|
||||
LogMe.Info($@"
|
||||
Created new 'skip' file
|
||||
@@ -464,6 +501,7 @@ Created new 'skip' file
|
||||
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book. Skip this book permanently?
|
||||
{0}
|
||||
|
||||
- Click YES to skip this book permanently.
|
||||
|
||||
@@ -487,7 +525,8 @@ An error occurred while trying to process this book. Skip this book permanently?
|
||||
class BackupLoop : BackupRunner
|
||||
{
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book
|
||||
An error occurred while trying to process this book.
|
||||
{0}
|
||||
|
||||
- ABORT: stop processing books.
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error: {ex.Message}", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBoxAlertAdmin.Show("Error attempting to save accounts", "Error saving accounts", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class EditTagsDialog
|
||||
partial class BookDetailsDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -3,15 +3,15 @@ using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class EditTagsDialog : Form
|
||||
public partial class BookDetailsDialog : Form
|
||||
{
|
||||
public string NewTags { get; private set; }
|
||||
|
||||
public EditTagsDialog()
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
public EditTagsDialog(string title, string rawTags) : this()
|
||||
public BookDetailsDialog(string title, string rawTags) : this()
|
||||
{
|
||||
this.Text = $"Edit Tags - {title}";
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace LibationWinForms.Dialogs
|
||||
// customDirectoryRb
|
||||
//
|
||||
this.customDirectoryRb.AutoSize = true;
|
||||
this.customDirectoryRb.Location = new System.Drawing.Point(2, 56);
|
||||
this.customDirectoryRb.Location = new System.Drawing.Point(2, 62);
|
||||
this.customDirectoryRb.Name = "customDirectoryRb";
|
||||
this.customDirectoryRb.Size = new System.Drawing.Size(14, 13);
|
||||
this.customDirectoryRb.TabIndex = 2;
|
||||
@@ -60,7 +60,7 @@ namespace LibationWinForms.Dialogs
|
||||
//
|
||||
this.customTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.customTb.Location = new System.Drawing.Point(22, 52);
|
||||
this.customTb.Location = new System.Drawing.Point(22, 58);
|
||||
this.customTb.Name = "customTb";
|
||||
this.customTb.Size = new System.Drawing.Size(588, 23);
|
||||
this.customTb.TabIndex = 3;
|
||||
@@ -68,7 +68,7 @@ namespace LibationWinForms.Dialogs
|
||||
// customBtn
|
||||
//
|
||||
this.customBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.customBtn.Location = new System.Drawing.Point(616, 52);
|
||||
this.customBtn.Location = new System.Drawing.Point(616, 58);
|
||||
this.customBtn.Name = "customBtn";
|
||||
this.customBtn.Size = new System.Drawing.Size(41, 27);
|
||||
this.customBtn.TabIndex = 4;
|
||||
@@ -82,7 +82,7 @@ namespace LibationWinForms.Dialogs
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.directorySelectControl.Location = new System.Drawing.Point(23, 0);
|
||||
this.directorySelectControl.Name = "directorySelectControl";
|
||||
this.directorySelectControl.Size = new System.Drawing.Size(635, 46);
|
||||
this.directorySelectControl.Size = new System.Drawing.Size(635, 52);
|
||||
this.directorySelectControl.TabIndex = 5;
|
||||
//
|
||||
// DirectoryOrCustomSelectControl
|
||||
@@ -95,7 +95,7 @@ namespace LibationWinForms.Dialogs
|
||||
this.Controls.Add(this.customDirectoryRb);
|
||||
this.Controls.Add(this.knownDirectoryRb);
|
||||
this.Name = "DirectoryOrCustomSelectControl";
|
||||
this.Size = new System.Drawing.Size(660, 81);
|
||||
this.Size = new System.Drawing.Size(660, 87);
|
||||
this.Load += new System.EventHandler(this.DirectoryOrCustomSelectControl_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.directoryComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.textBox1 = new System.Windows.Forms.TextBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// directoryComboBox
|
||||
@@ -45,23 +45,24 @@ namespace LibationWinForms.Dialogs
|
||||
this.directoryComboBox.TabIndex = 0;
|
||||
this.directoryComboBox.SelectedIndexChanged += new System.EventHandler(this.directoryComboBox_SelectedIndexChanged);
|
||||
//
|
||||
// label1
|
||||
// textBox1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(0, 26);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(97, 15);
|
||||
this.label1.TabIndex = 1;
|
||||
this.label1.Text = "Select a directory";
|
||||
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.textBox1.Location = new System.Drawing.Point(0, 29);
|
||||
this.textBox1.Name = "textBox1";
|
||||
this.textBox1.ReadOnly = true;
|
||||
this.textBox1.Size = new System.Drawing.Size(647, 23);
|
||||
this.textBox1.TabIndex = 1;
|
||||
//
|
||||
// DirectorySelectControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.textBox1);
|
||||
this.Controls.Add(this.directoryComboBox);
|
||||
this.Name = "DirectorySelectControl";
|
||||
this.Size = new System.Drawing.Size(647, 46);
|
||||
this.Size = new System.Drawing.Size(647, 52);
|
||||
this.Load += new System.EventHandler(this.DirectorySelectControl_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
@@ -71,6 +72,6 @@ namespace LibationWinForms.Dialogs
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.ComboBox directoryComboBox;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.TextBox textBox1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,6 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
}
|
||||
|
||||
private void directoryComboBox_SelectedIndexChanged(object sender, EventArgs e) => this.label1.Text = selectedItem.UiDisplayPath;
|
||||
private void directoryComboBox_SelectedIndexChanged(object sender, EventArgs e) => this.textBox1.Text = selectedItem.UiDisplayPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator";
|
||||
Serilog.Log.Logger.Error(ex, msg);
|
||||
MessageBox.Show(msg, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBoxAlertAdmin.Show(
|
||||
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
|
||||
"Error importing library",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.libationFilesSelectControl.Location = new System.Drawing.Point(14, 28);
|
||||
this.libationFilesSelectControl.Name = "libationFilesSelectControl";
|
||||
this.libationFilesSelectControl.Size = new System.Drawing.Size(909, 81);
|
||||
this.libationFilesSelectControl.Size = new System.Drawing.Size(909, 87);
|
||||
this.libationFilesSelectControl.TabIndex = 1;
|
||||
//
|
||||
// LibationFilesDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(933, 158);
|
||||
this.ClientSize = new System.Drawing.Size(933, 164);
|
||||
this.Controls.Add(this.libationFilesSelectControl);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
if (!System.IO.Directory.Exists(libationDir))
|
||||
{
|
||||
MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir);
|
||||
MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace LibationWinForms.Dialogs.Login
|
||||
|
||||
private void approvedBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Submit button clicked");
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
@@ -29,6 +30,8 @@ namespace LibationWinForms.Dialogs.Login
|
||||
Email = accountId;
|
||||
Password = this.passwordTb.Text;
|
||||
|
||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
// Close() not needed for AcceptButton
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
Answer = this.answerTb.Text;
|
||||
|
||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
// Close() not needed for AcceptButton
|
||||
}
|
||||
|
||||
@@ -14,49 +14,77 @@ namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
private RadioButton[] radioButtons { get; }
|
||||
|
||||
AudibleApi.MfaConfig _mfaConfig { get; }
|
||||
|
||||
public MfaDialog(AudibleApi.MfaConfig mfaConfig)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_mfaConfig = mfaConfig;
|
||||
|
||||
radioButtons = new[] { this.radioButton1, this.radioButton2, this.radioButton3 };
|
||||
|
||||
// optional string settings
|
||||
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
|
||||
this.Text = mfaConfig.Title;
|
||||
setOptional(this.radioButton1, mfaConfig.Button1Text);
|
||||
setOptional(this.radioButton2, mfaConfig.Button2Text);
|
||||
setOptional(this.radioButton3, mfaConfig.Button3Text);
|
||||
|
||||
// mandatory values
|
||||
radioButton1.Name = mfaConfig.Button1Name;
|
||||
radioButton1.Tag = mfaConfig.Button1Value;
|
||||
setRadioButton(0, this.radioButton1);
|
||||
setRadioButton(1, this.radioButton2);
|
||||
setRadioButton(2, this.radioButton3);
|
||||
|
||||
radioButton2.Name = mfaConfig.Button2Name;
|
||||
radioButton2.Tag = mfaConfig.Button2Value;
|
||||
|
||||
radioButton3.Name = mfaConfig.Button3Name;
|
||||
radioButton3.Tag = mfaConfig.Button3Value;
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new {
|
||||
paramButtonCount = mfaConfig.Buttons.Count,
|
||||
visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible)
|
||||
});
|
||||
}
|
||||
|
||||
private static void setOptional(RadioButton radioButton, string text)
|
||||
private void setRadioButton(int pos, RadioButton rb)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
radioButton.Text = text;
|
||||
if (_mfaConfig.Buttons.Count <= pos)
|
||||
{
|
||||
rb.Checked = false;
|
||||
rb.Enabled = false;
|
||||
rb.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = _mfaConfig.Buttons[pos];
|
||||
|
||||
// optional
|
||||
if (!string.IsNullOrWhiteSpace(btn.Text))
|
||||
rb.Text = btn.Text;
|
||||
|
||||
// mandatory values
|
||||
rb.Name = btn.Name;
|
||||
rb.Tag = btn.Value;
|
||||
}
|
||||
|
||||
public string SelectedName { get; private set; }
|
||||
public string SelectedValue { get; private set; }
|
||||
private void submitBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("RadioButton states: {@DebugInfo}", new {
|
||||
var selected = radioButtons.FirstOrDefault(rb => rb.Checked);
|
||||
|
||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new {
|
||||
rb1_visible = radioButton1.Visible,
|
||||
rb1_checked = radioButton1.Checked,
|
||||
r21_checked = radioButton2.Checked,
|
||||
rb3_checked = radioButton3.Checked
|
||||
|
||||
rb2_visible = radioButton2.Visible,
|
||||
rb2_checked = radioButton2.Checked,
|
||||
|
||||
rb3_visible = radioButton3.Visible,
|
||||
rb3_checked = radioButton3.Checked,
|
||||
|
||||
isSelected = selected is not null,
|
||||
name = selected?.Name,
|
||||
value = selected?.Tag
|
||||
});
|
||||
|
||||
var selected = radioButtons.Single(rb => rb.Checked);
|
||||
|
||||
Serilog.Log.Logger.Debug("Selected: {@DebugInfo}", new { isSelected = selected is not null, name = selected?.Name, value = selected?.Tag });
|
||||
if (selected is null)
|
||||
{
|
||||
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedName = selected.Name;
|
||||
SelectedValue = (string)selected.Tag;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace LibationWinForms.Login
|
||||
public string Get2faCode()
|
||||
{
|
||||
using var dialog = new _2faCodeDialog();
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
if (showDialog(dialog))
|
||||
return dialog.Code;
|
||||
return null;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace LibationWinForms.Login
|
||||
public string GetCaptchaAnswer(byte[] captchaImage)
|
||||
{
|
||||
using var dialog = new CaptchaDialog(captchaImage);
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
if (showDialog(dialog))
|
||||
return dialog.Answer;
|
||||
return null;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace LibationWinForms.Login
|
||||
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
|
||||
{
|
||||
using var dialog = new MfaDialog(mfaConfig);
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
if (showDialog(dialog))
|
||||
return (dialog.SelectedName, dialog.SelectedValue);
|
||||
return (null, null);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace LibationWinForms.Login
|
||||
public (string email, string password) GetLogin()
|
||||
{
|
||||
using var dialog = new AudibleLoginDialog(_account);
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
if (showDialog(dialog))
|
||||
return (dialog.Email, dialog.Password);
|
||||
return (null, null);
|
||||
}
|
||||
@@ -49,7 +49,15 @@ namespace LibationWinForms.Login
|
||||
public void ShowApprovalNeeded()
|
||||
{
|
||||
using var dialog = new ApprovalNeededDialog();
|
||||
dialog.ShowDialog();
|
||||
showDialog(dialog);
|
||||
}
|
||||
|
||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||
private static bool showDialog(System.Windows.Forms.Form dialog)
|
||||
{
|
||||
var result = dialog.ShowDialog();
|
||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
||||
return result == System.Windows.Forms.DialogResult.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,10 @@ namespace LibationWinForms.Dialogs.Login
|
||||
|
||||
private void submitBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Code = this.codeTb.Text;
|
||||
Code = this.codeTb.Text.Trim();
|
||||
|
||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
}
|
||||
|
||||
155
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.Designer.cs
generated
Normal file
155
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,155 @@
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class MessageBoxAlertAdminDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MessageBoxAlertAdminDialog));
|
||||
this.pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
this.descriptionLbl = new System.Windows.Forms.Label();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.githubLink = new System.Windows.Forms.LinkLabel();
|
||||
this.okBtn = new System.Windows.Forms.Button();
|
||||
this.logsLink = new System.Windows.Forms.LinkLabel();
|
||||
this.exceptionTb = new System.Windows.Forms.TextBox();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// pictureBox1
|
||||
//
|
||||
this.pictureBox1.Location = new System.Drawing.Point(12, 12);
|
||||
this.pictureBox1.Name = "pictureBox1";
|
||||
this.pictureBox1.Size = new System.Drawing.Size(64, 64);
|
||||
this.pictureBox1.TabIndex = 0;
|
||||
this.pictureBox1.TabStop = false;
|
||||
//
|
||||
// descriptionLbl
|
||||
//
|
||||
this.descriptionLbl.AutoSize = true;
|
||||
this.descriptionLbl.Location = new System.Drawing.Point(82, 12);
|
||||
this.descriptionLbl.Name = "descriptionLbl";
|
||||
this.descriptionLbl.Size = new System.Drawing.Size(89, 45);
|
||||
this.descriptionLbl.TabIndex = 0;
|
||||
this.descriptionLbl.Text = "[Error message]\r\n[Error message]\r\n[Error message]";
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 244);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(269, 90);
|
||||
this.label1.TabIndex = 2;
|
||||
this.label1.Text = resources.GetString("label1.Text");
|
||||
//
|
||||
// githubLink
|
||||
//
|
||||
this.githubLink.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.githubLink.AutoSize = true;
|
||||
this.githubLink.Location = new System.Drawing.Point(271, 274);
|
||||
this.githubLink.Name = "githubLink";
|
||||
this.githubLink.Size = new System.Drawing.Size(116, 15);
|
||||
this.githubLink.TabIndex = 3;
|
||||
this.githubLink.TabStop = true;
|
||||
this.githubLink.Text = "Click to go to github";
|
||||
this.githubLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.githubLink_LinkClicked);
|
||||
//
|
||||
// okBtn
|
||||
//
|
||||
this.okBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.okBtn.Location = new System.Drawing.Point(256, 347);
|
||||
this.okBtn.Name = "okBtn";
|
||||
this.okBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.okBtn.TabIndex = 5;
|
||||
this.okBtn.Text = "OK";
|
||||
this.okBtn.UseVisualStyleBackColor = true;
|
||||
this.okBtn.Click += new System.EventHandler(this.okBtn_Click);
|
||||
//
|
||||
// logsLink
|
||||
//
|
||||
this.logsLink.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.logsLink.AutoSize = true;
|
||||
this.logsLink.Location = new System.Drawing.Point(271, 289);
|
||||
this.logsLink.Name = "logsLink";
|
||||
this.logsLink.Size = new System.Drawing.Size(155, 15);
|
||||
this.logsLink.TabIndex = 4;
|
||||
this.logsLink.TabStop = true;
|
||||
this.logsLink.Text = "Click to open log files folder";
|
||||
this.logsLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.logsLink_LinkClicked);
|
||||
//
|
||||
// exceptionTb
|
||||
//
|
||||
this.exceptionTb.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.exceptionTb.Location = new System.Drawing.Point(12, 82);
|
||||
this.exceptionTb.Multiline = true;
|
||||
this.exceptionTb.Name = "exceptionTb";
|
||||
this.exceptionTb.ReadOnly = true;
|
||||
this.exceptionTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.exceptionTb.Size = new System.Drawing.Size(560, 159);
|
||||
this.exceptionTb.TabIndex = 1;
|
||||
//
|
||||
// MessageBoxAlertAdminDialog
|
||||
//
|
||||
this.AcceptButton = this.okBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(584, 382);
|
||||
this.Controls.Add(this.exceptionTb);
|
||||
this.Controls.Add(this.logsLink);
|
||||
this.Controls.Add(this.okBtn);
|
||||
this.Controls.Add(this.githubLink);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.descriptionLbl);
|
||||
this.Controls.Add(this.pictureBox1);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "MessageBoxAlertAdminDialog";
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "MessageBoxAlertAdmin";
|
||||
this.Load += new System.EventHandler(this.MessageBoxAlertAdminDialog_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.PictureBox pictureBox1;
|
||||
private System.Windows.Forms.Label descriptionLbl;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.LinkLabel githubLink;
|
||||
private System.Windows.Forms.Button okBtn;
|
||||
private System.Windows.Forms.LinkLabel logsLink;
|
||||
private System.Windows.Forms.TextBox exceptionTb;
|
||||
}
|
||||
}
|
||||
46
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs
Normal file
46
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class MessageBoxAlertAdminDialog : Form
|
||||
{
|
||||
public MessageBoxAlertAdminDialog() => InitializeComponent();
|
||||
|
||||
/// <summary>
|
||||
/// Displays a message box with specified text and caption.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to display in the message box.</param>
|
||||
/// <param name="caption">The text to display in the title bar of the message box.</param>
|
||||
/// <param name="exception">Exception to display</param>
|
||||
public MessageBoxAlertAdminDialog(string text, string caption, Exception exception) : this()
|
||||
{
|
||||
this.descriptionLbl.Text = text;
|
||||
this.Text = caption;
|
||||
this.exceptionTb.Text = $"{exception.Message}\r\n\r\n{exception.StackTrace}";
|
||||
}
|
||||
|
||||
private void MessageBoxAlertAdminDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
System.Media.SystemSounds.Hand.Play();
|
||||
pictureBox1.Image = SystemIcons.Error.ToBitmap();
|
||||
}
|
||||
|
||||
private void githubLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
=> Go.To.Url("https://github.com/rmcrackan/Libation/issues");
|
||||
|
||||
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
=> Go.To.Folder(FileManager.Configuration.Instance.LibationFiles);
|
||||
|
||||
private void okBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx
Normal file
68
LibationWinForms/Dialogs/MessageBoxAlertAdminDialog.resx
Normal file
@@ -0,0 +1,68 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="label1.Text" xml:space="preserve">
|
||||
<value>If you'd like to report this error to an advinistrator:
|
||||
|
||||
Step 1: Go to Libation's "issues" page on github
|
||||
Step 2: Find your log files
|
||||
Setp 3: Click "New issue" button
|
||||
Step 4: Drag/drop your log files</value>
|
||||
</data>
|
||||
</root>
|
||||
62
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
62
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -37,8 +37,11 @@
|
||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.logsBtn = new System.Windows.Forms.Button();
|
||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
this.advancedSettingsGb.SuspendLayout();
|
||||
this.booksGb.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
@@ -66,7 +69,7 @@
|
||||
// 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, 380);
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 419);
|
||||
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);
|
||||
@@ -79,7 +82,7 @@
|
||||
//
|
||||
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, 380);
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 419);
|
||||
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);
|
||||
@@ -97,12 +100,12 @@
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.advancedSettingsGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 141);
|
||||
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
|
||||
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, 226);
|
||||
this.advancedSettingsGb.TabIndex = 3;
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 232);
|
||||
this.advancedSettingsGb.TabIndex = 5;
|
||||
this.advancedSettingsGb.TabStop = false;
|
||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
@@ -122,10 +125,10 @@
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(7, 63);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(307, 19);
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(327, 19);
|
||||
this.convertLosslessRb.TabIndex = 0;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download books as .M4B files (Lossless Mp4a format)";
|
||||
this.convertLosslessRb.Text = "Download my books as .M4B files (Lossless Mp4a format)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
@@ -134,7 +137,7 @@
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(10, 175);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 46);
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
|
||||
this.inProgressSelectControl.TabIndex = 2;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
@@ -150,13 +153,23 @@
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
this.logsBtn.Location = new System.Drawing.Point(262, 147);
|
||||
this.logsBtn.Name = "logsBtn";
|
||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
this.logsBtn.TabIndex = 4;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 81);
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
||||
this.booksSelectControl.TabIndex = 1;
|
||||
//
|
||||
// booksGb
|
||||
@@ -167,18 +180,39 @@
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
this.booksGb.Location = new System.Drawing.Point(12, 12);
|
||||
this.booksGb.Name = "booksGb";
|
||||
this.booksGb.Size = new System.Drawing.Size(908, 123);
|
||||
this.booksGb.TabIndex = 6;
|
||||
this.booksGb.Size = new System.Drawing.Size(908, 129);
|
||||
this.booksGb.TabIndex = 1;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
this.loggingLevelLbl.AutoSize = true;
|
||||
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
|
||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
this.loggingLevelLbl.TabIndex = 2;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
//
|
||||
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.loggingLevelCb.FormattingEnabled = true;
|
||||
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
|
||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
this.loggingLevelCb.TabIndex = 3;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
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, 421);
|
||||
this.ClientSize = new System.Drawing.Size(933, 462);
|
||||
this.Controls.Add(this.logsBtn);
|
||||
this.Controls.Add(this.loggingLevelCb);
|
||||
this.Controls.Add(this.loggingLevelLbl);
|
||||
this.Controls.Add(this.booksGb);
|
||||
this.Controls.Add(this.advancedSettingsGb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
@@ -194,6 +228,7 @@
|
||||
this.booksGb.ResumeLayout(false);
|
||||
this.booksGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -209,5 +244,8 @@
|
||||
private System.Windows.Forms.RadioButton convertLossyRb;
|
||||
private System.Windows.Forms.RadioButton convertLosslessRb;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
private System.Windows.Forms.Label loggingLevelLbl;
|
||||
private System.Windows.Forms.ComboBox loggingLevelCb;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
@@ -17,6 +18,13 @@ namespace LibationWinForms.Dialogs
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
{
|
||||
loggingLevelCb.Items.Clear();
|
||||
foreach (var level in Enum<Serilog.Events.LogEventLevel>.GetValues())
|
||||
loggingLevelCb.Items.Add(level);
|
||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||
}
|
||||
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||
|
||||
@@ -49,45 +57,6 @@ namespace LibationWinForms.Dialogs
|
||||
inProgressSelectControl.SelectDirectory(config.InProgress);
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.DecryptToLossy = convertLossyRb.Checked;
|
||||
|
||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newBooks))
|
||||
{
|
||||
MessageBox.Show("Cannot set Books Location to blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(newBooks))
|
||||
{
|
||||
if (booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (booksSelectControl.SelectedDirectoryIsKnown)
|
||||
Directory.CreateDirectory(newBooks);
|
||||
}
|
||||
|
||||
config.Books = newBooks;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
|
||||
@@ -98,5 +67,57 @@ namespace LibationWinForms.Dialogs
|
||||
convertLosslessRb.Checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newBooks))
|
||||
{
|
||||
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(newBooks))
|
||||
{
|
||||
if (booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (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)
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.DecryptToLossy = convertLossyRb.Checked;
|
||||
|
||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
149
LibationWinForms/Form1.Designer.cs
generated
149
LibationWinForms/Form1.Designer.cs
generated
@@ -42,7 +42,9 @@
|
||||
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -50,34 +52,33 @@
|
||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.advancedSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.addFilterBtn = new System.Windows.Forms.Button();
|
||||
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridPanel
|
||||
//
|
||||
this.gridPanel.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.gridPanel.Location = new System.Drawing.Point(12, 56);
|
||||
this.gridPanel.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.gridPanel.Location = new System.Drawing.Point(14, 65);
|
||||
this.gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.gridPanel.Name = "gridPanel";
|
||||
this.gridPanel.Size = new System.Drawing.Size(839, 386);
|
||||
this.gridPanel.Size = new System.Drawing.Size(979, 445);
|
||||
this.gridPanel.TabIndex = 5;
|
||||
//
|
||||
// filterHelpBtn
|
||||
//
|
||||
this.filterHelpBtn.Location = new System.Drawing.Point(12, 27);
|
||||
this.filterHelpBtn.Location = new System.Drawing.Point(14, 31);
|
||||
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.filterHelpBtn.Name = "filterHelpBtn";
|
||||
this.filterHelpBtn.Size = new System.Drawing.Size(22, 23);
|
||||
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
|
||||
this.filterHelpBtn.TabIndex = 3;
|
||||
this.filterHelpBtn.Text = "?";
|
||||
this.filterHelpBtn.UseVisualStyleBackColor = true;
|
||||
@@ -86,9 +87,10 @@
|
||||
// filterBtn
|
||||
//
|
||||
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterBtn.Location = new System.Drawing.Point(776, 27);
|
||||
this.filterBtn.Location = new System.Drawing.Point(905, 31);
|
||||
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.filterBtn.Name = "filterBtn";
|
||||
this.filterBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.filterBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.filterBtn.TabIndex = 2;
|
||||
this.filterBtn.Text = "Filter";
|
||||
this.filterBtn.UseVisualStyleBackColor = true;
|
||||
@@ -96,34 +98,36 @@
|
||||
//
|
||||
// filterSearchTb
|
||||
//
|
||||
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterSearchTb.Location = new System.Drawing.Point(186, 29);
|
||||
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterSearchTb.Location = new System.Drawing.Point(217, 33);
|
||||
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.filterSearchTb.Name = "filterSearchTb";
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(584, 20);
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(681, 23);
|
||||
this.filterSearchTb.TabIndex = 1;
|
||||
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.importToolStripMenuItem,
|
||||
this.liberateToolStripMenuItem,
|
||||
this.exportToolStripMenuItem,
|
||||
this.quickFiltersToolStripMenuItem,
|
||||
this.settingsToolStripMenuItem});
|
||||
this.importToolStripMenuItem,
|
||||
this.liberateToolStripMenuItem,
|
||||
this.exportToolStripMenuItem,
|
||||
this.quickFiltersToolStripMenuItem,
|
||||
this.settingsToolStripMenuItem});
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Size = new System.Drawing.Size(863, 24);
|
||||
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1007, 24);
|
||||
this.menuStrip1.TabIndex = 0;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
// importToolStripMenuItem
|
||||
//
|
||||
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.noAccountsYetAddAccountToolStripMenuItem,
|
||||
this.scanLibraryToolStripMenuItem,
|
||||
this.scanLibraryOfAllAccountsToolStripMenuItem,
|
||||
this.noAccountsYetAddAccountToolStripMenuItem,
|
||||
this.scanLibraryToolStripMenuItem,
|
||||
this.scanLibraryOfAllAccountsToolStripMenuItem,
|
||||
this.scanLibraryOfSomeAccountsToolStripMenuItem});
|
||||
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
|
||||
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
|
||||
@@ -134,6 +138,7 @@
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
|
||||
//
|
||||
// scanLibraryToolStripMenuItem
|
||||
//
|
||||
@@ -159,8 +164,9 @@
|
||||
// liberateToolStripMenuItem
|
||||
//
|
||||
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.beginBookBackupsToolStripMenuItem,
|
||||
this.beginPdfBackupsToolStripMenuItem});
|
||||
this.beginBookBackupsToolStripMenuItem,
|
||||
this.beginPdfBackupsToolStripMenuItem,
|
||||
this.convertAllM4bToMp3ToolStripMenuItem});
|
||||
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
|
||||
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.liberateToolStripMenuItem.Text = "&Liberate";
|
||||
@@ -168,31 +174,46 @@
|
||||
// beginBookBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
|
||||
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// beginPdfBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
|
||||
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// convertAllM4bToMp3ToolStripMenuItem
|
||||
//
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]";
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Visible = false;
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
|
||||
//
|
||||
// exportToolStripMenuItem
|
||||
//
|
||||
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.exportLibraryToolStripMenuItem});
|
||||
this.exportLibraryToolStripMenuItem});
|
||||
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
|
||||
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
|
||||
this.exportToolStripMenuItem.Text = "E&xport";
|
||||
//
|
||||
// exportLibraryToolStripMenuItem
|
||||
//
|
||||
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
|
||||
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
|
||||
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
|
||||
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
|
||||
//
|
||||
// quickFiltersToolStripMenuItem
|
||||
//
|
||||
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.firstFilterIsDefaultToolStripMenuItem,
|
||||
this.editQuickFiltersToolStripMenuItem,
|
||||
this.toolStripSeparator1});
|
||||
this.firstFilterIsDefaultToolStripMenuItem,
|
||||
this.editQuickFiltersToolStripMenuItem,
|
||||
this.toolStripSeparator1});
|
||||
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
|
||||
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
|
||||
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
|
||||
@@ -219,9 +240,8 @@
|
||||
// settingsToolStripMenuItem
|
||||
//
|
||||
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.accountsToolStripMenuItem,
|
||||
this.basicSettingsToolStripMenuItem,
|
||||
this.advancedSettingsToolStripMenuItem});
|
||||
this.accountsToolStripMenuItem,
|
||||
this.basicSettingsToolStripMenuItem});
|
||||
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.settingsToolStripMenuItem.Text = "&Settings";
|
||||
@@ -229,34 +249,28 @@
|
||||
// accountsToolStripMenuItem
|
||||
//
|
||||
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
|
||||
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
|
||||
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
|
||||
this.accountsToolStripMenuItem.Text = "&Accounts...";
|
||||
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
|
||||
//
|
||||
// basicSettingsToolStripMenuItem
|
||||
//
|
||||
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
|
||||
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
|
||||
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings...";
|
||||
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
|
||||
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
|
||||
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
|
||||
//
|
||||
// advancedSettingsToolStripMenuItem
|
||||
//
|
||||
this.advancedSettingsToolStripMenuItem.Name = "advancedSettingsToolStripMenuItem";
|
||||
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
|
||||
this.advancedSettingsToolStripMenuItem.Text = "Ad&vanced Settings...";
|
||||
this.advancedSettingsToolStripMenuItem.Click += new System.EventHandler(this.advancedSettingsToolStripMenuItem_Click);
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.visibleCountLbl,
|
||||
this.springLbl,
|
||||
this.backupsCountsLbl,
|
||||
this.pdfsCountsLbl});
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 445);
|
||||
this.visibleCountLbl,
|
||||
this.springLbl,
|
||||
this.backupsCountsLbl,
|
||||
this.pdfsCountsLbl});
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 517);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Size = new System.Drawing.Size(863, 22);
|
||||
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
|
||||
this.statusStrip1.Size = new System.Drawing.Size(1007, 22);
|
||||
this.statusStrip1.TabIndex = 6;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
@@ -269,7 +283,7 @@
|
||||
// springLbl
|
||||
//
|
||||
this.springLbl.Name = "springLbl";
|
||||
this.springLbl.Size = new System.Drawing.Size(233, 17);
|
||||
this.springLbl.Size = new System.Drawing.Size(375, 17);
|
||||
this.springLbl.Spring = true;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
@@ -286,33 +300,20 @@
|
||||
//
|
||||
// addFilterBtn
|
||||
//
|
||||
this.addFilterBtn.Location = new System.Drawing.Point(40, 27);
|
||||
this.addFilterBtn.Location = new System.Drawing.Point(47, 31);
|
||||
this.addFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.addFilterBtn.Name = "addFilterBtn";
|
||||
this.addFilterBtn.Size = new System.Drawing.Size(140, 23);
|
||||
this.addFilterBtn.Size = new System.Drawing.Size(163, 27);
|
||||
this.addFilterBtn.TabIndex = 4;
|
||||
this.addFilterBtn.Text = "Add To Quick Filters";
|
||||
this.addFilterBtn.UseVisualStyleBackColor = true;
|
||||
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
|
||||
//
|
||||
// noAccountsYetAddAccountToolStripMenuItem
|
||||
//
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
|
||||
//
|
||||
// exportLibraryToolStripMenuItem
|
||||
//
|
||||
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
|
||||
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
|
||||
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
|
||||
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(863, 467);
|
||||
this.ClientSize = new System.Drawing.Size(1007, 539);
|
||||
this.Controls.Add(this.filterBtn);
|
||||
this.Controls.Add(this.addFilterBtn);
|
||||
this.Controls.Add(this.filterSearchTb);
|
||||
@@ -322,8 +323,10 @@
|
||||
this.Controls.Add(this.menuStrip1);
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
this.MainMenuStrip = this.menuStrip1;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "Form1";
|
||||
this.Text = "Libation: Liberate your Library";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
|
||||
this.Load += new System.EventHandler(this.Form1_Load);
|
||||
this.menuStrip1.ResumeLayout(false);
|
||||
this.menuStrip1.PerformLayout();
|
||||
@@ -358,12 +361,12 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem advancedSettingsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem accountsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfAllAccountsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
|
||||
}
|
||||
private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
@@ -33,6 +34,31 @@ namespace LibationWinForms
|
||||
|
||||
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
|
||||
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
|
||||
|
||||
// after backing up formats: can set default/temp visible text
|
||||
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
|
||||
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
|
||||
setVisibleCount(null, 0);
|
||||
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
// independent UI updates
|
||||
this.Load += setBackupCountsAsync;
|
||||
this.Load += (_, __) => RestoreSizeAndLocation();
|
||||
this.Load += (_, __) => RefreshImportMenu();
|
||||
|
||||
// start background service
|
||||
this.Load += (_, __) => startBackgroundImageDownloader();
|
||||
}
|
||||
|
||||
private static void startBackgroundImageDownloader()
|
||||
{
|
||||
// load default/missing cover images. this will also initiate the background image downloader
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
}
|
||||
|
||||
private void Form1_Load(object sender, EventArgs e)
|
||||
@@ -40,29 +66,89 @@ namespace LibationWinForms
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
// load default/missing cover images. this will also initiate the background image downloader
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
RefreshImportMenu();
|
||||
|
||||
setVisibleCount(null, 0);
|
||||
|
||||
reloadGrid();
|
||||
|
||||
reloadGrid();
|
||||
|
||||
// also applies filter. ONLY call AFTER loading grid
|
||||
loadInitialQuickFilterState();
|
||||
|
||||
// init bottom counts
|
||||
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
|
||||
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
|
||||
setBackupCounts(null, null);
|
||||
}
|
||||
|
||||
#region reload grid
|
||||
bool isProcessingGridSelect = false;
|
||||
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
SaveSizeAndLocation();
|
||||
}
|
||||
|
||||
private void RestoreSizeAndLocation()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var width = config.MainFormWidth;
|
||||
var height = config.MainFormHeight;
|
||||
|
||||
// too small -- something must have gone wrong. use defaults
|
||||
if (width < 25 || height < 25)
|
||||
{
|
||||
width = 1023;
|
||||
height = 578;
|
||||
}
|
||||
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (width > Screen.PrimaryScreen.WorkingArea.Width)
|
||||
width = Screen.PrimaryScreen.WorkingArea.Width;
|
||||
if (height > Screen.PrimaryScreen.WorkingArea.Height)
|
||||
height = Screen.PrimaryScreen.WorkingArea.Height;
|
||||
|
||||
var x = config.MainFormX;
|
||||
var y = config.MainFormY;
|
||||
|
||||
var rect = new System.Drawing.Rectangle(x, y, width, height);
|
||||
|
||||
// is proposed rect on a screen?
|
||||
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
||||
{
|
||||
this.StartPosition = FormStartPosition.Manual;
|
||||
this.DesktopBounds = rect;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
|
||||
this.Size = rect.Size;
|
||||
}
|
||||
|
||||
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
||||
this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
||||
}
|
||||
|
||||
private void SaveSizeAndLocation()
|
||||
{
|
||||
System.Drawing.Point location;
|
||||
System.Drawing.Size size;
|
||||
|
||||
// save location and size if the state is normal
|
||||
if (this.WindowState == FormWindowState.Normal)
|
||||
{
|
||||
location = this.Location;
|
||||
size = this.Size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// save the RestoreBounds if the form is minimized or maximized
|
||||
location = this.RestoreBounds.Location;
|
||||
size = this.RestoreBounds.Size;
|
||||
}
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
config.MainFormX = location.X;
|
||||
config.MainFormY = location.Y;
|
||||
|
||||
config.MainFormWidth = size.Width;
|
||||
config.MainFormHeight = size.Height;
|
||||
|
||||
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
|
||||
}
|
||||
|
||||
#region reload grid
|
||||
bool isProcessingGridSelect = false;
|
||||
private void reloadGrid()
|
||||
{
|
||||
// suppressed filter while init'ing UI
|
||||
@@ -84,14 +170,14 @@ namespace LibationWinForms
|
||||
{
|
||||
gridPanel.Controls.Remove(currProductsGrid);
|
||||
currProductsGrid.VisibleCountChanged -= setVisibleCount;
|
||||
currProductsGrid.BackupCountsChanged -= setBackupCounts;
|
||||
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
|
||||
currProductsGrid.Dispose();
|
||||
}
|
||||
|
||||
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
||||
currProductsGrid.VisibleCountChanged += setVisibleCount;
|
||||
currProductsGrid.BackupCountsChanged += setBackupCounts;
|
||||
gridPanel.Controls.Add(currProductsGrid);
|
||||
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
|
||||
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
|
||||
currProductsGrid.Display();
|
||||
}
|
||||
ResumeLayout();
|
||||
@@ -103,45 +189,26 @@ namespace LibationWinForms
|
||||
#endregion
|
||||
|
||||
#region bottom: backup counts
|
||||
private void setBackupCounts(object _, object __)
|
||||
private async void setBackupCountsAsync(object _, object __)
|
||||
{
|
||||
var books = DbContexts.GetContext()
|
||||
.GetLibrary_Flat_NoTracking()
|
||||
.Select(sp => sp.Book)
|
||||
.ToList();
|
||||
LibraryCommands.LibraryStats libraryStats = null;
|
||||
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
|
||||
|
||||
setBookBackupCounts(books);
|
||||
setPdfBackupCounts(books);
|
||||
}
|
||||
enum AudioFileState { full, aax, none }
|
||||
private void setBookBackupCounts(IEnumerable<Book> books)
|
||||
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
|
||||
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
|
||||
}
|
||||
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
|
||||
{
|
||||
static AudioFileState getAudioFileState(string productId)
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(productId))
|
||||
return AudioFileState.full;
|
||||
if (AudibleFileStorage.AAXC.Exists(productId))
|
||||
return AudioFileState.aax;
|
||||
return AudioFileState.none;
|
||||
}
|
||||
|
||||
var results = books
|
||||
.AsParallel()
|
||||
.Select(b => getAudioFileState(b.AudibleProductId))
|
||||
.ToList();
|
||||
var fullyBackedUp = results.Count(r => r == AudioFileState.full);
|
||||
var downloadedOnly = results.Count(r => r == AudioFileState.aax);
|
||||
var noProgress = results.Count(r => r == AudioFileState.none);
|
||||
|
||||
// enable/disable export
|
||||
exportLibraryToolStripMenuItem.Enabled = results.Any();
|
||||
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
|
||||
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
||||
|
||||
// update bottom numbers
|
||||
var pending = noProgress + downloadedOnly;
|
||||
var pending = booksNoProgress + booksDownloadedOnly;
|
||||
var statusStripText
|
||||
= !results.Any() ? "No books. Begin by importing your library"
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, noProgress, downloadedOnly, fullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(fullyBackedUp)} backed up";
|
||||
= !hasResults ? "No books. Begin by importing your library"
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
@@ -149,40 +216,29 @@ namespace LibationWinForms
|
||||
? $"{pending} remaining"
|
||||
: "All books have been liberated";
|
||||
|
||||
Serilog.Log.Logger.Information("Book counts. {@DebugInfo}", new { fullyBackedUp, downloadedOnly, noProgress, pending, statusStripText, menuItemText });
|
||||
|
||||
// update UI
|
||||
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
private void setPdfBackupCounts(IEnumerable<Book> books)
|
||||
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
{
|
||||
var boolResults = books
|
||||
.AsParallel()
|
||||
.Where(b => b.Supplements.Any())
|
||||
.Select(b => AudibleFileStorage.PDF.Exists(b.AudibleProductId))
|
||||
.ToList();
|
||||
var downloaded = boolResults.Count(r => r);
|
||||
var notDownloaded = boolResults.Count(r => !r);
|
||||
|
||||
// update bottom numbers
|
||||
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
var statusStripText
|
||||
= !boolResults.Any() ? ""
|
||||
: notDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, notDownloaded, downloaded)
|
||||
: $"| All {downloaded} PDFs downloaded";
|
||||
= !hasResults ? ""
|
||||
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
|
||||
: $"| All {pdfsDownloaded} PDFs downloaded";
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= notDownloaded > 0
|
||||
? $"{notDownloaded} remaining"
|
||||
= pdfsNotDownloaded > 0
|
||||
? $"{pdfsNotDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
|
||||
Serilog.Log.Logger.Information("PDF counts. {@DebugInfo}", new { downloaded, notDownloaded, statusStripText, menuItemText });
|
||||
|
||||
// update UI
|
||||
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = notDownloaded > 0);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
#endregion
|
||||
@@ -302,6 +358,9 @@ namespace LibationWinForms
|
||||
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
|
||||
|
||||
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
|
||||
|
||||
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
|
||||
#endregion
|
||||
|
||||
@@ -338,8 +397,7 @@ namespace LibationWinForms
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error attempting to export library");
|
||||
MessageBox.Show("Error attempting to export your library. Error message:\r\n\r\n" + ex.Message, "Error exporting", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBoxAlertAdmin.Show("Error attempting to export your library.", "Error exporting", ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -396,31 +454,6 @@ namespace LibationWinForms
|
||||
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
|
||||
|
||||
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
|
||||
|
||||
private void advancedSettingsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
if (libationFilesDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
// no change
|
||||
if (System.IO.Path.GetFullPath(libationFilesDialog.SelectedDirectory).EqualsInsensitive(System.IO.Path.GetFullPath(Configuration.Instance.LibationFiles)))
|
||||
return;
|
||||
|
||||
if (!Configuration.Instance.TrySetLibationFiles(libationFilesDialog.SelectedDirectory))
|
||||
{
|
||||
MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationFilesDialog.SelectedDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBox.Show(
|
||||
"You have changed a file path important for this program. All files will remain in their original location; nothing will be moved. Libation must be restarted so these changes are handled correctly.",
|
||||
"Closing Libation",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Exclamation);
|
||||
Application.Exit();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
@@ -3,42 +3,35 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
internal class GridEntry
|
||||
{
|
||||
private LibraryBook libraryBook;
|
||||
private LibraryBook libraryBook { get; }
|
||||
private Book book => libraryBook.Book;
|
||||
|
||||
public Book GetBook() => book;
|
||||
|
||||
// this special case is obvious and ugly
|
||||
public void REPLACE_Library_Book(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
public LibraryBook GetLibraryBook() => libraryBook;
|
||||
|
||||
public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
|
||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
||||
|
||||
[Browsable(false)]
|
||||
public string AudibleProductId => book.AudibleProductId;
|
||||
[Browsable(false)]
|
||||
public string Tags => book.UserDefinedItem.Tags;
|
||||
[Browsable(false)]
|
||||
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
|
||||
|
||||
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
|
||||
[Browsable(false)]
|
||||
public LiberatedState Liberated_Status
|
||||
=> FileManager.AudibleFileStorage.Audio.Exists(book.AudibleProductId) ? LiberatedState.Liberated
|
||||
: FileManager.AudibleFileStorage.AAXC.Exists(book.AudibleProductId) ? LiberatedState.PartialDownload
|
||||
: LiberatedState.NotDownloaded;
|
||||
|
||||
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
|
||||
public string PictureId => book.PictureId;
|
||||
[Browsable(false)]
|
||||
public PdfState Pdf_Status
|
||||
=> !book.Supplements.Any() ? PdfState.NoPdf
|
||||
: FileManager.AudibleFileStorage.PDF.Exists(book.AudibleProductId) ? PdfState.Downloaded
|
||||
: PdfState.NotDownloaded;
|
||||
public LiberatedState Liberated_Status => LibraryCommands.Liberated_Status(book);
|
||||
[Browsable(false)]
|
||||
public PdfState Pdf_Status => LibraryCommands.Pdf_Status(book);
|
||||
|
||||
// displayValues is what gets displayed
|
||||
// the value that gets returned from the property is the cell's value
|
||||
|
||||
27
LibationWinForms/MessageBoxAlertAdmin.cs
Normal file
27
LibationWinForms/MessageBoxAlertAdmin.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public static class MessageBoxAlertAdmin
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs error. Displays a message box dialog with specified text and caption.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to display in the message box.</param>
|
||||
/// <param name="caption">The text to display in the title bar of the message box.</param>
|
||||
/// <param name="exception">Exception to log</param>
|
||||
/// <returns>One of the System.Windows.Forms.DialogResult values.</returns>
|
||||
public static System.Windows.Forms.DialogResult Show(string text, string caption, Exception exception)
|
||||
{
|
||||
Serilog.Log.Logger.Error(exception, "Alert admin error: {@DebugText}", new { text, caption });
|
||||
|
||||
using var form = new MessageBoxAlertAdminDialog(text, caption, exception);
|
||||
return form.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
LibationWinForms/MessageBoxWarnIfVerboseLogging.cs
Normal file
27
LibationWinForms/MessageBoxWarnIfVerboseLogging.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public static class MessageBoxVerboseLoggingWarning
|
||||
{
|
||||
public static void ShowIfTrue()
|
||||
{
|
||||
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
|
||||
if (Log.Logger.IsVerboseEnabled())
|
||||
MessageBox.Show(@"
|
||||
Warning: verbose logging is enabled.
|
||||
|
||||
This should be used for debugging only. It creates many
|
||||
more logs and debug files, neither of which are as
|
||||
strictly anonomous.
|
||||
|
||||
When you are finished debugging, it's highly recommended
|
||||
to set your debug MinimumLevel to Information and restart
|
||||
Libation.
|
||||
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
@@ -35,21 +36,29 @@ namespace LibationWinForms
|
||||
// alias
|
||||
private DataGridView dataGridView => gridEntryDataGridView;
|
||||
|
||||
private LibationContext context;
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
formatDataGridView();
|
||||
addLiberateButtons();
|
||||
addEditTagsButtons();
|
||||
formatColumns();
|
||||
Disposed += (_, __) => context?.Dispose();
|
||||
formatDataGridView();
|
||||
addLiberateButtons();
|
||||
addEditTagsButtons();
|
||||
formatColumns();
|
||||
|
||||
manageLiveImageUpdateSubscriptions();
|
||||
|
||||
enableDoubleBuffering();
|
||||
}
|
||||
|
||||
private void formatDataGridView()
|
||||
private void enableDoubleBuffering()
|
||||
{
|
||||
var propertyInfo = dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
|
||||
//var before = (bool)propertyInfo.GetValue(dataGridView);
|
||||
propertyInfo.SetValue(dataGridView, true, null);
|
||||
//var after = (bool)propertyInfo.GetValue(dataGridView);
|
||||
}
|
||||
|
||||
private void formatDataGridView()
|
||||
{
|
||||
dataGridView.Dock = DockStyle.Fill;
|
||||
dataGridView.AllowUserToAddRows = false;
|
||||
@@ -125,25 +134,25 @@ namespace LibationWinForms
|
||||
{
|
||||
var libState = liberatedStatus switch
|
||||
{
|
||||
GridEntry.LiberatedState.Liberated => "Liberated",
|
||||
GridEntry.LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
|
||||
GridEntry.LiberatedState.NotDownloaded => "Book NOT downloaded",
|
||||
LiberatedState.Liberated => "Liberated",
|
||||
LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
|
||||
LiberatedState.NotDownloaded => "Book NOT downloaded",
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
|
||||
var pdfState = pdfStatus switch
|
||||
{
|
||||
GridEntry.PdfState.Downloaded => "\r\nPDF downloaded",
|
||||
GridEntry.PdfState.NotDownloaded => "\r\nPDF NOT downloaded",
|
||||
GridEntry.PdfState.NoPdf => "",
|
||||
PdfState.Downloaded => "\r\nPDF downloaded",
|
||||
PdfState.NotDownloaded => "\r\nPDF NOT downloaded",
|
||||
PdfState.NoPdf => "",
|
||||
_ => throw new Exception("Unexpected PDF state")
|
||||
};
|
||||
|
||||
var text = libState + pdfState;
|
||||
|
||||
if (liberatedStatus == GridEntry.LiberatedState.NotDownloaded ||
|
||||
liberatedStatus == GridEntry.LiberatedState.PartialDownload ||
|
||||
pdfStatus == GridEntry.PdfState.NotDownloaded)
|
||||
if (liberatedStatus == LiberatedState.NotDownloaded ||
|
||||
liberatedStatus == LiberatedState.PartialDownload ||
|
||||
pdfStatus == PdfState.NotDownloaded)
|
||||
text += "\r\nClick to complete";
|
||||
|
||||
//DEBUG//cell.Value = text;
|
||||
@@ -153,14 +162,14 @@ namespace LibationWinForms
|
||||
// draw img
|
||||
{
|
||||
var image_lib
|
||||
= liberatedStatus == GridEntry.LiberatedState.NotDownloaded ? "red"
|
||||
: liberatedStatus == GridEntry.LiberatedState.PartialDownload ? "yellow"
|
||||
: liberatedStatus == GridEntry.LiberatedState.Liberated ? "green"
|
||||
= liberatedStatus == LiberatedState.NotDownloaded ? "red"
|
||||
: liberatedStatus == LiberatedState.PartialDownload ? "yellow"
|
||||
: liberatedStatus == LiberatedState.Liberated ? "green"
|
||||
: throw new Exception("Unexpected liberation state");
|
||||
var image_pdf
|
||||
= pdfStatus == GridEntry.PdfState.NoPdf ? ""
|
||||
: pdfStatus == GridEntry.PdfState.NotDownloaded ? "_pdf_no"
|
||||
: pdfStatus == GridEntry.PdfState.Downloaded ? "_pdf_yes"
|
||||
= pdfStatus == PdfState.NoPdf ? ""
|
||||
: pdfStatus == PdfState.NotDownloaded ? "_pdf_no"
|
||||
: pdfStatus == PdfState.Downloaded ? "_pdf_yes"
|
||||
: throw new Exception("Unexpected PDF state");
|
||||
var image = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
|
||||
drawImage(e, image);
|
||||
@@ -171,24 +180,26 @@ namespace LibationWinForms
|
||||
{
|
||||
if (!isColumnValid(e, LIBERATE))
|
||||
return;
|
||||
|
||||
var productId = getGridEntry(e.RowIndex).GetBook().AudibleProductId;
|
||||
|
||||
var libraryBook = getGridEntry(e.RowIndex).GetLibraryBook();
|
||||
|
||||
// liberated: open explorer to file
|
||||
if (FileManager.AudibleFileStorage.Audio.Exists(productId))
|
||||
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
||||
{
|
||||
var filePath = FileManager.AudibleFileStorage.Audio.GetPath(productId);
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select, \"{filePath}\"");
|
||||
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
if (!Go.To.File(filePath))
|
||||
MessageBox.Show($"File not found:\r\n{filePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
|
||||
// else: liberate
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void RefreshRow(string productId)
|
||||
{
|
||||
var rowId = getRowId((ge) => ge.GetBook().AudibleProductId == productId);
|
||||
var rowId = getRowId((ge) => ge.AudibleProductId == productId);
|
||||
|
||||
// update cells incl Liberate button text
|
||||
dataGridView.InvalidateRow(rowId);
|
||||
@@ -244,11 +255,11 @@ namespace LibationWinForms
|
||||
// EditTagsDialog should display better-formatted title
|
||||
liveGridEntry.TryDisplayValue(nameof(liveGridEntry.Title), out string value);
|
||||
|
||||
var editTagsForm = new EditTagsDialog(value, liveGridEntry.Tags);
|
||||
if (editTagsForm.ShowDialog() != DialogResult.OK)
|
||||
var bookDetailsForm = new BookDetailsDialog(value, liveGridEntry.Tags);
|
||||
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
|
||||
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.GetBook(), bookDetailsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
return;
|
||||
|
||||
@@ -319,7 +330,7 @@ namespace LibationWinForms
|
||||
=> dataGridView.UIThread(() => updateRowImage(pictureId));
|
||||
private void updateRowImage(string pictureId)
|
||||
{
|
||||
var rowId = getRowId((ge) => ge.GetBook().PictureId == pictureId);
|
||||
var rowId = getRowId((ge) => ge.PictureId == pictureId);
|
||||
if (rowId > -1)
|
||||
dataGridView.InvalidateRow(rowId);
|
||||
}
|
||||
@@ -335,8 +346,8 @@ namespace LibationWinForms
|
||||
//
|
||||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
context = DbContexts.GetContext();
|
||||
var lib = context.GetLibrary_Flat_WithTracking();
|
||||
using var context = DbContexts.GetContext();
|
||||
var lib = context.GetLibrary_Flat_NoTracking();
|
||||
|
||||
// if no data. hide all columns. return
|
||||
if (!lib.Any())
|
||||
@@ -387,7 +398,7 @@ namespace LibationWinForms
|
||||
currencyManager.SuspendBinding();
|
||||
{
|
||||
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
|
||||
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).GetBook().AudibleProductId);
|
||||
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
||||
}
|
||||
currencyManager.ResumeBinding();
|
||||
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
* Powerful advanced search built on the Lucene search engine
|
||||
* Customizable saved filters for common searches
|
||||
* Open source
|
||||
* Tested on US Audible only. Should theoretically also work for Canada, UK, Germany, France, and Australia
|
||||
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
|
||||
|
||||
<a name="theBad"/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user