Compare commits

...

52 Commits

Author SHA1 Message Date
Robert McRackan
f16ad30891 bug fix from my last bug fix :( 2021-08-24 09:28:13 -04:00
Robert McRackan
ef53a6a8cb Bug fix: issue #92 2021-08-23 16:29:23 -04:00
Robert McRackan
9a37d434f1 FileLiberator is not db ignorant. It doesn't make context calls but still heavily uses the classes defined in the domain. Also uses internal util.s 2021-08-23 16:27:22 -04:00
Robert McRackan
d7eb190f69 reduce use of Book.Supplements 2021-08-23 16:16:08 -04:00
Robert McRackan
f19c46ee45 Book: add pdf url as is, not Absolute 2021-08-23 16:07:19 -04:00
rmcrackan
343c3b62d6 Merge pull request #90 from Mbucari/master
Fully implemented the MVVM pattern
2021-08-22 21:06:00 -04:00
Michael Bucari-Tovo
b1de10a71a Fix filtering. 2021-08-22 13:29:01 -06:00
Michael Bucari-Tovo
6beb5cc74a Made changes discussed. 2021-08-22 13:27:39 -06:00
Michael Bucari-Tovo
3767c3574a Merge branch 'master' of https://github.com/Mbucari/Libation 2021-08-21 22:09:27 -06:00
Michael Bucari-Tovo
4ceb4f9c03 Change back. 2021-08-21 22:09:13 -06:00
Mbucari
0f5149f7b4 Merge pull request #2 from rmcrackan/master
bug fix. race condition. can't check for book-exists-state before set…
2021-08-21 22:08:51 -06:00
Michael Bucari-Tovo
673451dc11 Git resolve 2021-08-21 22:08:35 -06:00
Michael Bucari-Tovo
e4257afc14 Version Num 2021-08-21 22:06:54 -06:00
Michael Bucari-Tovo
2a7e185dc3 Finish MVVM conversion 2021-08-21 22:03:16 -06:00
Michael Bucari-Tovo
9e06c343c1 Don't check if values have changed when updating the database. 2021-08-21 21:15:25 -06:00
Michael Bucari-Tovo
40b3a9990d FileLiberator is now DB ignorant. IProcessables update UserDaefinedData which notifies the view model. 2021-08-21 20:49:54 -06:00
Robert McRackan
d66c112a1e bug fix. race condition. can't check for book-exists-state before setting this state 2021-08-21 22:32:45 -04:00
Michael Bucari-Tovo
d826885728 Fix display for new LiberatedStatus values. 2021-08-21 18:37:07 -06:00
Michael Bucari-Tovo
263222d8cc Changed method signature. 2021-08-21 18:21:22 -06:00
Michael Bucari-Tovo
f25734334d Add separate command for updating tags 2021-08-21 18:16:24 -06:00
Michael Bucari-Tovo
ede8397f13 Needed to add check for actual file since Audio_Exists is now an application state. 2021-08-21 18:15:39 -06:00
Michael Bucari-Tovo
1369ee575a Replaced LiberatedState with LiberatedStatus and PdfState with LiberatedStatus? 2021-08-21 16:29:16 -06:00
Robert McRackan
c8e2418af7 incr ver for new liberation status handling 2021-08-21 14:04:48 -04:00
rmcrackan
2da25edafd Merge pull request #87 from Mbucari/master
Make sure DataGridView updates the display immediately after Details are changed.
2021-08-21 13:37:05 -04:00
Michael Bucari-Tovo
f60964f4c7 Unsubscribe IStreamable events from Disposed instead of FormClosed. 2021-08-21 08:47:24 -06:00
Michael Bucari-Tovo
3183f99153 Remove unnecessary overrides. 2021-08-21 08:45:43 -06:00
Michael Bucari-Tovo
2a22cff67c Revert "Fixed PDF download form disposed error"
This reverts commit 7fbe8ae769.
2021-08-21 08:14:10 -06:00
Michael Bucari-Tovo
7fbe8ae769 Fixed PDF download form disposed error 2021-08-21 08:03:40 -06:00
Robert McRackan
f9df466ad8 retain seeded file locations 2021-08-21 09:59:06 -04:00
Michael Bucari-Tovo
0b129fcf7c Fixed NetworkFileStream not resuming from cancellation. 2021-08-20 21:05:29 -06:00
Robert McRackan
2be5fd5af3 Omit '.libhack' skip/error files 2021-08-20 22:16:46 -04:00
Robert McRackan
c9727f84ab (hopefully) complete minimum viable product with stateful is-liberated status 2021-08-20 21:22:52 -04:00
Robert McRackan
aa56bb74a1 refactor out most of TransitionalFileLocator. Almost done with new stateful is-liberated paradigm 2021-08-20 20:51:37 -04:00
Michael Bucari-Tovo
85a6e21dcf Make sure network file isn't left open. 2021-08-20 17:03:15 -06:00
Michael Bucari-Tovo
8c620c25ab Separate concerns. 2021-08-20 16:10:05 -06:00
Michael Bucari-Tovo
813d91dfa4 Better naming 2021-08-20 15:40:16 -06:00
Michael Bucari-Tovo
d0d66c6135 Update using NotifyPropertyChanged instead of Row.Invalidate 2021-08-20 15:38:30 -06:00
Michael Bucari-Tovo
a8d609676e Null check. 2021-08-20 14:57:23 -06:00
Michael Bucari-Tovo
8386da5ec6 Make gridview update the row after details changed. 2021-08-20 14:56:52 -06:00
Michael Bucari-Tovo
f5089e7e29 Use local rowIndex instead of DataGridViewCell.RowIndex 2021-08-20 14:53:12 -06:00
Robert McRackan
a639857ec6 Book details changes liberated status in db and search engine. Minor changes to audible api to hopefully fix the weird log-in edge cases 2021-08-20 16:06:01 -04:00
Robert McRackan
35b5d7370c book details form now has a way for user to toggle is vs is-not liberated for book and pdf. NOT yet wired up 2021-08-20 15:21:43 -04:00
Robert McRackan
c9f988acf8 Book details form: quick and dirty 1st draft 2021-08-20 14:45:28 -04:00
Robert McRackan
6dfef09ea3 begin process of changing 'edit tags' => book details 2021-08-20 13:26:12 -04:00
rmcrackan
7e288c0c08 Merge pull request #86 from Mbucari/master
Use new Dinah.Core.Threading and remove unnecessary DataGridViewImageButtonColumn
2021-08-19 17:32:00 -04:00
Mbucari
dbcf6f25db Merge branch 'rmcrackan:master' into master 2021-08-19 15:14:56 -06:00
Michael Bucari-Tovo
0cc55fd1e8 Widen Liberate column so sort arrow is shown. 2021-08-18 15:53:35 -06:00
Michael Bucari-Tovo
e36ea70cd1 Removed unnecessary class and simplified. 2021-08-18 15:53:09 -06:00
Michael Bucari-Tovo
a86185e644 Updated to use new Dinah.Core.Threading 2021-08-18 14:29:25 -06:00
Michael Bucari-Tovo
64a8f007a5 Use new Dinah.Core string extensions. 2021-08-18 14:11:25 -06:00
Michael Bucari-Tovo
215a626c92 SynchronizeInvoker => Dinah.Core.Threading.SynchronizeInvoker 2021-08-18 14:08:48 -06:00
Michael Bucari-Tovo
de93047192 Use new Dinah.Core.Threading 2021-08-18 14:08:11 -06:00
45 changed files with 1274 additions and 677 deletions

View File

@@ -224,6 +224,8 @@ namespace AaxDecrypter
isCanceled = true;
aaxFile?.Cancel();
aaxFile?.Dispose();
nfsPersister?.NetworkFileStream?.Close();
nfsPersister?.Dispose();
}
}
}

