mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 09:58:43 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -21,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;
|
||||
@@ -32,19 +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");
|
||||
@@ -53,8 +53,6 @@ namespace AaxDecrypter
|
||||
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
OutputFormat = outputFormat;
|
||||
|
||||
Serilog.Log.Logger.Verbose("Download License. {@DebugInfo}", downloadLicense);
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + (outputFormat == OutputFormat.Mp4a ? "M4b" : "Mp3"),
|
||||
@@ -127,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);
|
||||
}
|
||||
|
||||
@@ -139,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);
|
||||
|
||||
@@ -161,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();
|
||||
@@ -193,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)
|
||||
{
|
||||
@@ -207,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -67,35 +69,15 @@ namespace FileLiberator
|
||||
{
|
||||
validate(libraryBook);
|
||||
|
||||
Serilog.Log.Logger.Debug("Trying to debug mysterious null ref exception {@DebugInfo}", new {
|
||||
libraryBookIsNull = libraryBook is null,
|
||||
BookIsNull = libraryBook?.Book is null,
|
||||
libraryBook?.Book?.Locale,
|
||||
libraryBook?.Book?.AudibleProductId,
|
||||
AccountLength = libraryBook?.Account?.Length
|
||||
});
|
||||
|
||||
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
Serilog.Log.Logger.Debug("Trying to debug mysterious null ref exception {@DebugInfo}", new { apiIsNull = api is null });
|
||||
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
Serilog.Log.Logger.Debug("Trying to debug mysterious null ref exception {@DebugInfo}", new {
|
||||
contentLicIsNull = contentLic is null,
|
||||
contentMetadataIsNull = contentLic?.ContentMetadata is null,
|
||||
voucherIsNull = contentLic?.Voucher is null,
|
||||
keyIsNull = contentLic?.Voucher?.Key is null,
|
||||
keyLength = contentLic?.Voucher?.Key?.Length,
|
||||
ivIsNull = contentLic?.Voucher?.Iv is null,
|
||||
ivLength = contentLic?.Voucher?.Iv?.Length,
|
||||
});
|
||||
|
||||
var aaxcDecryptDlLic = new DownloadLicense
|
||||
(
|
||||
contentLic.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic.Voucher?.Key,
|
||||
contentLic.Voucher?.Iv,
|
||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic?.Voucher?.Key,
|
||||
contentLic?.Voucher?.Iv,
|
||||
Resources.UserAgent
|
||||
);
|
||||
|
||||
@@ -117,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;
|
||||
@@ -133,7 +115,7 @@ namespace FileLiberator
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
return aaxcDownloader.OutputFileName;
|
||||
return outFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -190,6 +172,8 @@ namespace FileLiberator
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
@@ -235,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 "=>"
|
||||
|
||||
140
FileManager/BackgroundFileSystem.cs
Normal file
140
FileManager/BackgroundFileSystem.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.3.8.1</Version>
|
||||
<Version>5.4.8.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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;
|
||||
@@ -51,6 +54,7 @@ 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);
|
||||
@@ -141,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)))
|
||||
@@ -229,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)
|
||||
@@ -291,7 +361,7 @@ namespace LibationLauncher
|
||||
config.ConfigureLogging();
|
||||
|
||||
// Fwd Console to serilog.
|
||||
// Serilog also write to Console (should probably change this) so it might be asking for trouble.
|
||||
// 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()));
|
||||
@@ -308,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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,9 +13,9 @@ namespace LibationWinForms.BookLiberation
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -51,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();
|
||||
|
||||
@@ -80,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
|
||||
@@ -124,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;
|
||||
@@ -136,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;
|
||||
@@ -176,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(() =>
|
||||
@@ -190,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
|
||||
@@ -258,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(
|
||||
@@ -278,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));
|
||||
@@ -467,6 +483,7 @@ $@" Title: {libraryBook.Book.Title}
|
||||
|
||||
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
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -63,26 +63,29 @@ namespace LibationWinForms.Dialogs.Login
|
||||
public string SelectedValue { get; private set; }
|
||||
private void submitBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Serilog.Log.Logger.Information("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_visible = radioButton2.Visible,
|
||||
r21_checked = radioButton2.Checked,
|
||||
rb2_visible = radioButton2.Visible,
|
||||
rb2_checked = radioButton2.Checked,
|
||||
|
||||
rb3_visible = radioButton3.Visible,
|
||||
rb3_checked = radioButton3.Checked
|
||||
rb3_checked = radioButton3.Checked,
|
||||
|
||||
isSelected = selected is not null,
|
||||
name = selected?.Name,
|
||||
value = selected?.Tag
|
||||
});
|
||||
|
||||
var selected = radioButtons.FirstOrDefault(rb => rb.Checked);
|
||||
if (selected is null)
|
||||
{
|
||||
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Selected: {@DebugInfo}", new { isSelected = selected is not null, name = selected?.Name, value = selected?.Tag });
|
||||
|
||||
SelectedName = selected.Name;
|
||||
SelectedValue = (string)selected.Tag;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
19
LibationWinForms/Form1.Designer.cs
generated
19
LibationWinForms/Form1.Designer.cs
generated
@@ -42,6 +42,7 @@
|
||||
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();
|
||||
@@ -164,7 +165,8 @@
|
||||
//
|
||||
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.beginBookBackupsToolStripMenuItem,
|
||||
this.beginPdfBackupsToolStripMenuItem});
|
||||
this.beginPdfBackupsToolStripMenuItem,
|
||||
this.convertAllM4bToMp3ToolStripMenuItem});
|
||||
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
|
||||
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.liberateToolStripMenuItem.Text = "&Liberate";
|
||||
@@ -172,17 +174,25 @@
|
||||
// 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[] {
|
||||
@@ -357,5 +367,6 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
|
||||
}
|
||||
private System.Windows.Forms.ToolStripMenuItem convertAllM4bToMp3ToolStripMenuItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,45 +191,24 @@ namespace LibationWinForms
|
||||
#region bottom: backup counts
|
||||
private async void setBackupCountsAsync(object _, object __)
|
||||
{
|
||||
await Task.Run(() => {
|
||||
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
|
||||
@@ -237,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
|
||||
@@ -390,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
|
||||
|
||||
@@ -483,6 +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();
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,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;
|
||||
@@ -126,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;
|
||||
@@ -154,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);
|
||||
@@ -172,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);
|
||||
Go.To.File(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);
|
||||
@@ -245,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;
|
||||
|
||||
@@ -320,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);
|
||||
}
|
||||
@@ -336,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())
|
||||
@@ -388,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));
|
||||
|
||||
Reference in New Issue
Block a user