mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-03 03:18:27 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f16ad30891 | ||
|
|
ef53a6a8cb | ||
|
|
9a37d434f1 | ||
|
|
d7eb190f69 | ||
|
|
f19c46ee45 | ||
|
|
343c3b62d6 | ||
|
|
b1de10a71a | ||
|
|
6beb5cc74a | ||
|
|
3767c3574a | ||
|
|
4ceb4f9c03 | ||
|
|
0f5149f7b4 | ||
|
|
673451dc11 | ||
|
|
e4257afc14 | ||
|
|
2a7e185dc3 | ||
|
|
9e06c343c1 | ||
|
|
40b3a9990d | ||
|
|
d66c112a1e | ||
|
|
d826885728 | ||
|
|
263222d8cc | ||
|
|
f25734334d | ||
|
|
ede8397f13 | ||
|
|
1369ee575a | ||
|
|
c8e2418af7 | ||
|
|
2da25edafd | ||
|
|
f60964f4c7 | ||
|
|
3183f99153 | ||
|
|
2a22cff67c | ||
|
|
7fbe8ae769 | ||
|
|
f9df466ad8 | ||
|
|
0b129fcf7c | ||
|
|
2be5fd5af3 | ||
|
|
c9727f84ab | ||
|
|
aa56bb74a1 | ||
|
|
85a6e21dcf | ||
|
|
8c620c25ab | ||
|
|
813d91dfa4 | ||
|
|
d0d66c6135 | ||
|
|
a8d609676e | ||
|
|
8386da5ec6 | ||
|
|
f5089e7e29 | ||
|
|
a639857ec6 | ||
|
|
35b5d7370c | ||
|
|
c9f988acf8 | ||
|
|
6dfef09ea3 | ||
|
|
7e288c0c08 | ||
|
|
dbcf6f25db | ||
|
|
0cc55fd1e8 | ||
|
|
e36ea70cd1 | ||
|
|
a86185e644 | ||
|
|
64a8f007a5 | ||
|
|
215a626c92 | ||
|
|
de93047192 |
@@ -224,6 +224,8 @@ namespace AaxDecrypter
|
||||
isCanceled = true;
|
||||
aaxFile?.Cancel();
|
||||
aaxFile?.Dispose();
|
||||
nfsPersister?.NetworkFileStream?.Close();
|
||||
nfsPersister?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal file
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal file
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal file
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
LibationWinForms/ProductsGrid.Designer.cs
generated
8
LibationWinForms/ProductsGrid.Designer.cs
generated
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
20
LibationWinForms/Properties/Resources.Designer.cs
generated
20
LibationWinForms/Properties/Resources.Designer.cs
generated
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
LibationWinForms/Resources/edit_25x25.png
Normal file
BIN
LibationWinForms/Resources/edit_25x25.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 747 B |
BIN
LibationWinForms/Resources/edit_64x64.png
Normal file
BIN
LibationWinForms/Resources/edit_64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 813 B |
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user