View File

@@ -241,9 +241,12 @@ namespace AaxDecrypter
} while (downloadPosition < ContentLength && !isCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
_networkStream.Close();
downloadedPiece.Set();
downloadEnded.Set();
if (!isCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@@ -251,7 +254,6 @@ namespace AaxDecrypter
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
downloadEnded.Set();
}
#endregion

View File

@@ -11,13 +11,6 @@ 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
{
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
@@ -162,83 +155,24 @@ namespace ApplicationServices
#endregion
#region Update book details
public static int UpdateTags(Book book, string newTags)
public static int UpdateUserDefinedItem(Book book)
{
try
{
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)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
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;
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
SearchEngineCommands.UpdateLiberatedStatus(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");
Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
throw;
}
}
@@ -246,15 +180,15 @@ namespace ApplicationServices
// 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 LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists ? LiberatedStatus.Liberated
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
public static PdfState Pdf_Status(Book book)
=> !book.Supplements.Any() ? PdfState.NoPdf
: TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded
: PdfState.NotDownloaded;
public static LiberatedStatus? Pdf_Status(Book book)
=> !book.HasPdf ? null
: book.PDF_Exists ? LiberatedStatus.Liberated
: LiberatedStatus.NotLiberated;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
public static LibraryStats GetCounts()
@@ -265,19 +199,19 @@ namespace ApplicationServices
.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);
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
var boolResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.Supplements.Any())
.Where(lb => lb.Book.HasPdf)
.Select(lb => Pdf_Status(lb.Book))
.ToList();
var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded);
var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded);
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });

View File

@@ -47,8 +47,8 @@ namespace ApplicationServices
[Name("Publisher")]
public string Publisher { get; set; }
[Name("Pdf url")]
public string PdfUrl { get; set; }
[Name("Has PDF")]
public bool HasPdf { get; set; }
[Name("Series Names")]
public string SeriesNames { get; set; }
@@ -92,9 +92,6 @@ namespace ApplicationServices
[Name("Book Liberation Status")]
public string BookStatus { get; set; }
[Name("Book File Location")]
public string BookLocation { get; set; }
[Name("PDF Liberation Status")]
public string PdfStatus { get; set; }
}
@@ -112,7 +109,7 @@ namespace ApplicationServices
NarratorNames = a.Book.NarratorNames,
LengthInMinutes = a.Book.LengthInMinutes,
Publisher = a.Book.Publisher,
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
HasPdf = a.Book.HasPdf,
SeriesNames = a.Book.SeriesNames,
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
@@ -127,7 +124,6 @@ namespace ApplicationServices
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags,
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
BookLocation = a.Book.UserDefinedItem.BookLocation,
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString()
}).ToList();
}
@@ -186,7 +182,7 @@ namespace ApplicationServices
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Publisher),
nameof (ExportDto.PdfUrl),
nameof (ExportDto.HasPdf),
nameof (ExportDto.SeriesNames),
nameof (ExportDto.SeriesOrder),
nameof (ExportDto.CommunityRatingOverall),
@@ -201,7 +197,6 @@ namespace ApplicationServices
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags),
nameof (ExportDto.BookStatus),
nameof (ExportDto.BookLocation),
nameof (ExportDto.PdfStatus)
};
var col = 0;
@@ -239,7 +234,7 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.PdfUrl);
row.CreateCell(col++).SetCellValue(dto.HasPdf);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
@@ -265,7 +260,6 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.BookLocation);
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
rowIndex++;

View File

@@ -1,46 +0,0 @@
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);
}
}
}

View File

@@ -51,6 +51,24 @@ namespace DataLayer
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
// UserDefinedItem convenience properties
public bool Audio_Exists
{
get
{
var status = UserDefinedItem?.BookStatus;
return status.HasValue && status.Value != LiberatedStatus.NotLiberated;
}
}
public bool PDF_Exists
{
get
{
var status = UserDefinedItem?.PdfStatus;
return (status.HasValue && status.Value == LiberatedStatus.Liberated);
}
}
// is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0);

View File

@@ -14,7 +14,11 @@ namespace DataLayer
NotLiberated = 0,
Liberated = 1,
/// <summary>Error occurred during liberation. Don't retry</summary>
Error = 2
Error = 2,
/// <summary>Application-state only. Not a valid persistence state.</summary>
PartialDownload = 0x1000
}
public class UserDefinedItem
@@ -38,7 +42,15 @@ namespace DataLayer
public string Tags
{
get => _tags;
set => _tags = sanitize(value);
set
{
var newTags = sanitize(value);
if (_tags != newTags)
{
_tags = newTags;
ItemChanged?.Invoke(this, nameof(Tags));
}
}
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
@@ -94,12 +106,39 @@ namespace DataLayer
=> Rating.Update(overallRating, performanceRating, storyRating);
#endregion
#region LiberatedStatuses and book file location
public LiberatedStatus BookStatus { get; set; }
public string BookLocation { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
#endregion
#region LiberatedStatuses
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public LiberatedStatus BookStatus
{
get => _bookStatus;
set
{
if (_bookStatus != value)
{
_bookStatus = value;
ItemChanged?.Invoke(this, nameof(BookStatus));
}
}
}
public LiberatedStatus? PdfStatus
{
get => _pdfStatus;
set
{
if (_pdfStatus != value)
{
_pdfStatus = value;
ItemChanged?.Invoke(this, nameof(PdfStatus));
}
}
}
#endregion
/// <summary>
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
/// </summary>
public static event EventHandler<string> ItemChanged;
public override string ToString() => $"{Book} {Rating} {Tags}";
}
}

View File

@@ -0,0 +1,387 @@
// <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("20210821012137_RemoveUdiBookLocation")]
partial class RemoveUdiBookLocation
{
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<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
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class RemoveUdiBookLocation : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BookLocation",
table: "UserDefinedItem");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BookLocation",
table: "UserDefinedItem",
type: "TEXT",
nullable: true);
}
}
}

View File

@@ -253,9 +253,6 @@ namespace DataLayer.Migrations
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("BookLocation")
.HasColumnType("TEXT");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");

View File

@@ -120,8 +120,8 @@ namespace DtoImporterService
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
if (item.PdfUrl is not null)
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
return book;
}

View File

@@ -37,7 +37,7 @@ namespace FileLiberator
public bool Validate(LibraryBook libraryBook)
{
var path = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
@@ -49,7 +49,7 @@ namespace FileLiberator
try
{
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;

View File

@@ -16,28 +16,28 @@ namespace FileLiberator
public class DownloadDecryptBook : IAudioDecodable
{
private AaxcDownloadConverter aaxcDownloader;
private AaxcDownloadConverter aaxcDownloader;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
try
{
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book))
if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
@@ -47,14 +47,12 @@ namespace FileLiberator
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
if (!finalAudioExists)
if (!moveResults.movedAudioFile)
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);
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
return new StatusHandler();
}
@@ -127,7 +125,7 @@ namespace FileLiberator
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is null && Configuration.Instance.AllowLibationFixup)
{
@@ -144,10 +142,10 @@ namespace FileLiberator
{
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
@@ -162,6 +160,7 @@ namespace FileLiberator
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
bool movedAudioFile = false;
foreach (var f in sortedFiles)
{
var dest
@@ -174,11 +173,14 @@ namespace FileLiberator
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
}
AudibleFileStorage.Audio.Refresh();
return destinationDir;
return (destinationDir, movedAudioFile);
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
@@ -201,29 +203,28 @@ namespace FileLiberator
}
private static void validate(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
public bool Validate(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public void Cancel()
{

View File

@@ -15,7 +15,7 @@ namespace FileLiberator
{
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book);
&& !libraryBook.Book.PDF_Exists;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
@@ -23,8 +23,7 @@ namespace FileLiberator
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(libraryBook);
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
return result;
}
@@ -32,14 +31,14 @@ namespace FileLiberator
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var existingPath = Path.GetDirectoryName(ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book));
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
var file = getdownloadUrl(libraryBook);
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
AudibleFileStorage.PDF.StorageDirectory,
AudibleFileStorage.PdfStorageDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
@@ -61,7 +60,7 @@ namespace FileLiberator
}
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book)
=> !libraryBook.Book.PDF_Exists
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}

View File

@@ -7,7 +7,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@@ -18,10 +17,8 @@ namespace FileLiberator
// when used in foreach: stateful. deferred execution
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
=> DbContexts.GetContext()
.GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook));
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
=> library.Where(libraryBook => processable.Validate(libraryBook));
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
{

View File

@@ -15,14 +15,16 @@ namespace FileManager
protected abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string PdfStorageDirectory => BooksDirectory;
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
#region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage();
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string BooksDirectory
{
@@ -34,13 +36,13 @@ namespace FileManager
}
}
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion
#region instance
public FileType FileType => (FileType)Value;
private IEnumerable<string> extensions_noDots { get; }
protected IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
@@ -50,39 +52,25 @@ namespace FileManager
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
}
public void Refresh()
{
BookDirectoryFiles.RefreshFiles();
}
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetPath(productId) != null;
public string GetPath(string productId)
protected string GetFilePath(string productId)
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
string storageDir = StorageDirectory;
string regexPattern = $@"{productId}.*?\.({extAggr})$";
string firstOrNull;
if (storageDir == BooksDirectory)
if (StorageDirectory == BooksDirectory)
{
//If user changed the BooksDirectory, reinitialize.
if (storageDir != BookDirectoryFiles.RootDirectory)
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
{
lock (BookDirectoryFiles)
{
if (storageDir != BookDirectoryFiles.RootDirectory)
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
{
BookDirectoryFiles = new BackgroundFileSystem(storageDir, "*.*", SearchOption.AllDirectories);
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
}
}
}
@@ -93,7 +81,7 @@ namespace FileManager
{
firstOrNull =
Directory
.EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories)
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
}
@@ -103,6 +91,21 @@ namespace FileManager
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
}
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public void Refresh() => BookDirectoryFiles.RefreshFiles();
public string GetDestDir(string title, string asin)
{
@@ -118,32 +121,8 @@ namespace FileManager
public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
public const string SKIP_FILE_EXT = "libhack";
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 "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public string CreateSkipFile(string title, string asin, string contents = null)
{
var destinationDir = GetDestDir(title, asin);
Directory.CreateDirectory(destinationDir);
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents ?? string.Empty);
return path;
}
public string GetPath(string productId) => GetFilePath(productId);
}
public class AaxcFileStorage : AudibleFileStorage
@@ -156,17 +135,13 @@ namespace FileManager
public override string StorageDirectory => DownloadsInProgress;
public AaxcFileStorage() : base(FileType.AAXC) { }
}
public class PdfFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "pdf", "zip" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public PdfFileStorage() : base(FileType.PDF) { }
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetFilePath(productId) != null;
}
}

View File

@@ -9,7 +9,14 @@ using System.Threading.Tasks;
namespace FileManager
{
class BackgroundFileSystem
/// <summary>
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
///
/// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
/// this state and download again.
/// </summary>
internal class BackgroundFileSystem
{
public string RootDirectory { get; private set; }
public string SearchPattern { get; private set; }

View File

@@ -9,6 +9,7 @@ namespace FileManager
{
public static class FilePathCache
{
private const string FILENAME = "FileLocations.json";
internal class CacheEntry
{
public string Id { get; set; }
@@ -18,7 +19,7 @@ namespace FileManager
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
static FilePathCache()
{
@@ -84,7 +85,7 @@ namespace FileManager
try { resave(); }
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
throw;
}
}

View File

@@ -86,7 +86,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.T
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.5.0.12</Version>
<Version>5.5.5.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -5,13 +5,14 @@ using System.Linq;
using System.Windows.Forms;
using AudibleApi.Authorization;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using FileManager;
using InternalUtilities;
using LibationWinForms;
using LibationWinForms.Dialogs;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Serilog;
@@ -52,7 +53,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
migrate_to_v5_5_0(config);
ensureSerilogConfig(config);
configureLogging(config);
@@ -233,25 +234,26 @@ 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)
#region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_5_0(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_5_0_thread(Configuration config)
{
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
if (!File.Exists(filePaths))
return;
var fileLocations = Path.Combine(config.LibationFiles, "FileLocations.json");
if (!File.Exists(fileLocations))
File.Copy(filePaths, fileLocations);
// files to be deleted at the end
var libhackFilesToDelete = new List<string>();
// .libhack files => errors
var libhackFiles = Directory.EnumerateDirectories(config.Books, "*.libhack", SearchOption.AllDirectories);
using var context = ApplicationServices.DbContexts.GetContext();
context.Books.Load();
@@ -283,19 +285,29 @@ namespace LibationLauncher
if (fileType == FileType.Audio)
{
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book.UserDefinedItem.BookLocation = path;
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
if (lhack is null)
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
else
{
book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
libhackFilesToDelete.Add(lhack);
}
}
}
context.SaveChanges();
// only do this after save changes
foreach (var libhackFile in libhackFilesToDelete)
File.Delete(libhackFile);
File.Delete(filePaths);
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
}
debugStopwatch.Stop();
var debugTotal = debugStopwatch.Elapsed;
}
#endregion

View File

@@ -107,14 +107,14 @@ namespace LibationSearchEngine
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
new Dictionary<string, Func<LibraryBook, bool>>
{
["HasDownloads"] = lb => lb.Book.Supplements.Any(),
["HasDownload"] = lb => lb.Book.Supplements.Any(),
["Downloads"] = lb => lb.Book.Supplements.Any(),
["Download"] = lb => lb.Book.Supplements.Any(),
["HasPDFs"] = lb => lb.Book.Supplements.Any(),
["HasPDF"] = lb => lb.Book.Supplements.Any(),
["PDFs"] = lb => lb.Book.Supplements.Any(),
["PDF"] = lb => lb.Book.Supplements.Any(),
["HasDownloads"] = lb => lb.Book.HasPdf,
["HasDownload"] = lb => lb.Book.HasPdf,
["Downloads"] = lb => lb.Book.HasPdf,
["Download"] = lb => lb.Book.HasPdf,
["HasPDFs"] = lb => lb.Book.HasPdf,
["HasPDF"] = lb => lb.Book.HasPdf,
["PDFs"] = lb => lb.Book.HasPdf,
["PDF"] = lb => lb.Book.HasPdf,
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
@@ -139,9 +139,7 @@ namespace LibationSearchEngine
return authors.Intersect(narrators).Any();
}
private static bool isLiberated(Book book)
=> book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated
|| AudibleFileStorage.Audio.Exists(book.AudibleProductId);
private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated;
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
// use these common fields in the "all" default search field

View File

@@ -1,6 +1,6 @@
using System.ComponentModel;
using Dinah.Core.Threading;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace LibationWinForms
{
@@ -8,9 +8,7 @@ namespace LibationWinForms
{
public event PropertyChangedEventHandler PropertyChanged;
public AsyncNotifyPropertyChanged() { }
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) });
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.UIThread(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
}
}

View File

@@ -39,7 +39,6 @@ namespace LibationWinForms.BookLiberation
#endregion
#region IStreamable event handler overrides
public override void OnStreamingBegin(object sender, string beginString) { }
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
if (!downloadProgress.ProgressPercentage.HasValue)
@@ -54,7 +53,6 @@ namespace LibationWinForms.BookLiberation
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
=> updateRemainingTime((int)timeRemaining.TotalSeconds);
public override void OnStreamingCompleted(object sender, string completedString) { }
#endregion
#region IAudioDecodable event handlers

View File

@@ -1,6 +1,6 @@
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Windows.Forms;
using Dinah.Core.Threading;
using FileLiberator;
using System;
using System.Windows.Forms;
@@ -48,7 +48,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
streamable.StreamingCompleted += OnStreamingCompleted;
streamable.StreamingCompleted += OnStreamingCompletedClose;
FormClosed += UnsubscribeStreamable;
Disposed += UnsubscribeStreamable;
}
private void Subscribe(IProcessable processable)
{
@@ -81,7 +81,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
}
private void UnsubscribeStreamable(object sender, EventArgs e)
{
FormClosed -= UnsubscribeStreamable;
Disposed -= UnsubscribeStreamable;
Streamable.StreamingBegin -= OnStreamingBeginShow;
Streamable.StreamingBegin -= OnStreamingBegin;
@@ -122,8 +122,8 @@ namespace LibationWinForms.BookLiberation.BaseForms
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(() => Close());
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(Close);
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(Dispose);
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on that
@@ -132,7 +132,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show);
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThread(Show);
#endregion

View File

@@ -50,24 +50,24 @@ namespace LibationWinForms.BookLiberation
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(completedAction, logMe);
var backupBook = CreateBackupBook(logMe);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
public static async Task BackupAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(completedAction, logMe);
var backupBook = CreateBackupBook(logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
}
@@ -84,19 +84,19 @@ namespace LibationWinForms.BookLiberation
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
}
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
public static async Task BackupAllPdfsAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
private static IProcessable CreateBackupBook(LogMe logMe)
{
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
@@ -104,7 +104,6 @@ namespace LibationWinForms.BookLiberation
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{
await downloadPdf.TryProcessAsync(e);
completedAction(sender, e);
}
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
@@ -176,7 +175,7 @@ namespace LibationWinForms.BookLiberation
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
protected abstract DialogResult CreateSkipFileResult { get; }
protected abstract DialogResult SkipResult { get; }
public async Task RunBackupAsync()
{
@@ -244,15 +243,10 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == CreateSkipFileResult)
if (dialogResult == SkipResult)
{
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
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
{path}
".Trim());
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
return true;
@@ -273,7 +267,7 @@ An error occurred while trying to process this book. Skip this book permanently?
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
protected override DialogResult SkipResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
: base(logMe, processable)
@@ -302,7 +296,7 @@ An error occurred while trying to process this book.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
protected override DialogResult SkipResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
: base(logMe, processable, automatedBackupsForm) { }
@@ -310,7 +304,7 @@ An error occurred while trying to process this book.
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks())
foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetContext().GetLibrary_Flat_NoTracking()))
{
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing)

View File

@@ -0,0 +1,18 @@
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public class DataGridViewImageButtonCell : DataGridViewButtonCell
{
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
{
var w = image.Width;
var h = image.Height;
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
graphics.DrawImage(image, new Rectangle(x, y, w, h));
}
}
}

View File

@@ -1,50 +0,0 @@
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public abstract class DataGridViewImageButtonColumn : DataGridViewButtonColumn
{
private DataGridViewImageButtonCell _cellTemplate;
public override DataGridViewCell CellTemplate
{
get => GetCellTemplate();
set
{
if (value is DataGridViewImageButtonCell cellTemplate)
_cellTemplate = cellTemplate;
}
}
protected abstract DataGridViewImageButtonCell NewCell();
private DataGridViewImageButtonCell GetCellTemplate()
{
if (_cellTemplate is null)
return NewCell();
else
return _cellTemplate;
}
public override object Clone()
{
var clone = (DataGridViewImageButtonColumn)base.Clone();
clone._cellTemplate = _cellTemplate;
return clone;
}
}
public class DataGridViewImageButtonCell : DataGridViewButtonCell
{
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
{
var w = image.Width;
var h = image.Height;
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
graphics.DrawImage(image, new Rectangle(x, y, w, h));
}
}
}

View File

@@ -28,64 +28,210 @@
/// </summary>
private void InitializeComponent()
{
this.SaveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// SaveBtn
//
this.SaveBtn.Location = new System.Drawing.Point(396, 25);
this.SaveBtn.Name = "SaveBtn";
this.SaveBtn.Size = new System.Drawing.Size(75, 23);
this.SaveBtn.TabIndex = 1;
this.SaveBtn.Text = "Save";
this.SaveBtn.UseVisualStyleBackColor = true;
this.SaveBtn.Click += new System.EventHandler(this.SaveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Location = new System.Drawing.Point(12, 27);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(375, 20);
this.newTagsTb.TabIndex = 0;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(424, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
this.saveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.tagsDescLbl = new System.Windows.Forms.Label();
this.coverPb = new System.Windows.Forms.PictureBox();
this.detailsTb = new System.Windows.Forms.TextBox();
this.tagsGb = new System.Windows.Forms.GroupBox();
this.cancelBtn = new System.Windows.Forms.Button();
this.liberatedGb = new System.Windows.Forms.GroupBox();
this.pdfLiberatedCb = new System.Windows.Forms.ComboBox();
this.pdfLiberatedLbl = new System.Windows.Forms.Label();
this.bookLiberatedCb = new System.Windows.Forms.ComboBox();
this.bookLiberatedLbl = new System.Windows.Forms.Label();
this.liberatedDescLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.coverPb)).BeginInit();
this.tagsGb.SuspendLayout();
this.liberatedGb.SuspendLayout();
this.SuspendLayout();
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(376, 427);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 3;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.newTagsTb.Location = new System.Drawing.Point(7, 40);
this.newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(556, 23);
this.newTagsTb.TabIndex = 1;
//
// tagsDescLbl
//
this.tagsDescLbl.AutoSize = true;
this.tagsDescLbl.Location = new System.Drawing.Point(7, 19);
this.tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.tagsDescLbl.Name = "tagsDescLbl";
this.tagsDescLbl.Size = new System.Drawing.Size(458, 15);
this.tagsDescLbl.TabIndex = 0;
this.tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
"ores";
//
// EditTagsDialog
//
this.AcceptButton = this.SaveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(483, 60);
this.Controls.Add(this.label1);
this.Controls.Add(this.newTagsTb);
this.Controls.Add(this.SaveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "EditTagsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Tags";
this.ResumeLayout(false);
this.PerformLayout();
//
// coverPb
//
this.coverPb.Location = new System.Drawing.Point(12, 12);
this.coverPb.Name = "coverPb";
this.coverPb.Size = new System.Drawing.Size(80, 80);
this.coverPb.TabIndex = 3;
this.coverPb.TabStop = false;
//
// detailsTb
//
this.detailsTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.detailsTb.Location = new System.Drawing.Point(98, 12);
this.detailsTb.Multiline = true;
this.detailsTb.Name = "detailsTb";
this.detailsTb.ReadOnly = true;
this.detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.detailsTb.Size = new System.Drawing.Size(484, 202);
this.detailsTb.TabIndex = 0;
//
// tagsGb
//
this.tagsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tagsGb.Controls.Add(this.tagsDescLbl);
this.tagsGb.Controls.Add(this.newTagsTb);
this.tagsGb.Location = new System.Drawing.Point(12, 220);
this.tagsGb.Name = "tagsGb";
this.tagsGb.Size = new System.Drawing.Size(570, 73);
this.tagsGb.TabIndex = 1;
this.tagsGb.TabStop = false;
this.tagsGb.Text = "Edit Tags";
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.Location = new System.Drawing.Point(494, 427);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// liberatedGb
//
this.liberatedGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.liberatedGb.Controls.Add(this.pdfLiberatedCb);
this.liberatedGb.Controls.Add(this.pdfLiberatedLbl);
this.liberatedGb.Controls.Add(this.bookLiberatedCb);
this.liberatedGb.Controls.Add(this.bookLiberatedLbl);
this.liberatedGb.Controls.Add(this.liberatedDescLbl);
this.liberatedGb.Location = new System.Drawing.Point(12, 299);
this.liberatedGb.Name = "liberatedGb";
this.liberatedGb.Size = new System.Drawing.Size(570, 122);
this.liberatedGb.TabIndex = 2;
this.liberatedGb.TabStop = false;
this.liberatedGb.Text = "Liberated status: Whether the book/pdf has been downloaded";
//
// pdfLiberatedCb
//
this.pdfLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.pdfLiberatedCb.FormattingEnabled = true;
this.pdfLiberatedCb.Location = new System.Drawing.Point(244, 86);
this.pdfLiberatedCb.Name = "pdfLiberatedCb";
this.pdfLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.pdfLiberatedCb.TabIndex = 4;
//
// pdfLiberatedLbl
//
this.pdfLiberatedLbl.AutoSize = true;
this.pdfLiberatedLbl.Location = new System.Drawing.Point(210, 89);
this.pdfLiberatedLbl.Name = "pdfLiberatedLbl";
this.pdfLiberatedLbl.Size = new System.Drawing.Size(28, 15);
this.pdfLiberatedLbl.TabIndex = 3;
this.pdfLiberatedLbl.Text = "PDF";
//
// bookLiberatedCb
//
this.bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.bookLiberatedCb.FormattingEnabled = true;
this.bookLiberatedCb.Location = new System.Drawing.Point(47, 86);
this.bookLiberatedCb.Name = "bookLiberatedCb";
this.bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.bookLiberatedCb.TabIndex = 2;
//
// bookLiberatedLbl
//
this.bookLiberatedLbl.AutoSize = true;
this.bookLiberatedLbl.Location = new System.Drawing.Point(7, 89);
this.bookLiberatedLbl.Name = "bookLiberatedLbl";
this.bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
this.bookLiberatedLbl.TabIndex = 1;
this.bookLiberatedLbl.Text = "Book";
//
// liberatedDescLbl
//
this.liberatedDescLbl.AutoSize = true;
this.liberatedDescLbl.Location = new System.Drawing.Point(20, 31);
this.liberatedDescLbl.Name = "liberatedDescLbl";
this.liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
this.liberatedDescLbl.TabIndex = 0;
this.liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to" +
" Downloaded";
//
// BookDetailsDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(594, 466);
this.Controls.Add(this.liberatedGb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.tagsGb);
this.Controls.Add(this.detailsTb);
this.Controls.Add(this.coverPb);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "BookDetailsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Book Details";
((System.ComponentModel.ISupportInitialize)(this.coverPb)).EndInit();
this.tagsGb.ResumeLayout(false);
this.tagsGb.PerformLayout();
this.liberatedGb.ResumeLayout(false);
this.liberatedGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button SaveBtn;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.TextBox newTagsTb;
private System.Windows.Forms.Label label1;
}
private System.Windows.Forms.Label tagsDescLbl;
private System.Windows.Forms.PictureBox coverPb;
private System.Windows.Forms.TextBox detailsTb;
private System.Windows.Forms.GroupBox tagsGb;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.GroupBox liberatedGb;
private System.Windows.Forms.ComboBox pdfLiberatedCb;
private System.Windows.Forms.Label pdfLiberatedLbl;
private System.Windows.Forms.ComboBox bookLiberatedCb;
private System.Windows.Forms.Label bookLiberatedLbl;
private System.Windows.Forms.Label liberatedDescLbl;
}
}

View File

@@ -1,28 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
namespace LibationWinForms.Dialogs
{
public partial class BookDetailsDialog : Form
{
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public string NewTags { get; private set; }
public LiberatedStatus BookLiberatedStatus { get; private set; }
public LiberatedStatus? PdfLiberatedStatus { get; private set; }
private LibraryBook _libraryBook { get; }
private Book Book => _libraryBook.Book;
public BookDetailsDialog()
{
InitializeComponent();
}
public BookDetailsDialog(string title, string rawTags) : this()
public BookDetailsDialog(LibraryBook libraryBook) : this()
{
this.Text = $"Edit Tags - {title}";
_libraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
initDetails();
initTags();
initLiberated();
}
// 1st draft: lazily cribbed from GridEntry.ctor()
private void initDetails()
{
this.Text = Book.Title;
this.newTagsTb.Text = rawTags;
(var isDefault, var picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
this.coverPb.Image = Dinah.Core.Drawing.ImageReader.ToImage(picture);
var t = @$"
Title: {Book.Title}
Author(s): {Book.AuthorNames}
Narrator(s): {Book.NarratorNames}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Category: {string.Join(" > ", Book.CategoriesNames)}
Purchase Date: {_libraryBook.DateAdded.ToString("d")}
".Trim();
if (!string.IsNullOrWhiteSpace(Book.SeriesNames))
t += $"\r\nSeries: {Book.SeriesNames}";
var bookRating = Book.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(bookRating))
t += $"\r\nBook Rating:\r\n{bookRating}";
var myRating = Book.UserDefinedItem.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(myRating))
t += $"\r\nMy Rating:\r\n{myRating}";
this.detailsTb.Text = t;
}
private void initTags() => this.newTagsTb.Text = Book.UserDefinedItem.Tags;
private void initLiberated()
{
{
var status = Book.UserDefinedItem.BookStatus;
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
// this should only appear if is already an error. User should not be able to set status to error, only away from error
if (status == LiberatedStatus.Error)
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Error, Text = "Error" });
setDefaultComboBox(this.bookLiberatedCb, status);
}
{
var status = Book.UserDefinedItem.PdfStatus;
if (status is null)
this.pdfLiberatedCb.Enabled = false;
else
{
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
setDefaultComboBox(this.pdfLiberatedCb, status);
}
}
}
private static void setDefaultComboBox(ComboBox comboBox, LiberatedStatus? status)
{
if (!status.HasValue)
{
comboBox.SelectedIndex = 0;
return;
}
var item = comboBox.Items.Cast<liberatedComboBoxItem>().SingleOrDefault(item => item.Status == status.Value);
if (item is not null)
comboBox.SelectedItem = item;
else
comboBox.SelectedIndex = 0;
}
private void SaveBtn_Click(object sender, EventArgs e)
private void saveBtn_Click(object sender, EventArgs e)
{
NewTags = this.newTagsTb.Text;
DialogResult = DialogResult.OK;
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
if (this.pdfLiberatedCb.Enabled)
PdfLiberatedStatus = ((liberatedComboBoxItem)this.pdfLiberatedCb.SelectedItem).Status;
this.DialogResult = DialogResult.OK;
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -3,15 +3,17 @@ using System.Windows.Forms;
namespace LibationWinForms
{
public class EditTagsDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
protected override DataGridViewImageButtonCell NewCell()
=> new EditTagsDataGridViewImageButtonCell();
public EditTagsDataGridViewImageButtonColumn()
{
CellTemplate = new EditTagsDataGridViewImageButtonCell();
}
}
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static readonly Image ButtonImage = Properties.Resources.edit_tags_25x25;
private static readonly Image ButtonImage = Properties.Resources.edit_25x25;
private static readonly Color HiddenForeColor = Color.LightGray;
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
@@ -20,12 +22,12 @@ namespace LibationWinForms
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;
if (DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor != foreColor)
if (DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor != foreColor)
{
DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor = foreColor;
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = foreColor;
}
if (tagsString.Length == 0)
if (tagsString?.Length == 0)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
DrawButtonImage(graphics, ButtonImage, cellBounds);

View File

@@ -384,15 +384,14 @@ namespace LibationWinForms
#region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
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
#region Export menu

View File

@@ -7,10 +7,14 @@ using System.Linq;
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using Dinah.Core;
using Dinah.Core.Drawing;
namespace LibationWinForms
{
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{
#region implementation properties
@@ -25,12 +29,15 @@ namespace LibationWinForms
private Book Book => LibraryBook.Book;
private Image _cover;
private Action Refilter { get; }
public GridEntry(LibraryBook libraryBook)
public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
{
LibraryBook = libraryBook;
Refilter = refilterOnChanged;
_memberValues = CreateMemberValueDictionary();
//Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
@@ -47,9 +54,9 @@ namespace LibationWinForms
Title = Book.Title;
Series = Book.SeriesNames;
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
MyRating = ValueOrDefault(Book.UserDefinedItem.Rating?.ToStarString(), "");
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
PurchaseDate = libraryBook.DateAdded.ToString("d");
ProductRating = ValueOrDefault(Book.Rating?.ToStarString(), "");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames;
Narrators = Book.NarratorNames;
Category = string.Join(" > ", Book.CategoriesNames);
@@ -57,7 +64,7 @@ namespace LibationWinForms
Description = GetDescriptionDisplay(Book);
}
//DisplayTags and Liberate properties are live.
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e)
@@ -69,8 +76,77 @@ namespace LibationWinForms
}
}
#region Data Source properties
#region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
var udi = sender as UserDefinedItem;
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
switch (itemName)
{
case nameof(udi.Tags):
{
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(DisplayTags));
}
break;
case nameof(udi.BookStatus):
{
Book.UserDefinedItem.BookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
case nameof(udi.PdfStatus):
{
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
}
if (!suspendCommit)
Commit();
}
private bool suspendCommit = false;
/// <summary>
/// Begin editing the model, suspending commits until <see cref="EndEdit"/> is called.
/// </summary>
public void BeginEdit() => suspendCommit = true;
/// <summary>
/// Save all edits to the database.
/// </summary>
public void EndEdit()
{
Commit();
suspendCommit = false;
}
private void Commit()
{
//We don't want LiberatedStatus.PartialDownload to be a persistent status.
var bookStatus = Book.UserDefinedItem.BookStatus;
var saveStatus = bookStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : bookStatus;
Book.UserDefinedItem.BookStatus = saveStatus;
LibraryCommands.UpdateUserDefinedItem(Book);
Book.UserDefinedItem.BookStatus = bookStatus;
Refilter?.Invoke();
}
#endregion
#region Model properties exposed to the view
public Image Cover
{
get
@@ -95,8 +171,22 @@ namespace LibationWinForms
public string Category { get; }
public string Misc { get; }
public string Description { get; }
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book));
public string DisplayTags
{
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
set => Book.UserDefinedItem.Tags = value;
}
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
{
get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book));
set
{
LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
}
}
#endregion
#region Data Sorting
@@ -120,7 +210,7 @@ namespace LibationWinForms
{ nameof(Category), () => Category },
{ nameof(Misc), () => Misc },
{ nameof(DisplayTags), () => DisplayTags },
{ nameof(Liberate), () => Liberate.Item1 }
{ nameof(Liberate), () => Liberate.BookStatus }
};
// Instantiate comparers for every exposed member object type.
@@ -130,7 +220,7 @@ namespace LibationWinForms
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberatedState), new ObjectComparer<LiberatedState>() },
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
};
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
@@ -154,37 +244,7 @@ namespace LibationWinForms
#endregion
#region Static library display functions
public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
{
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedState.Liberated => ("Liberated", "green"),
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
PdfState.NoPdf => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedState.NotDownloaded ||
liberatedStatus == LiberatedState.PartialDownload ||
pdfStatus == PdfState.NotDownloaded)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
return (mouseoverText, buttonImage);
}
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// </summary>
@@ -207,8 +267,8 @@ namespace LibationWinForms
{
var details = new List<string>();
var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]");
var acct = ValueOrDefault(libraryBook.Account, "[unknown]");
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
details.Add($"Account: {locale} - {acct}");
@@ -228,10 +288,12 @@ namespace LibationWinForms
return string.Join("\r\n", details);
}
//Maybe add to Dinah StringExtensions?
private static string ValueOrDefault(string value, string defaultValue)
=> string.IsNullOrWhiteSpace(value) ? defaultValue : value;
#endregion
~GridEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
}

View File

@@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>

View File

@@ -3,13 +3,16 @@ using System;
using System.Drawing;
using System.Windows.Forms;
using System.Linq;
using DataLayer;
namespace LibationWinForms
{
public class LiberateDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
protected override DataGridViewImageButtonCell NewCell()
=> new LiberateDataGridViewImageButtonCell();
public LiberateDataGridViewImageButtonColumn()
{
CellTemplate = new LiberateDataGridViewImageButtonCell();
}
}
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
@@ -18,14 +21,48 @@ namespace LibationWinForms
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is (LiberatedState liberatedState, PdfState pdfState))
if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
{
(string mouseoverText, Bitmap buttonImage) = GridEntry.GetLiberateDisplay(liberatedState, pdfState);
var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
DrawButtonImage(graphics, buttonImage, cellBounds);
ToolTipText = mouseoverText;
}
}
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
{
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedStatus.Liberated => ("Liberated", "green"),
LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"),
LiberatedStatus.Error => ("Book downloaded ERROR", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"),
LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"),
LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"),
null => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedStatus.NotLiberated ||
liberatedStatus == LiberatedStatus.PartialDownload ||
pdfStatus == LiberatedStatus.NotLiberated)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
return (mouseoverText, buttonImage);
}
}
}

View File

@@ -92,7 +92,7 @@
this.gridEntryDataGridView.ReadOnly = true;
this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1505, 380);
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageButtonBoxColumn1
@@ -103,7 +103,7 @@
this.dataGridViewImageButtonBoxColumn1.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.dataGridViewImageButtonBoxColumn1.Width = 70;
this.dataGridViewImageButtonBoxColumn1.Width = 75;
//
// dataGridViewImageColumn1
//
@@ -200,7 +200,7 @@
// dataGridViewImageButtonBoxColumn2
//
this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags";
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Edit Tags";
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Tags and Details";
this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2";
this.dataGridViewImageButtonBoxColumn2.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
@@ -213,7 +213,7 @@
this.Controls.Add(this.gridEntryDataGridView);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(1505, 380);
this.Size = new System.Drawing.Size(1510, 380);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);

View File

@@ -67,7 +67,7 @@ namespace LibationWinForms
await Liberate_Click(liveGridEntry);
break;
case nameof(liveGridEntry.DisplayTags):
EditTags_Click(liveGridEntry);
Details_Click(liveGridEntry);
break;
}
}
@@ -77,30 +77,32 @@ namespace LibationWinForms
var libraryBook = liveGridEntry.LibraryBook;
// liberated: open explorer to file
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
if (libraryBook.Book.Audio_Exists)
{
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var filePath = FileManager.AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath))
MessageBox.Show($"File not found:\r\n{filePath}");
return;
}
// else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
}
private void EditTags_Click(GridEntry liveGridEntry)
private void Details_Click(GridEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags);
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags);
if (qtyChanges == 0)
return;
liveGridEntry.BeginEdit();
//Re-apply filters
Filter();
liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
liveGridEntry.EndEdit();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
@@ -129,7 +131,7 @@ namespace LibationWinForms
}
var orderedGridEntries = lib
.Select(lb => new GridEntry(lb)).ToList()
.Select(lb => new GridEntry(lb, Filter)).ToList()
// default load order
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
//// more advanced example: sort by author, then series, then title
@@ -147,19 +149,6 @@ namespace LibationWinForms
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
public void RefreshRow(string productId)
{
var rowIndex = getRowIndex((ge) => ge.AudibleProductId == productId);
// update cells incl Liberate button text
_dataGridView.InvalidateRow(rowIndex);
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
Filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#region Filter
@@ -192,10 +181,7 @@ namespace LibationWinForms
#endregion
#region DataGridView Macro
private int getRowIndex(Func<GridEntry, bool> func) => _dataGridView.GetRowIdOfBoundItem(func);
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion
}
}

View File

@@ -90,6 +90,26 @@ namespace LibationWinForms.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap edit_25x25 {
get {
object obj = ResourceManager.GetObject("edit_25x25", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap edit_64x64 {
get {
object obj = ResourceManager.GetObject("edit_64x64", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -53,12 +112,12 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
@@ -68,6 +127,12 @@
<data name="default_cover_80x80" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_80x80.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit_25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_64x64" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit_64x64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_tags_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit-tags-25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -1,122 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading;
namespace LibationWinForms
{
public class SynchronizeInvoker : ISynchronizeInvoke
{
public bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
private int InstanceThreadId { get; set; } = Thread.CurrentThread.ManagedThreadId;
private SynchronizationContext SyncContext { get; } = SynchronizationContext.Current;
public SynchronizeInvoker()
{
if (SyncContext is null)
throw new NullReferenceException($"Could not capture a current {nameof(SynchronizationContext)}");
}
public IAsyncResult BeginInvoke(Action action) => BeginInvoke(action, null);
public IAsyncResult BeginInvoke(Delegate method) => BeginInvoke(method, null);
public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
var tme = new ThreadMethodEntry(method, args);
if (InvokeRequired)
{
SyncContext.Post(OnSendOrPostCallback, tme);
}
else
{
tme.Complete();
tme.CompletedSynchronously = true;
}
return tme;
}
public object EndInvoke(IAsyncResult result)
{
if (result is not ThreadMethodEntry crossThread)
throw new ArgumentException($"{nameof(result)} was not returned by {nameof(SynchronizeInvoker)}.{nameof(BeginInvoke)}");
if (!crossThread.IsCompleted)
crossThread.AsyncWaitHandle.WaitOne();
return crossThread.ReturnValue;
}
public object Invoke(Action action) => Invoke(action, null);
public object Invoke(Delegate method) => Invoke(method, null);
public object Invoke(Delegate method, object[] args)
{
var tme = new ThreadMethodEntry(method, args);
if (InvokeRequired)
{
SyncContext.Send(OnSendOrPostCallback, tme);
}
else
{
tme.Complete();
tme.CompletedSynchronously = true;
}
return tme.ReturnValue;
}
/// <summary>
/// This callback executes on the SynchronizationContext thread.
/// </summary>
private static void OnSendOrPostCallback(object asyncArgs)
{
var e = asyncArgs as ThreadMethodEntry;
e.Complete();
}
private class ThreadMethodEntry : IAsyncResult
{
public object AsyncState => null;
public bool CompletedSynchronously { get; internal set; }
public bool IsCompleted { get; private set; }
public object ReturnValue { get; private set; }
public WaitHandle AsyncWaitHandle => completedEvent;
private Delegate method;
private object[] args;
private ManualResetEvent completedEvent;
public ThreadMethodEntry(Delegate method, object[] args)
{
this.method = method;
this.args = args;
completedEvent = new ManualResetEvent(initialState: false);
}
public void Complete()
{
try
{
switch (method)
{
case Action actiton:
actiton();
break;
default:
ReturnValue = method.DynamicInvoke(args);
break;
}
}
finally
{
IsCompleted = true;
completedEvent.Set();
}
}
~ThreadMethodEntry()
{
completedEvent.Close();
}
}
}
}

View File

@@ -52,11 +52,13 @@ publish win64 platform, single-file
dotnet publish -r win-x64 -c Release
-- end HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
edit tags icon images from:
icons8.com
search: tags
-- end IMAGES ---------------------------------------------------------------------------------------------------------------------
'edit' icon: https://www.iconfinder.com/icons/383147/edit_icon
-- end IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
-- begin AUDIBLE DETAILS ---------------------------------------------------------------------------------------------------------------------
alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' , 'product_id' in different parts of the site
@@ -65,7 +67,7 @@ alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' ,
-- begin SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
do NOT combine jsons for
- audible-scraped persistence: library, book details
- libation-generated persistence: FilePaths.json
- libation-generated persistence: FileLocations.json
- user-defined persistence: BookTags.json
-- end SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------