Compare commits

..

23 Commits

Author SHA1 Message Date
Robert McRackan
f9917d4064 edit comments 2019-12-05 16:14:39 -05:00
Robert McRackan
0f9f0d9eae New feature: liberate individual book 2019-12-05 15:55:46 -05:00
Robert McRackan
498aeaac3a Change "Download Status" column to "Liberate" button column. Displays text status. No functionality added yet 2019-12-05 12:39:38 -05:00
Robert McRackan
9534969c2d Series should sort irrespective of initial the/a/an (like Title already does) 2019-12-04 13:32:25 -05:00
Robert McRackan
b120bb8a66 Replace custom FileLogger with Serilog 2019-12-04 09:58:31 -05:00
Robert McRackan
f8a51f0882 Upgrade to Core 3.1 2019-12-03 16:47:53 -05:00
Robert McRackan
7529fdf878 Add logging 2019-12-02 15:14:19 -05:00
Robert McRackan
f1aacd92ad Bugfix: decrypt file conflict 2019-12-02 14:39:46 -05:00
Robert McRackan
b1b426427c Bugfix: initial bottom counts can throw error when a book was moved since Libation was last run 2019-11-27 16:57:35 -05:00
Robert McRackan
0683e5f55b Optimize tag persistence 2019-11-27 15:36:34 -05:00
Robert McRackan
5c81441f83 Bugfix: decrypt book with no author 2019-11-27 09:27:43 -05:00
Robert McRackan
57bc74cd23 Improved logging for file decrypt 2019-11-26 13:13:16 -05:00
Robert McRackan
1cecd4ba2e Improved logging. Updated nuget packages 2019-11-26 10:42:38 -05:00
Robert McRackan
7a4bd639fb Add notes for v3.1-beta.5 2019-11-25 14:12:07 -05:00
Robert McRackan
87e6a46808 remove db file 2019-11-25 14:09:53 -05:00
Robert McRackan
a2e30df51f Improved importing 2019-11-25 13:45:29 -05:00
Robert McRackan
c8e759c067 update notes 2019-11-24 21:47:03 -05:00
Robert McRackan
6c9074169a Added beta-specific logging 2019-11-24 21:45:35 -05:00
Robert McRackan
1375da2065 Improved performance calculating "liberated" status 2019-11-21 23:07:06 -05:00
Robert McRackan
d5d72a13f6 Login dialogs can get lost. Show on task bar 2019-11-20 13:00:13 -05:00
Robert McRackan
a1ba324166 Has PDFs => Has PDF 2019-11-19 13:34:38 -05:00
Robert McRackan
b0139c47be live update newly downloaded and cached images 2019-11-19 11:22:41 -05:00
Robert McRackan
80b0ef600d Better ToString for DataLayer objects 2019-11-19 09:54:42 -05:00
77 changed files with 1645 additions and 1547 deletions

1
.gitignore vendored
View File

@@ -332,3 +332,4 @@ ASALocalRun/
# manually ignored files
/__TODO.txt
/DataLayer/LibationContext.db

View File

@@ -72,8 +72,9 @@ namespace AaxDecrypter
}
private AaxToM4bConverter(string inputFile, string decryptKey)
{
if (string.IsNullOrWhiteSpace(inputFile)) throw new ArgumentNullException(nameof(inputFile), "Input file may not be null or whitespace");
if (!File.Exists(inputFile)) throw new ArgumentNullException(nameof(inputFile), "File does not exist");
ArgumentValidator.EnsureNotNullOrWhiteSpace(inputFile, nameof(inputFile));
if (!File.Exists(inputFile))
throw new ArgumentNullException(nameof(inputFile), "File does not exist");
steps = new StepSequence
{
@@ -89,22 +90,24 @@ namespace AaxDecrypter
["End: Create Nfo"] = End_CreateNfo
};
this.inputFileName = inputFile;
inputFileName = inputFile;
this.decryptKey = decryptKey;
}
private async Task prelimProcessing()
{
this.tags = new Tags(this.inputFileName);
this.encodingInfo = new EncodingInfo(this.inputFileName);
this.chapters = new Chapters(this.inputFileName, this.tags.duration.TotalSeconds);
tags = new Tags(inputFileName);
encodingInfo = new EncodingInfo(inputFileName);
chapters = new Chapters(inputFileName, tags.duration.TotalSeconds);
var defaultFilename = Path.Combine(
Path.GetDirectoryName(this.inputFileName),
getASCIITag(this.tags.author),
getASCIITag(this.tags.title) + ".m4b"
Path.GetDirectoryName(inputFileName),
getASCIITag(tags.author),
getASCIITag(tags.title) + ".m4b"
);
SetOutputFilename(defaultFilename);
// set default name
SetOutputFilename(defaultFilename);
await Task.Run(() => saveCover(inputFileName));
}
@@ -118,7 +121,7 @@ namespace AaxDecrypter
private void saveCover(string aaxFile)
{
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
this.coverBytes = file.Tag.Pictures[0].Data.Data;
coverBytes = file.Tag.Pictures[0].Data.Data;
}
private void printPrelim()
@@ -156,21 +159,24 @@ namespace AaxDecrypter
public void SetOutputFilename(string outFileName)
{
this.outputFileName = outFileName;
outputFileName = outFileName;
if (Path.GetExtension(this.outputFileName) != ".m4b")
this.outputFileName = outputFileWithNewExt(".m4b");
if (Path.GetExtension(outputFileName) != ".m4b")
outputFileName = outputFileWithNewExt(".m4b");
this.outDir = Path.GetDirectoryName(this.outputFileName);
if (File.Exists(outputFileName))
File.Delete(outputFileName);
outDir = Path.GetDirectoryName(outputFileName);
}
private string outputFileWithNewExt(string extension)
=> Path.Combine(this.outDir, Path.GetFileNameWithoutExtension(this.outputFileName) + '.' + extension.Trim('.'));
=> Path.Combine(outDir, Path.GetFileNameWithoutExtension(outputFileName) + '.' + extension.Trim('.'));
public bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = this.outDir;
Directory.CreateDirectory(this.outDir);
ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir);
return true;
}
@@ -178,7 +184,7 @@ namespace AaxDecrypter
{
DecryptProgressUpdate?.Invoke(this, 0);
var tempRipFile = Path.Combine(this.outDir, "funny.aac");
var tempRipFile = Path.Combine(outDir, "funny.aac");
var fail = "WARNING-Decrypt failure. ";
@@ -193,7 +199,7 @@ namespace AaxDecrypter
if (returnCode == -99)
{
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
this.decryptKey = null;
decryptKey = null;
returnCode = getKey_decrypt(tempRipFile);
}
}
@@ -232,7 +238,7 @@ namespace AaxDecrypter
Console.WriteLine("Cracking activation bytes");
var activation_bytes = BytesCracker.GetActivationBytes(checksum);
this.decryptKey = activation_bytes;
decryptKey = activation_bytes;
Console.WriteLine("Activation bytes cracked. Decrypt key: " + activation_bytes);
}
@@ -243,10 +249,10 @@ namespace AaxDecrypter
Console.WriteLine("Decrypting with key " + decryptKey);
var returnCode = 100;
var thread = new Thread(() => returnCode = this.ngDecrypt());
var thread = new Thread(() => returnCode = ngDecrypt());
thread.Start();
double fileLen = new FileInfo(this.inputFileName).Length;
double fileLen = new FileInfo(inputFileName).Length;
while (thread.IsAlive && returnCode == 100)
{
Thread.Sleep(500);
@@ -266,7 +272,7 @@ namespace AaxDecrypter
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.mp4trackdumpPath,
Arguments = "-c " + this.encodingInfo.channels + " -r " + this.encodingInfo.sampleRate + " \"" + this.inputFileName + "\""
Arguments = "-c " + encodingInfo.channels + " -r " + encodingInfo.sampleRate + " \"" + inputFileName + "\""
};
info.EnvironmentVariables["VARIABLE"] = decryptKey;
@@ -279,21 +285,22 @@ namespace AaxDecrypter
return exitCode;
}
// temp file names for steps 3, 4, 5
string tempChapsPath => Path.Combine(this.outDir, "tempChaps.mp4");
// temp file names for steps 3, 4, 5
string tempChapsGuid { get; } = Guid.NewGuid().ToString().ToUpper().Replace("-", "");
string tempChapsPath => Path.Combine(outDir, $"tempChaps_{tempChapsGuid}.mp4");
string mp4_file => outputFileWithNewExt(".mp4");
string ff_txt_file => mp4_file + ".ff.txt";
public bool Step3_Chapterize()
{
string str1 = "";
if (this.chapters.FirstChapterStart != 0.0)
if (chapters.FirstChapterStart != 0.0)
{
str1 = " -ss " + this.chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (this.chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
str1 = " -ss " + chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
}
string ffmpegTags = this.tags.GenerateFfmpegTags();
string ffmpegChapters = this.chapters.GenerateFfmpegChapters();
string ffmpegTags = tags.GenerateFfmpegTags();
string ffmpegChapters = chapters.GenerateFfmpegChapters();
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
var tagAndChapterInfo = new ProcessStartInfo
@@ -309,8 +316,8 @@ namespace AaxDecrypter
public bool Step4_InsertCoverArt()
{
// save cover image as temp file
var coverPath = Path.Combine(this.outDir, "cover-" + Guid.NewGuid() + ".jpg");
FileExt.CreateFile(coverPath, this.coverBytes);
var coverPath = Path.Combine(outDir, "cover-" + Guid.NewGuid() + ".jpg");
FileExt.CreateFile(coverPath, coverBytes);
var insertCoverArtInfo = new ProcessStartInfo
{
@@ -329,26 +336,26 @@ namespace AaxDecrypter
{
FileExt.SafeDelete(mp4_file);
FileExt.SafeDelete(ff_txt_file);
FileExt.SafeMove(tempChapsPath, this.outputFileName);
FileExt.SafeMove(tempChapsPath, outputFileName);
return true;
}
public bool Step6_AddTags()
{
this.tags.AddAppleTags(this.outputFileName);
tags.AddAppleTags(outputFileName);
return true;
}
public bool End_CreateCue()
{
File.WriteAllText(outputFileWithNewExt(".cue"), this.chapters.GetCuefromChapters(Path.GetFileName(this.outputFileName)));
File.WriteAllText(outputFileWithNewExt(".cue"), chapters.GetCuefromChapters(Path.GetFileName(outputFileName)));
return true;
}
public bool End_CreateNfo()
{
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, this.tags, this.encodingInfo, this.chapters));
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, tags, encodingInfo, chapters));
return true;
}
}

View File

@@ -28,7 +28,7 @@ namespace AaxDecrypter
using TagLib.File tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
this.title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
this.album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
this.author = tagLibFile.Tag.FirstPerformer;
this.author = tagLibFile.Tag.FirstPerformer ?? "[unknown]";
this.year = tagLibFile.Tag.Year.ToString();
this.comments = tagLibFile.Tag.Comment;
this.duration = tagLibFile.Properties.Duration;

View File

@@ -9,30 +9,49 @@ namespace ApplicationServices
{
public static class LibraryCommands
{
public static async Task<(int totalCount, int newCount)> IndexLibraryAsync(ILoginCallback callback)
public static async Task<(int totalCount, int newCount)> ImportLibraryAsync(ILoginCallback callback)
{
var audibleApiActions = new AudibleApiActions();
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
var totalCount = items.Count;
try
{
var audibleApiActions = new AudibleApiActions();
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
var totalCount = items.Count;
Serilog.Log.Logger.Debug($"GetAllLibraryItems: Total count {totalCount}");
var libImporter = new LibraryImporter();
var newCount = await Task.Run(() => libImporter.Import(items));
var libImporter = new LibraryImporter();
var newCount = await Task.Run(() => libImporter.Import(items));
Serilog.Log.Logger.Debug($"Import: New count {newCount}");
await Task.Run(() => SearchEngineCommands.FullReIndex());
await Task.Run(() => SearchEngineCommands.FullReIndex());
Serilog.Log.Logger.Debug("FullReIndex: success");
return (totalCount, newCount);
return (totalCount, newCount);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error importing library");
throw;
}
}
public static int UpdateTags(this LibationContext context, Book book, string newTags)
{
book.UserDefinedItem.Tags = newTags;
try
{
book.UserDefinedItem.Tags = newTags;
var qtyChanges = context.SaveChanges();
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
return qtyChanges;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
@@ -12,13 +12,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -3,54 +3,50 @@ using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20191115193402_Fresh")]
[Migration("20191125182309_Fresh")]
partial class Fresh
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasAnnotation("ProductVersion", "3.0.0");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("bit");
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<string>("PictureId")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -64,16 +60,16 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("tinyint");
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
@@ -88,17 +84,16 @@ namespace DataLayer.Migrations
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
@@ -121,29 +116,35 @@ namespace DataLayer.Migrations
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleAuthorId")
.HasColumnType("nvarchar(max)");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
.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("int");
.HasColumnType("INTEGER");
b.Property<DateTime>("DateAdded")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -154,14 +155,13 @@ namespace DataLayer.Migrations
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("SeriesId");
@@ -173,13 +173,13 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("real");
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
@@ -201,18 +201,16 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.HasKey("BookId");
@@ -226,14 +224,13 @@ namespace DataLayer.Migrations
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
@@ -248,10 +245,10 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("BookId");
@@ -263,16 +260,16 @@ namespace DataLayer.Migrations
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");

View File

@@ -12,7 +12,7 @@ namespace DataLayer.Migrations
columns: table => new
{
CategoryId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleCategoryId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
ParentCategoryCategoryId = table.Column<int>(nullable: true)
@@ -33,9 +33,9 @@ namespace DataLayer.Migrations
columns: table => new
{
ContributorId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(nullable: true),
AudibleAuthorId = table.Column<string>(nullable: true)
AudibleContributorId = table.Column<string>(nullable: true)
},
constraints: table =>
{
@@ -47,7 +47,7 @@ namespace DataLayer.Migrations
columns: table => new
{
SeriesId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleSeriesId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true)
},
@@ -61,7 +61,7 @@ namespace DataLayer.Migrations
columns: table => new
{
BookId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
AudibleProductId = table.Column<string>(nullable: true),
Title = table.Column<string>(nullable: true),
Description = table.Column<string>(nullable: true),
@@ -159,7 +159,7 @@ namespace DataLayer.Migrations
columns: table => new
{
SupplementId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
.Annotation("Sqlite:Autoincrement", true),
BookId = table.Column<int>(nullable: false),
Url = table.Column<string>(nullable: true)
},
@@ -200,6 +200,11 @@ namespace DataLayer.Migrations
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
values: new object[] { -1, "", "", null });
migrationBuilder.InsertData(
table: "Contributors",
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
values: new object[] { -1, null, "" });
migrationBuilder.CreateIndex(
name: "IX_BookContributor_BookId",
table: "BookContributor",

View File

@@ -3,7 +3,6 @@ using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
@@ -15,40 +14,37 @@ namespace DataLayer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasAnnotation("ProductVersion", "3.0.0");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("bit");
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<string>("PictureId")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -62,16 +58,16 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("tinyint");
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
@@ -86,17 +82,16 @@ namespace DataLayer.Migrations
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
@@ -119,29 +114,35 @@ namespace DataLayer.Migrations
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleAuthorId")
.HasColumnType("nvarchar(max)");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
.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("int");
.HasColumnType("INTEGER");
b.Property<DateTime>("DateAdded")
.HasColumnType("datetime2");
.HasColumnType("TEXT");
b.HasKey("BookId");
@@ -152,14 +153,13 @@ namespace DataLayer.Migrations
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("nvarchar(450)");
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b.HasKey("SeriesId");
@@ -171,13 +171,13 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("real");
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
@@ -199,18 +199,16 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b1.HasKey("BookId");
@@ -224,14 +222,13 @@ namespace DataLayer.Migrations
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
@@ -246,10 +243,10 @@ namespace DataLayer.Migrations
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("nvarchar(max)");
.HasColumnType("TEXT");
b1.HasKey("BookId");
@@ -261,16 +258,16 @@ namespace DataLayer.Migrations
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("int");
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("real");
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");

View File

@@ -62,7 +62,8 @@ namespace DataLayer
string description,
int lengthInMinutes,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators)
IEnumerable<Contributor> narrators,
Category category)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
@@ -80,8 +81,7 @@ namespace DataLayer
_seriesLink = new HashSet<SeriesBook>();
_supplements = new HashSet<Supplement>();
// since category/id is never null, nullity means it hasn't been loaded
CategoryId = Category.GetEmpty().CategoryId;
Category = category;
// simple assigns
Title = title;
@@ -91,7 +91,7 @@ namespace DataLayer
// assigns with biz logic
ReplaceAuthors(authors);
ReplaceNarrators(narrators);
}
}
#region contributors, authors, narrators
// use uninitialised backing fields - this means we can detect if the collection was loaded
@@ -123,16 +123,10 @@ namespace DataLayer
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
// the edge cases of doing local-loaded vs remote-only got weird. just load it
if (_contributorsLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add contributors");
if (_contributorsLink is null)
getEntry(context).Collection(s => s.ContributorsLink).Load();
context.Entry(this).Collection(s => s.ContributorsLink).Load();
}
var roleContributions = getContributions(role);
var roleContributions = getContributions(role);
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
if (isIdentical)
return;
@@ -140,7 +134,8 @@ namespace DataLayer
_contributorsLink.RemoveWhere(bc => bc.Role == role);
addNewContributors(newContributors, role);
}
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
{
byte order = 0;
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
@@ -155,6 +150,18 @@ namespace DataLayer
.ToList();
#endregion
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
var entry = context.Entry(this);
if (!entry.IsKeySet)
throw new InvalidOperationException("Could not load a valid Book from database");
return entry;
}
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
@@ -186,16 +193,10 @@ namespace DataLayer
// our add() is conditional upon what's already included in the collection.
// therefore if not loaded, a trip is required. might as well just load it
if (_seriesLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add series");
if (_seriesLink is null)
getEntry(context).Collection(s => s.SeriesLink).Load();
context.Entry(this).Collection(s => s.SeriesLink).Load();
}
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
if (singleSeriesBook == null)
_seriesLink.Add(new SeriesBook(series, this, index));
else
@@ -206,13 +207,12 @@ namespace DataLayer
#region supplements
private HashSet<Supplement> _supplements;
public IEnumerable<Supplement> Supplements => _supplements?.ToList();
public bool HasPdfs => Supplements.Any();
public bool HasPdf => Supplements.Any();
public void AddSupplementDownloadUrl(string url)
{
// supplements are owned by Book, so no need to Load():
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner.
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
@@ -232,19 +232,14 @@ namespace DataLayer
}
public void UpdateCategory(Category category, DbContext context = null)
{
// since category is never null, nullity means it hasn't been loaded
if (Category != null || CategoryId == Category.GetEmpty().CategoryId)
{
Category = category;
return;
}
{
// since category is never null, nullity means it hasn't been loaded
if (Category is null)
getEntry(context).Reference(s => s.Category).Load();
if (context == null)
throw new Exception("need context");
context.Entry(this).Reference(s => s.Category).Load();
Category = category;
Category = category;
}
}
public override string ToString() => $"[{AudibleProductId}] {Title}";
}
}

View File

@@ -23,5 +23,7 @@ namespace DataLayer
Role = role;
Order = order;
}
}
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
}
}

View File

@@ -18,8 +18,7 @@ namespace DataLayer
public class Category
{
// Empty is a special case. use private ctor w/o validation
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "", ParentCategory = null };
public bool IsEmpty() => string.IsNullOrWhiteSpace(AudibleCategoryId) || string.IsNullOrWhiteSpace(Name) || ParentCategory == null;
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "" };
internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; }
@@ -48,5 +47,7 @@ namespace DataLayer
if (parentCategory != null)
ParentCategory = parentCategory;
}
}
public override string ToString() => $"[{AudibleCategoryId}] {Name}";
}
}

View File

@@ -5,26 +5,31 @@ using Dinah.Core;
namespace DataLayer
{
public class Contributor
{
// contributors search links are just name with url-encoding. space can be + or %20
// author search link: /search?searchAuthor=Robert+Bevan
// narrator search link: /search?searchNarrator=Robert+Bevan
// can also search multiples. concat with comma before url encode
{
// Empty is a special case. use private ctor w/o validation
public static Contributor GetEmpty() => new Contributor { ContributorId = -1, Name = "" };
// id.s
// ----
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
// goes to summary page
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
// contributors search links are just name with url-encoding. space can be + or %20
// author search link: /search?searchAuthor=Robert+Bevan
// narrator search link: /search?searchNarrator=Robert+Bevan
// can also search multiples. concat with comma before url encode
internal int ContributorId { get; private set; }
// id.s
// ----
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
// goes to summary page
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
internal int ContributorId { get; private set; }
public string Name { get; private set; }
private HashSet<BookContributor> _booksLink;
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
public string AudibleContributorId { get; private set; }
private Contributor() { }
public Contributor(string name)
{
@@ -34,49 +39,13 @@ namespace DataLayer
Name = name;
}
public Contributor(string name, string audibleContributorId) : this(name)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(audibleContributorId))
AudibleContributorId = audibleContributorId;
}
public string AudibleAuthorId { get; private set; }
public void UpdateAudibleAuthorId(string authorId)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(authorId))
AudibleAuthorId = authorId;
}
#region // AudibleAuthorId refactor: separate author-specific info. overkill for a single optional string
///// <summary>Most authors in Audible have a unique id</summary>
//public AudibleAuthorProperty AudibleAuthorProperty { get; private set; }
//public void UpdateAuthorId(string authorId, LibationContext context = null)
//{
// if (authorId == null)
// return;
// if (AudibleAuthorProperty != null)
// {
// AudibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// return;
// }
// if (context == null)
// throw new ArgumentNullException(nameof(context), "You must provide a context");
// if (context.Contributors.Find(ContributorId) == null)
// throw new InvalidOperationException("Could not update audible author id.");
// var audibleAuthorProperty = new AudibleAuthorProperty();
// audibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// context.AuthorProperties.Add(audibleAuthorProperty);
//}
//public class AudibleAuthorProperty
//{
// public int ContributorId { get; private set; }
// public Contributor Contributor { get; set; }
// public string AudibleAuthorId { get; private set; }
// public void UpdateAudibleAuthorId(string authorId)
// {
// if (!string.IsNullOrWhiteSpace(authorId))
// AudibleAuthorId = authorId;
// }
//}
//// ...and create EF table config
#endregion
}
public override string ToString() => Name;
}
}

View File

@@ -17,5 +17,7 @@ namespace DataLayer
Book = book;
DateAdded = dateAdded;
}
}
public override string ToString() => $"{DateAdded:d} {Book}";
}
}

View File

@@ -72,5 +72,7 @@ namespace DataLayer
return string.Join("\r\n", items);
}
}
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
}
}

View File

@@ -66,5 +66,7 @@ namespace DataLayer
if (_booksLink.SingleOrDefault(sb => sb.Book == book) == null)
_booksLink.Add(new SeriesBook(this, book, index));
}
}
public override string ToString() => Name;
}
}

View File

@@ -34,5 +34,7 @@ namespace DataLayer
if (index.HasValue)
Index = index.Value;
}
}
public override string ToString() => $"Series={Series} Book={Book}";
}
}

View File

@@ -20,5 +20,7 @@ namespace DataLayer
Book = book;
Url = url;
}
}
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
}
}

View File

@@ -78,5 +78,7 @@ namespace DataLayer
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
=> Rating.Update(overallRating, performanceRating, storyRating);
}
public override string ToString() => $"{Book} {Rating} {Tags}";
}
}

View File

@@ -56,12 +56,16 @@ namespace DataLayer
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
modelBuilder.ApplyConfiguration(new CategoryConfig());
// seeds go here. examples in scratch pad
modelBuilder
.Entity<Category>()
.HasData(Category.GetEmpty());
// seeds go here. examples in scratch pad
modelBuilder
.Entity<Category>()
.HasData(Category.GetEmpty());
modelBuilder
.Entity<Contributor>()
.HasData(Contributor.GetEmpty());
// views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types
}
}
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
}
}
}

View File

@@ -4,7 +4,6 @@ using System.Linq;
using Dinah.Core.Collections.Generic;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace DataLayer
{
@@ -14,25 +13,18 @@ namespace DataLayer
public void Executing(DbContext context)
{
// persist tags:
var modifiedEntities = context
var tagsCollection
= context
.ChangeTracker
.Entries()
.Where(p => p.State.In(EntityState.Modified, EntityState.Added))
.ToList();
persistTags(modifiedEntities);
}
private static void persistTags(List<EntityEntry> modifiedEntities)
{
var tagSets = modifiedEntities
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
.Select(e => e.Entity as UserDefinedItem)
// filter by null but NOT by blank. blank is the valid way to show the absence of tags
.Where(a => a != null)
.Where(udi => udi != null)
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
.Select(t => (t.Book.AudibleProductId, t.Tags))
.ToList();
foreach (var t in tagSets)
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
FileManager.TagsPersistence.Save(tagsCollection);
}
}
}

View File

@@ -7,15 +7,22 @@ nuget
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
Microsoft.EntityFrameworkCore.Sqlite
MIGRATIONS require standard, not core
using standard instead of core. edit 3 things in csproj
1of3: pluralize xml TargetFramework tag to TargetFrameworks
2of2: TargetFrameworks from: netstandard2.1
to: netcoreapp3.0;netstandard2.1
3of3: add
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
MIGRATIONS
require core, not standard
this can be a problem b/c standard and framework can only reference standard, not core
TO USE MIGRATIONS (core and/or standard)
add to csproj
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD
edit csproj
pluralize this xml tag
from: TargetFramework
to: TargetFrameworks
inside of TargetFrameworks
from: netstandard2.1
to: netcoreapp3.1;netstandard2.1
run. error
SQLite Error 1: 'no such table: Blogs'.

View File

@@ -3,7 +3,10 @@
"LibationContext_sqlserver": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;",
"// on windows sqlite paths accept windows and/or unix slashes": "",
"// sqlite notes": "",
"// absolute path example": "Data Source=C:/foo/bar/sample.db",
"// relative path example": "Data Source=sample.db",
"// on windows: sqlite paths accept windows and/or unix slashes": "",
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
}
}

View File

@@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class BookImporter : ItemsImporterBase
{
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new BookValidator().Validate(items);
protected override int DoImport(IEnumerable<Item> items, LibationContext context)
{
// pre-req.s
new ContributorImporter().Import(items, context);
new SeriesImporter().Import(items, context);
new CategoryImporter().Import(items, context);
// get distinct
var productIds = items.Select(i => i.ProductId).ToList();
// load db existing => .Local
loadLocal_books(productIds, context);
// upsert
var qtyNew = upsertBooks(items, context);
return qtyNew;
}
private void loadLocal_books(List<string> productIds, LibationContext context)
{
var localProductIds = context.Books.Local.Select(b => b.AudibleProductId);
var remainingProductIds = productIds
.Distinct()
.Except(localProductIds)
.ToList();
// GetBooks() eager loads Series, category, et al
if (remainingProductIds.Any())
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
}
private int upsertBooks(IEnumerable<Item> items, LibationContext context)
{
var qtyNew = 0;
foreach (var item in items)
{
var book = context.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.ProductId);
if (book is null)
{
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var authors = item
.Authors
.Select(a => context.Contributors.Local.Single(c => a.Name == c.Name))
.ToList();
// if no narrators listed, author is the narrator
if (item.Narrators is null || !item.Narrators.Any())
item.Narrators = item.Authors;
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var narrators = item
.Narrators
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
book = context.Books.Add(new Book(
new AudibleProductId(item.ProductId),
item.Title,
item.Description,
item.LengthInMinutes,
authors,
narrators)
).Entity;
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
qtyNew++;
}
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
//
// this was round 1 when it was a 2 step process
//
//// update series even for existing books. these are occasionally updated
//var seriesIds = item.Series.Select(kvp => kvp.SeriesId).ToList();
//var allSeries = context.Series.Local.Where(c => seriesIds.Contains(c.AudibleSeriesId)).ToList();
//foreach (var series in allSeries)
// book.UpsertSeries(series);
// these will upsert over library-scraped series, but will not leave orphans
if (item.Series != null)
{
foreach (var seriesEntry in item.Series)
{
var series = context.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
book.UpsertSeries(series, seriesEntry.Index);
}
}
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == item.Categories.LastOrDefault().CategoryId);
if (category != null)
book.UpdateCategory(category, context);
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
}
return qtyNew;
}
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class BookImporter : ItemsImporterBase
{
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new BookValidator().Validate(items);
protected override int DoImport(IEnumerable<Item> items, LibationContext context)
{
// pre-req.s
new ContributorImporter().Import(items, context);
new SeriesImporter().Import(items, context);
new CategoryImporter().Import(items, context);
// get distinct
var productIds = items.Select(i => i.ProductId).ToList();
// load db existing => .Local
loadLocal_books(productIds, context);
// upsert
var qtyNew = upsertBooks(items, context);
return qtyNew;
}
private void loadLocal_books(List<string> productIds, LibationContext context)
{
var localProductIds = context.Books.Local.Select(b => b.AudibleProductId);
var remainingProductIds = productIds
.Distinct()
.Except(localProductIds)
.ToList();
// GetBooks() eager loads Series, category, et al
if (remainingProductIds.Any())
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
}
private int upsertBooks(IEnumerable<Item> items, LibationContext context)
{
var qtyNew = 0;
foreach (var item in items)
{
var book = context.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.ProductId);
if (book is null)
{
book = createNewBook(item, context);
qtyNew++;
}
updateBook(item, book, context);
}
return qtyNew;
}
private static Book createNewBook(Item item, LibationContext context)
{
// absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true)
item.Authors = new[] { new Person { Name = "", Asin = null } };
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var authors = item
.Authors
.Select(a => context.Contributors.Local.Single(c => a.Name == c.Name))
.ToList();
var narrators
= item.Narrators is null || !item.Narrators.Any()
// if no narrators listed, author is the narrator
? authors
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
: item
.Narrators
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
// absence of categories is very rare, but possible
var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";
var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory);
var book = context.Books.Add(new Book(
new AudibleProductId(item.ProductId),
item.Title,
item.Description,
item.LengthInMinutes,
authors,
narrators,
category)
).Entity;
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
return book;
}
private static void updateBook(Item item, Book book, LibationContext context)
{
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
// update series even for existing books. these are occasionally updated
// these will upsert over library-scraped series, but will not leave orphans
if (item.Series != null)
{
foreach (var seriesEntry in item.Series)
{
var series = context.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
book.UpsertSeries(series, seriesEntry.Index);
}
}
}
}
}

View File

@@ -33,8 +33,11 @@ namespace DtoImporterService
.Except(localIds)
.ToList();
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingCategoryIds.Any())
context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId)).ToList();
context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId) || c.Name == emptyName).ToList();
}
// only use after loading contributors => local

View File

@@ -19,10 +19,10 @@ namespace DtoImporterService
var publishers = items.GetPublishersDistinct().ToList();
// load db existing => .Local
var allNames = authors
.Select(a => a.Name)
var allNames = publishers
.Union(authors.Select(n => n.Name))
.Union(narrators.Select(n => n.Name))
.Union(publishers)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
loadLocal_contributors(allNames, context);
@@ -36,9 +36,6 @@ namespace DtoImporterService
private void loadLocal_contributors(List<string> contributorNames, LibationContext context)
{
contributorNames.Remove(null);
contributorNames.Remove("");
//// BAD: very inefficient
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
@@ -50,11 +47,10 @@ namespace DtoImporterService
.ToList();
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingContribNames.Any())
context.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
// _________________________________^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// i tried to extract this pattern, but this part prohibits doing so
// wouldn't work anyway for Books.GetBooks()
context.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
}
// only use after loading contributors => local
@@ -67,11 +63,9 @@ namespace DtoImporterService
var person = context.Contributors.Local.SingleOrDefault(c => c.Name == p.Name);
if (person == null)
{
person = context.Contributors.Add(new Contributor(p.Name)).Entity;
person = context.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
qtyNew++;
}
person.UpdateAudibleAuthorId(p.Asin);
}
return qtyNew;

View File

@@ -20,12 +20,28 @@ namespace DtoImporterService
}
}
var exceptions = Validate(param);
if (exceptions != null && exceptions.Any())
throw new AggregateException($"Device Jobs Service configuration validation failed", exceptions);
try
{
var exceptions = Validate(param);
if (exceptions != null && exceptions.Any())
throw new AggregateException($"Importer validation failed", exceptions);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Import error: validation");
throw;
}
var result = func(param, context);
return result;
try
{
var result = func(param, context);
return result;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Import error: post-validation importing");
throw;
}
}
IEnumerable<Exception> Validate(T param);
}

View File

@@ -17,17 +17,17 @@ namespace DtoImporterService
var series = items.GetSeriesDistinct().ToList();
// load db existing => .Local
var seriesIds = series.Select(s => s.SeriesId).ToList();
loadLocal_series(seriesIds, context);
loadLocal_series(series, context);
// upsert
var qtyNew = upsertSeries(series, context);
return qtyNew;
}
private void loadLocal_series(List<string> seriesIds, LibationContext context)
private void loadLocal_series(List<AudibleApiDTOs.Series> series, LibationContext context)
{
var localIds = context.Series.Local.Select(s => s.AudibleSeriesId);
var seriesIds = series.Select(s => s.SeriesId).ToList();
var localIds = context.Series.Local.Select(s => s.AudibleSeriesId).ToList();
var remainingSeriesIds = seriesIds
.Distinct()
.Except(localIds)

View File

@@ -17,28 +17,22 @@ namespace FileLiberator
/// </summary>
public class BackupBook : IProcessable
{
public event EventHandler<string> Begin;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<string> Completed;
public event EventHandler<LibraryBook> Completed;
public DownloadBook DownloadBook { get; } = new DownloadBook();
public DecryptBook DecryptBook { get; } = new DecryptBook();
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
// ValidateAsync() doesn't need UI context
public async Task<bool> ValidateAsync(LibraryBook libraryBook)
=> await validateAsync_ConfigureAwaitFalse(libraryBook.Book.AudibleProductId).ConfigureAwait(false);
private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId)
=> !await AudibleFileStorage.Audio.ExistsAsync(productId);
public bool Validate(LibraryBook libraryBook)
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
// do NOT use ConfigureAwait(false) on ProcessAsync()
// often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var productId = libraryBook.Book.AudibleProductId;
var displayMessage = $"[{productId}] {libraryBook.Book.Title}";
Begin?.Invoke(this, displayMessage);
Begin?.Invoke(this, libraryBook);
try
{
@@ -64,7 +58,7 @@ namespace FileLiberator
}
finally
{
Completed?.Invoke(this, displayMessage);
Completed?.Invoke(this, libraryBook);
}
}
}

View File

@@ -21,7 +21,7 @@ namespace FileLiberator
/// </summary>
public class DecryptBook : IDecryptable
{
public event EventHandler<string> Begin;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<string> DecryptBegin;
@@ -32,32 +32,27 @@ namespace FileLiberator
public event EventHandler<int> UpdateProgress;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<string> Completed;
public event EventHandler<LibraryBook> Completed;
// ValidateAsync() doesn't need UI context
public async Task<bool> ValidateAsync(LibraryBook libraryBook)
=> await validateAsync_ConfigureAwaitFalse(libraryBook.Book.AudibleProductId).ConfigureAwait(false);
private async Task<bool> validateAsync_ConfigureAwaitFalse(string productId)
=> await AudibleFileStorage.AAX.ExistsAsync(productId)
&& !await AudibleFileStorage.Audio.ExistsAsync(productId);
public bool Validate(LibraryBook libraryBook)
=> AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
// do NOT use ConfigureAwait(false) on ProcessAsync()
// often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
Begin?.Invoke(this, displayMessage);
Begin?.Invoke(this, libraryBook);
try
{
var aaxFilename = await AudibleFileStorage.AAX.GetAsync(libraryBook.Book.AudibleProductId);
var aaxFilename = AudibleFileStorage.AAX.GetPath(libraryBook.Book.AudibleProductId);
if (aaxFilename == null)
return new StatusHandler { "aaxFilename parameter is null" };
if (!FileUtility.FileExists(aaxFilename))
return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" };
if (await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId))
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
@@ -72,14 +67,14 @@ namespace FileLiberator
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
var statusHandler = new StatusHandler();
var finalAudioExists = await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId);
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
statusHandler.AddError("Cannot find final audio file after decryption");
return statusHandler;
}
finally
{
Completed?.Invoke(this, displayMessage);
Completed?.Invoke(this, libraryBook);
}
}
@@ -97,17 +92,16 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
// override default which was set in CreateAsync
converter.SetOutputFilename(proposedOutputFile);
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
// REAL WORK DONE HERE
var success = await Task.Run(() => converter.Run());
// decrypt failed
if (!success)
{
Console.WriteLine("decrypt failed");
return null;
}
Configuration.Instance.DecryptKey = converter.decryptKey;
@@ -120,46 +114,58 @@ namespace FileLiberator
}
private static void moveFilesToBooksDir(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = getDestDir(product);
Directory.CreateDirectory(destinationDir);
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = product.Title.IndexOf(':');
var titleDir = (underscoreIndex < 4) ? product.Title : product.Title.Substring(0, underscoreIndex);
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
Directory.CreateDirectory(finalDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
files = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
var musicFileExt = musicFiles
.Select(f => f.Extension)
.Distinct()
.Single()
.Trim('.');
foreach (var f in sortedFiles)
{
var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f)
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
? FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId)
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
foreach (var f in files)
{
var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f)
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
? FileUtility.GetValidFilename(finalDir, product.Title, musicFileExt, product.AudibleProductId)
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
: FileUtility.GetValidFilename(finalDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
File.Move(f.FullName, dest);
}
}
File.Move(f.FullName, dest);
}
}
}
private static string getDestDir(Book product)
{
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = product.Title.IndexOf(':');
var titleDir
= underscoreIndex < 4
? product.Title
: product.Title.Substring(0, underscoreIndex);
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
return finalDir;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
var sortedFiles = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
return sortedFiles;
}
}
}

View File

@@ -17,16 +17,16 @@ namespace FileLiberator
/// </summary>
public class DownloadBook : DownloadableBase
{
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)
&& !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
public override bool Validate(LibraryBook libraryBook)
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var tempAaxFilename = getDownloadPath(libraryBook);
var actualFilePath = await downloadBookAsync(libraryBook, tempAaxFilename);
moveBook(libraryBook, actualFilePath);
return await verifyDownloadAsync(libraryBook);
return verifyDownload(libraryBook);
}
private static string getDownloadPath(LibraryBook libraryBook)
@@ -44,6 +44,14 @@ namespace FileLiberator
tempAaxFilename,
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
// if bad file download, a 0-33 byte file will be created
System.Threading.Thread.Sleep(100);
if (new FileInfo(actualFilePath).Length < 100)
{
File.Delete(actualFilePath);
throw new Exception("Error downloading file");
}
return actualFilePath;
}
@@ -58,8 +66,8 @@ namespace FileLiberator
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
}
private static async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId)
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded AAX file cannot be found" }
: new StatusHandler();
}

View File

@@ -12,26 +12,26 @@ namespace FileLiberator
{
public class DownloadPdf : DownloadableBase
{
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
&& !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId);
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var proposedDownloadFilePath = await getProposedDownloadFilePathAsync(libraryBook);
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
return await verifyDownloadAsync(libraryBook);
return verifyDownload(libraryBook);
}
private static async Task<string> getProposedDownloadFilePathAsync(LibraryBook libraryBook)
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var destinationDir =
// this is safe b/c GetDirectoryName(null) == null
Path.GetDirectoryName(await AudibleFileStorage.Audio.GetAsync(libraryBook.Book.AudibleProductId))
Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId))
?? AudibleFileStorage.PDF.StorageDirectory;
return Path.Combine(destinationDir, Path.GetFileName(getdownloadUrl(libraryBook)));
@@ -45,8 +45,8 @@ namespace FileLiberator
(p) => client.DownloadFileAsync(getdownloadUrl(libraryBook), proposedDownloadFilePath, p));
}
private static async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId)
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}

View File

@@ -8,8 +8,8 @@ namespace FileLiberator
{
public abstract class DownloadableBase : IDownloadable
{
public event EventHandler<string> Begin;
public event EventHandler<string> Completed;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> DownloadBegin;
public event EventHandler<DownloadProgress> DownloadProgressChanged;
@@ -18,7 +18,7 @@ namespace FileLiberator
public event EventHandler<string> StatusUpdate;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
public abstract Task<bool> ValidateAsync(LibraryBook libraryBook);
public abstract bool Validate(LibraryBook libraryBook);
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
@@ -26,9 +26,7 @@ namespace FileLiberator
// often calls events which prints to forms in the UI context
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var displayMessage = $"[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}";
Begin?.Invoke(this, displayMessage);
Begin?.Invoke(this, libraryBook);
try
{
@@ -36,7 +34,7 @@ namespace FileLiberator
}
finally
{
Completed?.Invoke(this, displayMessage);
Completed?.Invoke(this, libraryBook);
}
}

View File

@@ -1,11 +1,12 @@
using System;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
public interface IDownloadable : IProcessable
{
event EventHandler<string> DownloadBegin;
event EventHandler<Dinah.Core.Net.Http.DownloadProgress> DownloadProgressChanged;
event EventHandler<DownloadProgress> DownloadProgressChanged;
event EventHandler<string> DownloadCompleted;
}
}

View File

@@ -7,15 +7,15 @@ namespace FileLiberator
{
public interface IProcessable
{
event EventHandler<string> Begin;
event EventHandler<LibraryBook> Begin;
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
event EventHandler<string> StatusUpdate;
event EventHandler<string> Completed;
event EventHandler<LibraryBook> Completed;
/// <returns>True == Valid</returns>
Task<bool> ValidateAsync(LibraryBook libraryBook);
/// <returns>True == Valid</returns>
bool Validate(LibraryBook libraryBook);
/// <returns>True == success</returns>
Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
@@ -9,8 +10,7 @@ namespace FileLiberator
{
//
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
// - ValidateAsync() doesn't need UI context. however, each class already uses ConfigureAwait(false)
// - ProcessAsync() often does a lot with forms in the UI context
// ProcessAsync() often does a lot with forms in the UI context
//
@@ -18,31 +18,54 @@ namespace FileLiberator
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
public static async Task<StatusHandler> ProcessFirstValidAsync(this IProcessable processable)
{
var libraryBook = await processable.GetNextValidAsync();
var libraryBook = processable.getNextValidBook();
if (libraryBook == null)
return null;
// this should never happen. check anyway. ProcessFirstValidAsync returning null is the signal that we're done. we can't let another IProcessable accidentally send this commans
var status = await processable.ProcessAsync(libraryBook);
return await processBookAsync(processable, libraryBook);
}
/// <summary>Process the first valid product. Create default context</summary>
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, string productId)
{
using var context = LibationContext.Create();
var libraryBook = context
.Library
.GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
if (libraryBook == null)
return null;
if (!processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" };
return await processBookAsync(processable, libraryBook);
}
private static async Task<StatusHandler> processBookAsync(IProcessable processable, LibraryBook libraryBook)
{
// this should never happen. check anyway. ProcessFirstValidAsync returning null is the signal that we're done. we can't let another IProcessable accidentally send this command
var status = await processable.ProcessAsync(libraryBook);
if (status == null)
throw new Exception("Processable should never return a null status");
return status;
}
public static async Task<LibraryBook> GetNextValidAsync(this IProcessable processable)
private static LibraryBook getNextValidBook(this IProcessable processable)
{
var libraryBooks = LibraryQueries.GetLibrary_Flat_NoTracking();
foreach (var libraryBook in libraryBooks)
if (await processable.ValidateAsync(libraryBook))
if (processable.Validate(libraryBook))
return libraryBook;
return null;
}
}
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
=> await processable.ValidateAsync(libraryBook)
=> processable.Validate(libraryBook)
? await processable.ProcessAsync(libraryBook)
: new StatusHandler();
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="7.1.1" />
<PackageReference Include="Polly" Version="7.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using Dinah.Core;
using Dinah.Core.Collections.Generic;
@@ -21,23 +21,6 @@ namespace FileManager
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
#region static
// centralize filetype mappings to ensure uniqueness
private static Dictionary<string, FileType> extensionMap => new Dictionary<string, FileType>
{
[".m4b"] = FileType.Audio,
[".mp3"] = FileType.Audio,
[".aac"] = FileType.Audio,
[".mp4"] = FileType.Audio,
[".m4a"] = FileType.Audio,
[".ogg"] = FileType.Audio,
[".flac"] = FileType.Audio,
[".aax"] = FileType.AAX,
[".pdf"] = FileType.PDF,
[".zip"] = FileType.PDF,
};
public static AudibleFileStorage Audio { get; }
public static AudibleFileStorage AAX { get; }
public static AudibleFileStorage PDF { get; }
@@ -81,9 +64,9 @@ namespace FileManager
// must do this in static ctor, not w/inline properties
// static properties init before static ctor so these dir.s would still be null
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory);
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal);
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory);
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory, "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac");
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal, "aax");
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory, "pdf", "zip");
}
#endregion
@@ -92,9 +75,14 @@ namespace FileManager
public string StorageDirectory => DisplayName;
public IEnumerable<string> Extensions => extensionMap.Where(kvp => kvp.Value == FileType).Select(kvp => kvp.Key);
private IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
private AudibleFileStorage(FileType fileType, string storageDirectory) : base((int)fileType, storageDirectory) { }
private AudibleFileStorage(FileType fileType, string storageDirectory, params string[] extensions) : base((int)fileType, storageDirectory)
{
extensions_noDots = extensions.Select(ext => ext.Trim('.')).ToList();
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
}
/// <summary>
/// Example for full books:
@@ -102,78 +90,30 @@ namespace FileManager
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public async Task<bool> ExistsAsync(string productId)
=> (await GetAsync(productId).ConfigureAwait(false)) != null;
public bool Exists(string productId)
=> GetPath(productId) != null;
public async Task<string> GetAsync(string productId)
=> await getAsync(productId).ConfigureAwait(false);
private async Task<string> getAsync(string productId)
public string GetPath(string productId)
{
{
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
}
// this is how files are saved by default. check this method first
{
var diskFile_byDirName = (await Task.Run(() => getFile_checkDirName(productId)).ConfigureAwait(false));
if (diskFile_byDirName != null)
{
FilePathCache.Upsert(productId, FileType, diskFile_byDirName);
return diskFile_byDirName;
}
}
var firstOrNull =
Directory
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
{
var diskFile_byFileName = (await Task.Run(() => getFile_checkFileName(productId, StorageDirectory, SearchOption.AllDirectories)).ConfigureAwait(false));
if (diskFile_byFileName != null)
{
FilePathCache.Upsert(productId, FileType, diskFile_byFileName);
return diskFile_byFileName;
}
}
return null;
if (firstOrNull is null)
return null;
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
}
// returns audio file if there is a directory where both are true
// - the directory name contains the productId
// - the directory contains an audio file in it's top dir (not recursively)
private string getFile_checkDirName(string productId)
{
foreach (var d in Directory.EnumerateDirectories(StorageDirectory, "*.*", SearchOption.AllDirectories))
{
if (!fileHasId(d, productId))
continue;
var firstAudio = Directory
.EnumerateFiles(d, "*.*", SearchOption.TopDirectoryOnly)
.FirstOrDefault(f => IsFileTypeMatch(f));
if (firstAudio != null)
return firstAudio;
}
return null;
}
// returns audio file if there is an file where both are true
// - the file name contains the productId
// - the file is an audio type
private string getFile_checkFileName(string productId, string dir, SearchOption searchOption)
=> Directory
.EnumerateFiles(dir, "*.*", searchOption)
.FirstOrDefault(f => fileHasId(f, productId) && IsFileTypeMatch(f));
public bool IsFileTypeMatch(string filename)
=> Extensions.ContainsInsensative(Path.GetExtension(filename));
public bool IsFileTypeMatch(FileInfo fileInfo)
=> Extensions.ContainsInsensative(fileInfo.Extension);
// use GetFileName, NOT GetFileNameWithoutExtension. This tests files AND directories. if the dir has a dot in the final part of the path, it will be treated like the file extension
private static bool fileHasId(string file, string productId)
=> Path.GetFileName(file).ContainsInsensitive(productId);
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion
}
}

View File

@@ -141,10 +141,10 @@ namespace FileManager
private static string getNonDevelopmentDir(string path)
{
// examples:
// \Libation\Core2_0\bin\Debug\netcoreapp3.0
// \Libation\Core2_0\bin\Debug\netcoreapp3.1
// \Libation\StndLib\bin\Debug\netstandard2.1
// \Libation\MyWnfrm\bin\Debug
// \Libation\Core2_0\bin\Release\netcoreapp3.0
// \Libation\Core2_0\bin\Release\netcoreapp3.1
// \Libation\StndLib\bin\Release\netstandard2.1
// \Libation\MyWnfrm\bin\Release

View File

@@ -2,11 +2,12 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core.Collections.Immutable;
using Newtonsoft.Json;
namespace FileManager
{
public static class FilePathCache
public static class FilePathCache
{
internal class CacheEntry
{
@@ -15,22 +16,25 @@ namespace FileManager
public string Path { get; set; }
}
static List<CacheEntry> inMemoryCache = new List<CacheEntry>();
static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
static FilePathCache()
{
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (FileUtility.FileExists(JsonFile))
inMemoryCache = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
if (FileUtility.FileExists(JsonFile))
{
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
cache = new Cache<CacheEntry>(list);
}
}
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
public static string GetPath(string id, FileType type)
{
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry == null)
return null;
@@ -44,51 +48,47 @@ namespace FileManager
return entry.Path;
}
private static object locker { get; } = new object();
private static void remove(CacheEntry entry)
{
lock (locker)
{
inMemoryCache.Remove(entry);
save();
}
}
{
cache.Remove(entry);
save();
}
public static void Upsert(string id, FileType type, string path)
{
if (!FileUtility.FileExists(path))
throw new FileNotFoundException("Cannot add path to cache. File not found");
lock (locker)
{
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry != null)
entry.Path = path;
else
{
entry = new CacheEntry { Id = id, FileType = type, Path = path };
inMemoryCache.Add(entry);
}
save();
}
}
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
// ONLY call this within lock()
private static void save()
{
// create json if not exists
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryCache, Formatting.Indented));
try { resave(); }
catch (IOException)
{
try { resave(); }
catch (IOException)
{
Console.WriteLine("...that's not good");
throw;
}
}
if (entry is null)
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
else
entry.Path = path;
save();
}
// cache is thread-safe and lock free. but file saving is not
private static object locker { get; } = new object();
private static void save()
{
// create json if not exists
static void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
lock (locker)
{
try { resave(); }
catch (IOException)
{
try { resave(); }
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
throw;
}
}
}
}
}
}

View File

@@ -38,6 +38,8 @@ namespace FileManager
timer.Elapsed += (_, __) => timerDownload();
}
public static event EventHandler<string> PictureCached;
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
{
@@ -86,6 +88,8 @@ namespace FileManager
var bytes = downloadBytes(def);
saveFile(def, bytes);
cache[def] = bytes;
PictureCached?.Invoke(nameof(PictureStorage), def.PictureId);
}
finally
{

View File

@@ -105,12 +105,12 @@ namespace FileManager
catch (IOException)
{
try { resave(); }
catch (IOException)
{
Console.WriteLine("...that's not good");
throw;
}
}
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, "Error saving QuickFilters.json");
throw;
}
}
}
}
}

View File

@@ -27,11 +27,13 @@ namespace FileManager
= Policy.Handle<Exception>()
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
public static void Save(string productId, string tags)
public static void Save(IEnumerable<(string productId, string tags)> tagsCollection)
{
ensureCache();
cache[productId] = tags;
// on initial reload, there's a huge benefit to adding to cache individually then updating the file only once
foreach ((string productId, string tags) in tagsCollection)
cache[productId] = tags;
lock (locker)
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));

View File

@@ -27,11 +27,24 @@ namespace InternalUtilities
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS
});
// important! use this convert method
var libResult = LibraryDtoV10.FromJson(page.ToString());
var pageStr = page.ToString();
LibraryDtoV10 libResult;
try
{
// important! use this convert method
libResult = LibraryDtoV10.FromJson(pageStr);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error converting library for importing use. Full library:\r\n" + pageStr);
throw;
}
if (!libResult.Items.Any())
break;
else
Serilog.Log.Logger.Debug($"Page {i}: {libResult.Items.Length} results");
allItems.AddRange(libResult.Items);
}

View File

@@ -29,12 +29,12 @@ namespace InternalUtilities
{
var exceptions = new List<Exception>();
// a book having no authors is rare but allowed
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items)));
if (items.Any(i => string.IsNullOrWhiteSpace(i.Title)))
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.Title)}", nameof(items)));
if (items.Any(i => i.Authors is null))
exceptions.Add(new ArgumentException($"Collection contains item(s) with null {nameof(Item.Authors)}", nameof(items)));
return exceptions;
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>

View File

@@ -6,5 +6,5 @@
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="GridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>WinFormsDesigner.GridEntry, LibationWinForm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
<TypeInfo>LibationWinForm.GridEntry, LibationWinForm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -6,17 +6,38 @@ namespace LibationWinForm.BookLiberation
{
public partial class AutomatedBackupsForm : Form
{
public bool KeepGoingIsChecked => keepGoingCb.Checked;
public bool KeepGoingVisible
{
get => keepGoingCb.Visible;
set => keepGoingCb.Visible = value;
}
public bool KeepGoingChecked => keepGoingCb.Checked;
public bool KeepGoing
=> keepGoingCb.Visible
&& keepGoingCb.Enabled
&& keepGoingCb.Checked;
public AutomatedBackupsForm()
{
InitializeComponent();
}
public void AppendError(Exception ex) => AppendText("ERROR: " + ex.Message);
public void AppendText(string text) => logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
public void AppendError(Exception ex)
{
Serilog.Log.Logger.Error(ex, "Automated backup: error");
appendText("ERROR: " + ex.Message);
}
public void AppendText(string text)
{
Serilog.Log.Logger.Debug($"Automated backup: {text}");
appendText(text);
}
private void appendText(string text)
=> logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
public void FinalizeUI()
public void FinalizeUI()
{
keepGoingCb.Enabled = false;
logTb.AppendText("");

View File

@@ -8,18 +8,19 @@ namespace LibationWinForm.BookLiberation
{
public partial class DecryptForm : Form
{
public DecryptForm()
public DecryptForm()
{
InitializeComponent();
}
System.IO.TextWriter origOut = Console.Out;
System.IO.TextWriter origOut { get; } = Console.Out;
private void DecryptForm_Load(object sender, EventArgs e)
{
// redirect Console.WriteLine to console, textbox
System.IO.TextWriter origOut = Console.Out;
var controlWriter = new RichTextBoxTextWriter(this.rtbLog);
var multiLogger = new MultiTextWriter(origOut, controlWriter);
var multiLogger = new MultiTextWriter(
origOut,
new RichTextBoxTextWriter(this.rtbLog),
new SerilogTextWriter());
Console.SetOut(multiLogger);
}
@@ -58,13 +59,6 @@ namespace LibationWinForm.BookLiberation
public void SetCoverImage(byte[] coverBytes)
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes));
public static void AppendError(Exception ex) => AppendText("ERROR: " + ex.Message);
public static void AppendText(string text) =>
// redirected to log textbox
Console.WriteLine($"{DateTime.Now} {text}")
//logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"))
;
public void UpdateProgress(int percentage) => progressBar1.UIThread(() => progressBar1.Value = percentage);
public void UpdateProgress(int percentage) => progressBar1.UIThread(() => progressBar1.Value = percentage);
}
}

View File

@@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileLiberator;
namespace LibationWinForm.BookLiberation
{
public class BookLiberatorControllerExamples
{
async Task BackupBookAsync(string productId)
{
using var context = LibationContext.Create();
var libraryBook = context
.Library
.GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
if (libraryBook == null)
return;
var backupBook = new BackupBook();
backupBook.DownloadBook.Completed += SetBackupCountsAsync;
backupBook.DecryptBook.Completed += SetBackupCountsAsync;
await ProcessValidateLibraryBookAsync(backupBook, libraryBook);
}
static async Task<StatusHandler> ProcessValidateLibraryBookAsync(IProcessable processable, LibraryBook libraryBook)
{
if (!await processable.ValidateAsync(libraryBook))
return new StatusHandler { "Validation failed" };
return await processable.ProcessAsync(libraryBook);
}
// Download First Book (Download encrypted/DRM file)
async Task DownloadFirstBookAsync()
{
var downloadBook = ProcessorAutomationController.GetWiredUpDownloadBook();
downloadBook.Completed += SetBackupCountsAsync;
await downloadBook.ProcessFirstValidAsync();
}
// Decrypt First Book (Remove DRM from downloaded file)
async Task DecryptFirstBookAsync()
{
var decryptBook = ProcessorAutomationController.GetWiredUpDecryptBook();
decryptBook.Completed += SetBackupCountsAsync;
await decryptBook.ProcessFirstValidAsync();
}
// Backup First Book (Decrypt a non-liberated book. Download if needed)
async Task BackupFirstBookAsync()
{
var backupBook = ProcessorAutomationController.GetWiredUpBackupBook();
backupBook.DownloadBook.Completed += SetBackupCountsAsync;
backupBook.DecryptBook.Completed += SetBackupCountsAsync;
await backupBook.ProcessFirstValidAsync();
}
async void SetBackupCountsAsync(object obj, string str) => throw new NotImplementedException();
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileLiberator;
namespace LibationWinForm.BookLiberation
@@ -12,37 +14,38 @@ namespace LibationWinForm.BookLiberation
// 1) we can't forget to do it
// 2) we can't accidentally do it mult times becaues we lost track of complexity
//
public static BackupBook GetWiredUpBackupBook()
public static BackupBook GetWiredUpBackupBook(EventHandler<LibraryBook> completedAction = null)
{
var backupBook = new BackupBook();
backupBook.DownloadBook.Begin += (_, __) => wireUpDownloadable(backupBook.DownloadBook);
backupBook.DecryptBook.Begin += (_, __) => wireUpDecryptable(backupBook.DecryptBook);
backupBook.DownloadPdf.Begin += (_, __) => wireUpDecryptable(backupBook.DecryptBook);
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook);
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
return backupBook;
if (completedAction != null)
{
backupBook.DownloadBook.Completed += completedAction;
backupBook.DecryptBook.Completed += completedAction;
backupBook.DownloadPdf.Completed += completedAction;
}
return backupBook;
}
public static DecryptBook GetWiredUpDecryptBook()
{
var decryptBook = new DecryptBook();
decryptBook.Begin += (_, __) => wireUpDecryptable(decryptBook);
return decryptBook;
}
public static DownloadBook GetWiredUpDownloadBook()
{
var downloadBook = new DownloadBook();
downloadBook.Begin += (_, __) => wireUpDownloadable(downloadBook);
return downloadBook;
}
public static DownloadPdf GetWiredUpDownloadPdf()
public static DownloadPdf GetWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction = null)
{
var downloadPdf = new DownloadPdf();
downloadPdf.Begin += (_, __) => wireUpDownloadable(downloadPdf);
downloadPdf.Begin += (_, __) => wireUpEvents(downloadPdf);
if (completedAction != null)
downloadPdf.Completed += completedAction;
return downloadPdf;
}
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
private static void wireUpDownloadable(IDownloadable downloadable)
private static void wireUpEvents(IDownloadable downloadable)
{
#region create form
var downloadDialog = new DownloadForm();
@@ -82,7 +85,7 @@ namespace LibationWinForm.BookLiberation
// unless we dispose, if the form is created but un-used/never-shown then weird UI stuff can happen
// also, since event unsubscribe occurs on FormClosing and an unused form is never closed, then the events will never be unsubscribed
void dialogDispose(object _, string __)
void dialogDispose(object _, object __)
{
if (!downloadDialog.IsDisposed)
downloadDialog.Dispose();
@@ -106,7 +109,7 @@ namespace LibationWinForm.BookLiberation
}
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
private static void wireUpDecryptable(IDecryptable decryptBook)
private static void wireUpEvents(IDecryptable decryptBook)
{
#region create form
var decryptDialog = new DecryptForm();
@@ -153,17 +156,23 @@ namespace LibationWinForm.BookLiberation
#endregion
}
public static async Task RunAutomaticDownload(IDownloadable downloadable)
public static async Task RunAutomaticDownloadAsync(IDownloadable downloadable)
{
AutomatedBackupsForm automatedBackupsForm = attachToBackupsForm(downloadable);
await runBackupLoopAsync(downloadable, automatedBackupsForm);
}
private static AutomatedBackupsForm attachToBackupsForm(IDownloadable downloadable)
{
#region create form
var automatedBackupsForm = new AutomatedBackupsForm();
#endregion
#region define how model actions will affect form behavior
void begin(object _, string str) => automatedBackupsForm.AppendText("Begin: " + str);
void begin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
// extra line after book is completely finished
void completed(object _, string str) => automatedBackupsForm.AppendText("Completed: " + str + Environment.NewLine);
void completed(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
@@ -182,42 +191,55 @@ namespace LibationWinForm.BookLiberation
};
#endregion
await runBackupLoop(downloadable, automatedBackupsForm);
return automatedBackupsForm;
}
public static async Task RunAutomaticBackup(BackupBook backupBook)
public static async Task RunAutomaticBackupAsync(BackupBook backupBook)
{
var automatedBackupsForm = attachToBackupsForm(backupBook);
await runBackupLoopAsync(backupBook, automatedBackupsForm);
}
public static async Task RunSingleBackupAsync(BackupBook backupBook, string productId)
{
var automatedBackupsForm = attachToBackupsForm(backupBook);
automatedBackupsForm.KeepGoingVisible = false;
await runSingleBackupAsync(backupBook, automatedBackupsForm, productId);
}
private static AutomatedBackupsForm attachToBackupsForm(BackupBook backupBook)
{
#region create form
var automatedBackupsForm = new AutomatedBackupsForm();
#endregion
#region define how model actions will affect form behavior
void downloadBookBegin(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Begin: " + str);
void downloadBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
void downloadBookCompleted(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Completed: " + str);
void decryptBookBegin(object _, string str) => automatedBackupsForm.AppendText("DecryptStep_Begin: " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void decryptBookCompleted(object _, string str) => automatedBackupsForm.AppendText("DecryptStep_Completed: " + str + Environment.NewLine);
void downloadPdfBegin(object _, string str) => automatedBackupsForm.AppendText("PdfStep_Begin: " + str);
// extra line after book is completely finished
void downloadPdfCompleted(object _, string str) => automatedBackupsForm.AppendText("PdfStep_Completed: " + str + Environment.NewLine);
#endregion
void decryptBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void downloadPdfCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
backupBook.DownloadBook.Begin += downloadBookBegin;
#region subscribe new form to model's events
backupBook.DownloadBook.Begin += downloadBookBegin;
backupBook.DownloadBook.StatusUpdate += statusUpdate;
backupBook.DownloadBook.Completed += downloadBookCompleted;
backupBook.DecryptBook.Begin += decryptBookBegin;
backupBook.DecryptBook.StatusUpdate += statusUpdate;
backupBook.DecryptBook.Completed += decryptBookCompleted;
backupBook.DownloadPdf.Begin += downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate += statusUpdate;
backupBook.DownloadPdf.Completed += downloadPdfCompleted;
#endregion
backupBook.DownloadPdf.Begin += downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate += statusUpdate;
backupBook.DownloadPdf.Completed += downloadPdfCompleted;
#endregion
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
automatedBackupsForm.FormClosing += (_, __) =>
#region when form closes, unsubscribe from model's events
// unsubscribe so disposed forms aren't still trying to receive notifications
automatedBackupsForm.FormClosing += (_, __) =>
{
backupBook.DownloadBook.Begin -= downloadBookBegin;
backupBook.DownloadBook.StatusUpdate -= statusUpdate;
@@ -225,48 +247,29 @@ namespace LibationWinForm.BookLiberation
backupBook.DecryptBook.Begin -= decryptBookBegin;
backupBook.DecryptBook.StatusUpdate -= statusUpdate;
backupBook.DecryptBook.Completed -= decryptBookCompleted;
backupBook.DownloadPdf.Begin -= downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate -= statusUpdate;
backupBook.DownloadPdf.Completed -= downloadPdfCompleted;
};
backupBook.DownloadPdf.Begin -= downloadPdfBegin;
backupBook.DownloadPdf.StatusUpdate -= statusUpdate;
backupBook.DownloadPdf.Completed -= downloadPdfCompleted;
};
#endregion
await runBackupLoop(backupBook, automatedBackupsForm);
return automatedBackupsForm;
}
// automated backups looper feels like a composible IProcessable: logic, UI, begin + process child + end
// however the process step doesn't follow the pattern: Validate(product) + Process(product)
private static async Task runBackupLoop(IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
private static async Task runBackupLoopAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
{
automatedBackupsForm.Show();
try
{
do
var shouldContinue = true;
while (shouldContinue)
{
var statusHandler = await processable.ProcessFirstValidAsync();
if (statusHandler == null)
{
automatedBackupsForm.AppendText("Done. All books have been processed");
break;
}
if (statusHandler.HasErrors)
{
automatedBackupsForm.AppendText("ERROR. All books have not been processed. Most recent valid book: processing failed");
foreach (var errorMessage in statusHandler.Errors)
automatedBackupsForm.AppendText(errorMessage);
break;
}
if (!automatedBackupsForm.KeepGoingIsChecked)
{
automatedBackupsForm.AppendText("'Keep going' is unchecked");
break;
}
shouldContinue = validateStatus(statusHandler, automatedBackupsForm);
}
while (automatedBackupsForm.KeepGoingIsChecked);
}
catch (Exception ex)
{
@@ -275,5 +278,48 @@ namespace LibationWinForm.BookLiberation
automatedBackupsForm.FinalizeUI();
}
private static async Task runSingleBackupAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm, string productId)
{
automatedBackupsForm.Show();
try
{
var statusHandler = await processable.ProcessSingleAsync(productId);
validateStatus(statusHandler, automatedBackupsForm);
}
catch (Exception ex)
{
automatedBackupsForm.AppendError(ex);
}
automatedBackupsForm.FinalizeUI();
}
private static bool validateStatus(StatusHandler statusHandler, AutomatedBackupsForm automatedBackupsForm)
{
if (statusHandler == null)
{
automatedBackupsForm.AppendText("Done. All books have been processed");
return false;
}
if (statusHandler.HasErrors)
{
automatedBackupsForm.AppendText("ERROR. All books have not been processed. Most recent valid book: processing failed");
foreach (var errorMessage in statusHandler.Errors)
automatedBackupsForm.AppendText(errorMessage);
return false;
}
if (!automatedBackupsForm.KeepGoing)
{
if (automatedBackupsForm.KeepGoingVisible && !automatedBackupsForm.KeepGoingChecked)
automatedBackupsForm.AppendText("'Keep going' is unchecked");
return false;
}
return true;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Windows.Forms;
using System;
using System.Windows.Forms;
using ApplicationServices;
namespace LibationWinForm
@@ -18,11 +19,12 @@ namespace LibationWinForm
{
try
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportLibraryAsync(new Login.WinformResponder());
}
catch
catch (Exception ex)
{
MessageBox.Show("Error importing library. Please try again. If this happens after 2 or 3 tries, contact administrator", "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
var msg = "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator";
MessageBox.Show(msg, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
this.Close();

View File

@@ -94,7 +94,6 @@
this.MinimizeBox = false;
this.Name = "AudibleLoginDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Audible Login";
this.ResumeLayout(false);

View File

@@ -84,7 +84,6 @@
this.MinimizeBox = false;
this.Name = "CaptchaDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).EndInit();

View File

@@ -74,7 +74,6 @@
this.MinimizeBox = false;
this.Name = "_2faCodeDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "2FA Code";
this.ResumeLayout(false);

View File

@@ -37,7 +37,7 @@ namespace LibationWinForm
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
}
private async void Form1_Load(object sender, EventArgs e)
private void Form1_Load(object sender, EventArgs e)
{
// call static ctor. There are bad race conditions if static ctor is first executed when we're running in parallel in setBackupCountsAsync()
var foo = FilePathCache.JsonFile;
@@ -59,7 +59,7 @@ namespace LibationWinForm
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
await setBackupCountsAsync();
setBackupCounts(null, null);
}
}
@@ -86,69 +86,53 @@ namespace LibationWinForm
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.BackupCountsChanged -= setBackupCounts;
currProductsGrid.Dispose();
}
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
currProductsGrid.VisibleCountChanged += setVisibleCount;
currProductsGrid.BackupCountsChanged += setBackupCounts;
gridPanel.Controls.Add(currProductsGrid);
currProductsGrid.Display();
}
ResumeLayout();
}
#endregion
#endregion
#region bottom: qty books visible
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
#endregion
#endregion
#region bottom: backup counts
private async Task setBackupCountsAsync()
#region bottom: backup counts
private void setBackupCounts(object _, object __)
{
var books = LibraryQueries.GetLibrary_Flat_NoTracking()
.Select(sp => sp.Book)
.ToList();
await setBookBackupCountsAsync(books).ConfigureAwait(false);
await setPdfBackupCountsAsync(books).ConfigureAwait(false);
}
setBookBackupCounts(books);
setPdfBackupCounts(books);
}
enum AudioFileState { full, aax, none }
private async Task setBookBackupCountsAsync(IEnumerable<Book> books)
private void setBookBackupCounts(IEnumerable<Book> books)
{
var libraryProductIds = books
.Select(b => b.AudibleProductId)
.ToList();
var noProgress = 0;
var downloadedOnly = 0;
var fullyBackedUp = 0;
//// serial
//foreach (var productId in libraryProductIds)
//{
// if (await AudibleFileStorage.Audio.ExistsAsync(productId))
// fullyBackedUp++;
// else if (await AudibleFileStorage.AAX.ExistsAsync(productId))
// downloadedOnly++;
// else
// noProgress++;
//}
// parallel
async Task<AudioFileState> getAudioFileStateAsync(string productId)
AudioFileState getAudioFileState(string productId)
{
if (await AudibleFileStorage.Audio.ExistsAsync(productId))
if (AudibleFileStorage.Audio.Exists(productId))
return AudioFileState.full;
if (await AudibleFileStorage.AAX.ExistsAsync(productId))
if (AudibleFileStorage.AAX.Exists(productId))
return AudioFileState.aax;
return AudioFileState.none;
}
var tasks = libraryProductIds.Select(productId => getAudioFileStateAsync(productId));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
fullyBackedUp = results.Count(r => r == AudioFileState.full);
downloadedOnly = results.Count(r => r == AudioFileState.aax);
noProgress = results.Count(r => r == AudioFileState.none);
}
var results = books
.AsParallel()
.Select(b => getAudioFileState(b.AudibleProductId))
.ToList();
var fullyBackedUp = results.Count(r => r == AudioFileState.full);
var downloadedOnly = results.Count(r => r == AudioFileState.aax);
var noProgress = results.Count(r => r == AudioFileState.none);
// update bottom numbers
var pending = noProgress + downloadedOnly;
@@ -166,32 +150,15 @@ namespace LibationWinForm
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
private async Task setPdfBackupCountsAsync(IEnumerable<Book> books)
private void setPdfBackupCounts(IEnumerable<Book> books)
{
var libraryProductIds = books
.Where(b => b.Supplements.Any())
.Select(b => b.AudibleProductId)
.ToList();
int notDownloaded;
int downloaded;
//// serial
//notDownloaded = 0;
//downloaded = 0;
//foreach (var productId in libraryProductIds)
//{
// if (await AudibleFileStorage.PDF.ExistsAsync(productId))
// downloaded++;
// else
// notDownloaded++;
//}
// parallel
var tasks = libraryProductIds.Select(productId => AudibleFileStorage.PDF.ExistsAsync(productId));
var boolResults = await Task.WhenAll(tasks).ConfigureAwait(false);
downloaded = boolResults.Count(r => r);
notDownloaded = boolResults.Count(r => !r);
var boolResults = books
.AsParallel()
.Where(b => b.Supplements.Any())
.Select(b => AudibleFileStorage.PDF.Exists(b.AudibleProductId))
.ToList();
var downloaded = boolResults.Count(r => r);
var notDownloaded = boolResults.Count(r => !r);
// update bottom numbers
var text
@@ -210,8 +177,8 @@ namespace LibationWinForm
}
#endregion
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new Dialogs.SearchSyntaxDialog().ShowDialog();
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new Dialogs.SearchSyntaxDialog().ShowDialog();
private void AddFilterBtn_Click(object sender, EventArgs e)
{
@@ -258,7 +225,7 @@ namespace LibationWinForm
#endregion
#region index menu
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var dialog = new IndexLibraryDialog();
dialog.ShowDialog();
@@ -268,33 +235,25 @@ namespace LibationWinForm
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
// update backup counts if we have new library items
if (newAdded > 0)
await setBackupCountsAsync();
if (totalProcessed > 0)
reloadGrid();
}
#endregion
#region liberate menu
private async void setBackupCountsAsync(object _, string __) => await setBackupCountsAsync();
#endregion
#region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
{
var backupBook = BookLiberation.ProcessorAutomationController.GetWiredUpBackupBook();
backupBook.DownloadBook.Completed += setBackupCountsAsync;
backupBook.DecryptBook.Completed += setBackupCountsAsync;
backupBook.DownloadPdf.Completed += setBackupCountsAsync;
await BookLiberation.ProcessorAutomationController.RunAutomaticBackup(backupBook);
var backupBook = BookLiberation.ProcessorAutomationController.GetWiredUpBackupBook(updateGridRow);
await BookLiberation.ProcessorAutomationController.RunAutomaticBackupAsync(backupBook);
}
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
{
var downloadPdf = BookLiberation.ProcessorAutomationController.GetWiredUpDownloadPdf();
downloadPdf.Completed += setBackupCountsAsync;
await BookLiberation.ProcessorAutomationController.RunAutomaticDownload(downloadPdf);
var downloadPdf = BookLiberation.ProcessorAutomationController.GetWiredUpDownloadPdf(updateGridRow);
await BookLiberation.ProcessorAutomationController.RunAutomaticDownloadAsync(downloadPdf);
}
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
#endregion
#region quick filters menu

View File

@@ -26,15 +26,54 @@ namespace LibationWinForm
[Browsable(false)]
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
// formatReplacements is what gets displayed
[Browsable(false)]
public string Download_Status
{
get
{
var print
= FileManager.AudibleFileStorage.Audio.Exists(book.AudibleProductId) ? "Liberated"
: FileManager.AudibleFileStorage.AAX.Exists(book.AudibleProductId) ? "DRM"
: "NOT d/l'ed";
if (!book.Supplements.Any())
return print;
print += "\r\n";
var downloadStatuses = book.Supplements
.Select(d => FileManager.AudibleFileStorage.PDF.Exists(book.AudibleProductId))
// break delayed execution right now!
.ToList();
var count = downloadStatuses.Count;
if (count == 1)
{
print += downloadStatuses[0]
? "PDF d/l'ed"
: "PDF NOT d/l'ed";
}
else
{
var downloadedCount = downloadStatuses.Count(s => s);
print
+= downloadedCount == count ? $"{count} PDFs d/l'ed"
: downloadedCount == 0 ? $"{count} PDFs NOT d/l'ed"
: $"{downloadedCount} of {count} PDFs d/l'ed";
}
return print;
}
}
// displayValues is what gets displayed
// the value that gets returned from the property is the cell's value
// this allows for the value to be sorted one way and displayed another
// eg:
// orig title: The Computer
// formatReplacement: The Computer
// value for sorting: Computer
private Dictionary<string, string> formatReplacements { get; } = new Dictionary<string, string>();
public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value);
private Dictionary<string, string> displayValues { get; } = new Dictionary<string, string>();
public bool TryDisplayValue(string key, out string value) => displayValues.TryGetValue(key, out value);
public Image Cover =>
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(book.PictureId, FileManager.PictureSize._80x80);
@@ -43,16 +82,8 @@ namespace LibationWinForm
{
get
{
formatReplacements[nameof(Title)] = book.Title;
var sortName = book.Title
.Replace("|", "")
.Replace(":", "")
.ToLowerInvariant();
if (sortName.StartsWith("the ") || sortName.StartsWith("a ") || sortName.StartsWith("an "))
sortName = sortName.Substring(sortName.IndexOf(" ") + 1);
return sortName;
displayValues[nameof(Title)] = book.Title;
return getSortName(book.Title);
}
}
@@ -63,7 +94,7 @@ namespace LibationWinForm
{
get
{
formatReplacements[nameof(Length)]
displayValues[nameof(Length)]
= book.LengthInMinutes == 0
? ""
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
@@ -72,7 +103,29 @@ namespace LibationWinForm
}
}
public string Series => book.SeriesNames;
public string Series
{
get
{
displayValues[nameof(Series)] = book.SeriesNames;
return getSortName(book.SeriesNames);
}
}
private static string[] sortPrefixIgnores { get; } = new[] { "the", "a", "an" };
private static string getSortName(string unformattedName)
{
var sortName = unformattedName
.Replace("|", "")
.Replace(":", "")
.ToLowerInvariant()
.Trim();
if (sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart();
return sortName;
}
private string descriptionCache = null;
public string Description
@@ -111,7 +164,7 @@ namespace LibationWinForm
{
get
{
formatReplacements[nameof(Product_Rating)] = starString(book.Rating);
displayValues[nameof(Product_Rating)] = starString(book.Rating);
return firstScore(book.Rating);
}
}
@@ -120,7 +173,7 @@ namespace LibationWinForm
{
get
{
formatReplacements[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
displayValues[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss");
}
}
@@ -129,7 +182,7 @@ namespace LibationWinForm
{
get
{
formatReplacements[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
displayValues[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
return firstScore(book.UserDefinedItem.Rating);
}
}
@@ -146,8 +199,8 @@ namespace LibationWinForm
get
{
var details = new List<string>();
if (book.HasPdfs)
details.Add("Has PDFs");
if (book.HasPdf)
details.Add("Has PDF");
if (book.IsAbridged)
details.Add("Abridged");
if (book.DatePublished.HasValue)
@@ -162,43 +215,5 @@ namespace LibationWinForm
return string.Join("\r\n", details);
}
}
public string Download_Status
{
get
{
var print
= FileManager.AudibleFileStorage.Audio.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult() ? "Liberated"
: FileManager.AudibleFileStorage.AAX.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult() ? "DRM"
: "NOT d/l'ed";
if (!book.Supplements.Any())
return print;
print += "\r\n";
var downloadStatuses = book.Supplements
.Select(d => FileManager.AudibleFileStorage.PDF.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult())
// break delayed execution right now!
.ToList();
var count = downloadStatuses.Count;
if (count == 1)
{
print += downloadStatuses[0]
? "PDF d/l'ed"
: "PDF NOT d/l'ed";
}
else
{
var downloadedCount = downloadStatuses.Count(s => s);
print
+= downloadedCount == count ? $"{count} PDFs d/l'ed"
: downloadedCount == 0 ? $"{count} PDFs NOT d/l'ed"
: $"{downloadedCount} of {count} PDFs d/l'ed";
}
return print;
}
}
}
}

View File

@@ -1,201 +1,191 @@
namespace LibationWinForm
{
partial class ProductsGrid
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
partial class ProductsGrid
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn12 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(LibationWinForm.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2,
this.dataGridViewTextBoxColumn3,
this.dataGridViewTextBoxColumn4,
this.dataGridViewTextBoxColumn5,
this.dataGridViewTextBoxColumn6,
this.dataGridViewTextBoxColumn7,
this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11,
this.dataGridViewTextBoxColumn12});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// dataGridViewTextBoxColumn12
//
this.dataGridViewTextBoxColumn12.DataPropertyName = "Download_Status";
this.dataGridViewTextBoxColumn12.HeaderText = "Download_Status";
this.dataGridViewTextBoxColumn12.Name = "dataGridViewTextBoxColumn12";
this.dataGridViewTextBoxColumn12.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(LibationWinForm.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2,
this.dataGridViewTextBoxColumn3,
this.dataGridViewTextBoxColumn4,
this.dataGridViewTextBoxColumn5,
this.dataGridViewTextBoxColumn6,
this.dataGridViewTextBoxColumn7,
this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
}
}
#endregion
#endregion
private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn12;
}
private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
@@ -6,6 +7,7 @@ using ApplicationServices;
using DataLayer;
using Dinah.Core.Collections.Generic;
using Dinah.Core.DataBinding;
using Dinah.Core.Windows.Forms;
namespace LibationWinForm
{
@@ -23,27 +25,32 @@ namespace LibationWinForm
// - click on Data Sources > ProductItem. drowdown: DataGridView
// - drag/drop ProductItem on design surface
public partial class ProductsGrid : UserControl
{
public event EventHandler<int> VisibleCountChanged;
{
public event EventHandler<int> VisibleCountChanged;
public event EventHandler BackupCountsChanged;
private const string EDIT_TAGS = "Edit Tags";
private const string LIBERATE = "Liberate";
// alias
private DataGridView dataGridView => gridEntryDataGridView;
private DataGridView dataGridView;
private LibationContext context;
public ProductsGrid()
{
InitializeComponent();
Disposed += (_, __) => { if (context != null) context.Dispose(); };
formatDataGridView();
addLiberateButtons();
addEditTagsButtons();
formatColumns();
Disposed += (_, __) => context?.Dispose();
manageLiveImageUpdateSubscriptions();
}
private bool hasBeenDisplayed = false;
public void Display()
private void formatDataGridView()
{
if (hasBeenDisplayed)
return;
hasBeenDisplayed = true;
dataGridView = gridEntryDataGridView;
dataGridView.Dock = DockStyle.Fill;
dataGridView.AllowUserToAddRows = false;
dataGridView.AllowUserToDeleteRows = false;
@@ -52,26 +59,169 @@ namespace LibationWinForm
dataGridView.DefaultCellStyle.WrapMode = DataGridViewTriState.True;
dataGridView.ReadOnly = true;
dataGridView.RowHeadersVisible = false;
// adjust height for 80x80 pictures.
// this must be done before databinding. or can alter later by iterating through rows
dataGridView.RowTemplate.Height = 82;
dataGridView.CellFormatting += replaceFormatted;
dataGridView.CellFormatting += hiddenFormatting;
// sorting breaks filters. must reapply filters after sorting
dataGridView.Sorted += (_, __) => filter();
}
{ // add tag buttons
var editUserTagsButton = new DataGridViewButtonColumn { HeaderText = "Edit Tags" };
dataGridView.Columns.Add(editUserTagsButton);
#region format text cells. ie: not buttons
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
{
var col = ((DataGridView)sender).Columns[e.ColumnIndex];
if (col is DataGridViewTextBoxColumn textCol && GetGridEntry(e.RowIndex).TryDisplayValue(textCol.Name, out string value))
e.Value = value;
}
// add image and handle click
dataGridView.CellPainting += paintEditTag_TextAndImage;
dataGridView.CellContentClick += dataGridView_GridButtonClick;
private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
var dgv = (DataGridView)sender;
// no action needed for buttons
if (e.RowIndex < 0 || dgv.Columns[e.ColumnIndex] is DataGridViewButtonColumn)
return;
var isHidden = GetGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden");
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].Style
= isHidden
? new DataGridViewCellStyle { ForeColor = Color.LightGray }
: dgv.DefaultCellStyle;
}
#endregion
#region liberation buttons
private void addLiberateButtons()
{
dataGridView.Columns.Insert(0, new DataGridViewButtonColumn { HeaderText = LIBERATE });
dataGridView.CellPainting += liberate_Paint;
dataGridView.CellContentClick += liberate_Click;
}
private void liberate_Paint(object sender, DataGridViewCellPaintingEventArgs e)
{
var dgv = (DataGridView)sender;
if (!isColumnValid(dgv, e.RowIndex, e.ColumnIndex, LIBERATE))
return;
dgv.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = GetGridEntry(e.RowIndex).Download_Status;
}
private async void liberate_Click(object sender, DataGridViewCellEventArgs e)
{
var dgv = (DataGridView)sender;
if (!isColumnValid(dgv, e.RowIndex, e.ColumnIndex, LIBERATE))
return;
var productId = GetGridEntry(e.RowIndex).GetBook().AudibleProductId;
var backupBook = BookLiberation.ProcessorAutomationController.GetWiredUpBackupBook((_, __) => RefreshRow(productId));
await BookLiberation.ProcessorAutomationController.RunSingleBackupAsync(backupBook, productId);
}
#endregion
public void RefreshRow(string productId)
{
var rowId = GetRowId((ge) => ge.GetBook().AudibleProductId == productId);
// update cells incl Liberate button text
dataGridView.InvalidateRow(rowId);
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#region tag buttons
private void addEditTagsButtons()
{
dataGridView.Columns.Add(new DataGridViewButtonColumn { HeaderText = EDIT_TAGS });
dataGridView.CellPainting += editTags_Paint;
dataGridView.CellContentClick += editTags_Click;
}
private void editTags_Paint(object sender, DataGridViewCellPaintingEventArgs e)
{
// DataGridView Image for Button Column: https://stackoverflow.com/a/36253883
var dgv = (DataGridView)sender;
if (!isColumnValid(dgv, e.RowIndex, e.ColumnIndex, EDIT_TAGS))
return;
var displayTags = GetGridEntry(e.RowIndex).TagsEnumerated.ToList();
var cell = dgv.Rows[e.RowIndex].Cells[e.ColumnIndex];
if (displayTags.Any())
cell.Value = string.Join("\r\n", displayTags);
else // no tags: use image
{
// clear tag text
cell.Value = "";
var image = Properties.Resources.edit_tags_25x25;
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
var w = image.Width;
var h = image.Height;
var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2;
var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2;
e.Graphics.DrawImage(image, new Rectangle(x, y, w, h));
e.Handled = true;
}
}
private void editTags_Click(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
var dgv = (DataGridView)sender;
if (!isColumnValid(dgv, e.RowIndex, e.ColumnIndex, EDIT_TAGS))
return;
var liveGridEntry = GetGridEntry(e.RowIndex);
// EditTagsDialog should display better-formatted title
liveGridEntry.TryDisplayValue(nameof(liveGridEntry.Title), out string value);
var editTagsForm = new EditTagsDialog(value, liveGridEntry.Tags);
if (editTagsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
if (qtyChanges == 0)
return;
// force a re-draw, and re-apply filters
// needed to update text colors
dgv.InvalidateRow(e.RowIndex);
filter();
}
#endregion
private static bool isColumnValid(DataGridView dgv, int rowIndex, int colIndex, string colName)
{
var col = dgv.Columns[colIndex];
return rowIndex >= 0 && col.HeaderText == colName && col is DataGridViewButtonColumn;
}
private void formatColumns()
{
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
{
DataGridViewColumn col = dataGridView.Columns[i];
var col = dataGridView.Columns[i];
// initial HeaderText is the lookup name from GridEntry class. any formatting below won't change this
col.Name = col.HeaderText;
@@ -81,35 +231,59 @@ namespace LibationWinForm
col.HeaderText = col.HeaderText.Replace("_", " ");
col.Width = col.Name switch
{
nameof(GridEntry.Cover) => 80,
nameof(GridEntry.Title) => col.Width * 2,
nameof(GridEntry.Misc) => (int)(col.Width * 1.35),
var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8,
_ => col.Width
};
col.Width = col.Name switch
{
nameof(GridEntry.Cover) => 80,
nameof(GridEntry.Title) => col.Width * 2,
nameof(GridEntry.Misc) => (int)(col.Width * 1.35),
var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8,
_ => col.Width
};
}
}
#region live update newly downloaded and cached images
private void manageLiveImageUpdateSubscriptions()
{
FileManager.PictureStorage.PictureCached += crossThreadImageUpdate;
Disposed += (_, __) => FileManager.PictureStorage.PictureCached -= crossThreadImageUpdate;
}
private void crossThreadImageUpdate(object _, string pictureId)
=> dataGridView.UIThread(() => updateRowImage(pictureId));
private void updateRowImage(string pictureId)
{
var rowId = GetRowId((ge) => ge.GetBook().PictureId == pictureId);
if (rowId > -1)
dataGridView.InvalidateRow(rowId);
}
#endregion
private bool hasBeenDisplayed = false;
public void Display()
{
if (hasBeenDisplayed)
return;
hasBeenDisplayed = true;
//
// transform into sorted GridEntry.s BEFORE binding
//
context = LibationContext.Create();
var lib = context.GetLibrary_Flat_WithTracking();
// if no data. hide all columns. return
if (!lib.Any())
{
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
dataGridView.Columns.RemoveAt(i);
return;
}
//
// transform into sorted GridEntry.s BEFORE binding
//
context = LibationContext.Create();
var lib = context.GetLibrary_Flat_WithTracking();
// if no data. hide all columns. return
if (!lib.Any())
{
for (var i = dataGridView.ColumnCount - 1; i >= 0; i--)
dataGridView.Columns.RemoveAt(i);
return;
}
var orderedGridEntries = lib
var orderedGridEntries = lib
.Select(lb => new GridEntry(lb)).ToList()
// default load order
.OrderByDescending(ge => ge.Purchase_Date)
// default load order
.OrderByDescending(ge => ge.Purchase_Date)
//// more advanced example: sort by author, then series, then title
//.OrderBy(ge => ge.Authors)
// .ThenBy(ge => ge.Series)
@@ -125,92 +299,10 @@ namespace LibationWinForm
// FILTER
//
filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
private void paintEditTag_TextAndImage(object sender, DataGridViewCellPaintingEventArgs e)
{
// DataGridView Image for Button Column: https://stackoverflow.com/a/36253883
if (e.RowIndex < 0 || !(((DataGridView)sender).Columns[e.ColumnIndex] is DataGridViewButtonColumn))
return;
var gridEntry = getGridEntry(e.RowIndex);
var displayTags = gridEntry.TagsEnumerated.ToList();
if (displayTags.Any())
dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = string.Join("\r\n", displayTags);
else // no tags: use image
{
// clear tag text
dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = "";
// images from: icons8.com -- search: tags
var image = Properties.Resources.edit_tags_25x25;
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
var w = image.Width;
var h = image.Height;
var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2;
var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2;
e.Graphics.DrawImage(image, new Rectangle(x, y, w, h));
e.Handled = true;
}
}
private void dataGridView_GridButtonClick(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
if (e.RowIndex < 0)
return;
if (sender != dataGridView)
throw new Exception($"{nameof(dataGridView_GridButtonClick)} has incorrect sender ...somehow");
if (!(dataGridView.Columns[e.ColumnIndex] is DataGridViewButtonColumn))
return;
var liveGridEntry = getGridEntry(e.RowIndex);
// EditTagsDialog should display better-formatted title
liveGridEntry.TryGetFormatted(nameof(liveGridEntry.Title), out string value);
var editTagsForm = new EditTagsDialog(value, liveGridEntry.Tags);
if (editTagsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
if (qtyChanges == 0)
return;
// force a re-draw, and re-apply filters
// needed to update text colors
dataGridView.InvalidateRow(e.RowIndex);
filter();
}
#region Cell Formatting
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
{
var col = ((DataGridView)sender).Columns[e.ColumnIndex];
if (col is DataGridViewTextBoxColumn textCol && getGridEntry(e.RowIndex).TryGetFormatted(textCol.Name, out string value))
e.Value = value;
}
private void hiddenFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
var isHidden = getGridEntry(e.RowIndex).TagsEnumerated.Contains("hidden");
dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Style
= isHidden
? new DataGridViewCellStyle { ForeColor = Color.LightGray }
: dataGridView.DefaultCellStyle;
}
#endregion
#region filter
string _filterSearchString;
private void filter() => Filter(_filterSearchString);
@@ -229,15 +321,17 @@ namespace LibationWinForm
currencyManager.SuspendBinding();
{
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).GetBook().AudibleProductId);
dataGridView.Rows[r].Visible = productIds.Contains(GetGridEntry(r).GetBook().AudibleProductId);
}
currencyManager.ResumeBinding();
VisibleCountChanged?.Invoke(this, dataGridView.Rows.Cast<DataGridViewRow>().Count(r => r.Visible));
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
var luceneSearchString_debug = searchResults.SearchString;
}
#endregion
private GridEntry getGridEntry(int rowIndex) => (GridEntry)dataGridView.Rows[rowIndex].DataBoundItem;
}
private int GetRowId(Func<GridEntry, bool> func) => dataGridView.GetRowIdOfBoundItem(func);
private GridEntry GetGridEntry(int rowIndex) => dataGridView.GetBoundItem<GridEntry>(rowIndex);
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Logging;
using Serilog;
namespace LibationWinForm
{
@@ -14,6 +16,8 @@ namespace LibationWinForm
if (!createSettings())
return;
init();
Application.Run(new Form1());
}
@@ -29,7 +33,7 @@ Please fill in a few settings on the following page. You can also change these s
After you make your selections, get started by importing your library.
Go to Import > Scan Library
".Trim();
MessageBox.Show(welcomeText, "Welcom to Libation", MessageBoxButtons.OK);
MessageBox.Show(welcomeText, "Welcome to Libation", MessageBoxButtons.OK);
var dialogResult = new SettingsDialog().ShowDialog();
if (dialogResult != DialogResult.OK)
{
@@ -39,5 +43,32 @@ Go to Import > Scan Library
return true;
}
private static void init()
{
// default. for reference. output example:
// 2019-11-26 08:48:40.224 -05:00 [DBG] Begin Libation
var default_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}";
// with class and method info. output example:
// 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForm.Program.init()) Begin Libation
var code_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
var logPath = System.IO.Path.Combine(FileManager.Configuration.Instance.LibationFiles, "Log.log");
Log.Logger = new LoggerConfiguration()
.Enrich.WithCaller()
.MinimumLevel.Debug()
.WriteTo.File(logPath,
rollingInterval: RollingInterval.Month,
outputTemplate: code_outputTemplate)
.CreateLogger();
Log.Logger.Debug("Begin Libation");
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
}
}
}

View File

@@ -1,6 +1,14 @@
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
https://github.com/rmcrackan/Libation/releases
v3.1-beta.9 : New feature: liberate individual book
v3.1-beta.8 : Bugfix: decrypt file conflict
v3.1-beta.7 : Bugfix: decrypt book with no author
v3.1-beta.6 : Improved logging
v3.1-beta.5 : Improved importing
v3.1-beta.4 : Added beta-specific logging
v3.1-beta.3 : fixed known performance issue: Full-screen grid is slow to respond loading when books aren't liberated
v3.1-beta.2 : fixed known performance issue: Tag add/edit
v3.1-beta.1 : RELEASE TO BETA
v3.0.3 : Switch to SQLite. No longer relies on LocalDB, which must be installed separately
v3.0.2 : Final using LocalDB
@@ -33,24 +41,17 @@ publish win64 platform, single-file
dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
-- end HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES ---------------------------------------------------------------------------------------------------------------------
edit tags icon images from:
icons8.com
search: tags
-- end IMAGES ---------------------------------------------------------------------------------------------------------------------
-- 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
-- end AUDIBLE DETAILS ---------------------------------------------------------------------------------------------------------------------
-- begin SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
core libraries
extend Standard Libraries
additional simple libraries for general purpose programming
utils: domain ignorant
domain: biz logic
db ignorant
db, domain objects
domain internal utilities: domain aware. db ignorant
domain utilities: domain aware. db aware
incl non-db persistence. unless it needs to be tied into a db intercepter
all user-provided data must be backed up to json
application
do NOT combine jsons for
- audible-scraped persistence: library, book details
- libation-generated persistence: FilePaths.json
@@ -74,31 +75,4 @@ aggregate root is transactional boundary
// // test with and without : using TransactionScope scope = new TransactionScope();
//System.Transactions.Transaction.Current.TransactionCompleted += (sender, e) => { };
// also : https://docs.microsoft.com/en-us/dotnet/api/system.transactions.transaction.enlistvolatile
pattern when using 1 db context per form
public Ctor()
{
InitializeComponent();
// dispose context here only. DO NOT dispose in OnParentChanged(). parent form will call dispose after this one has been switched.
// disposing context prematurely can result in:
// The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.
this.Disposed += (_, __) => context?.Dispose();
}
-- end EF CORE ---------------------------------------------------------------------------------------------------------------------
-- begin ASYNC/AWAIT ---------------------------------------------------------------------------------------------------------------------
Using Async and Await to update the UI Thread <20> Stephen Haunts { Coding in the Trenches }
https://stephenhaunts.com/2014/10/14/using-async-and-await-to-update-the-ui-thread/
Using Async and Await to update the UI Thread Part 2 <20> Stephen Haunts { Coding in the Trenches }
https://stephenhaunts.com/2014/10/16/using-async-and-await-to-update-the-ui-thread-part-2/
Simple Async Await Example for Asynchronous Programming <20> Stephen Haunts { Coding in the Trenches }
https://stephenhaunts.com/2014/10/10/simple-async-await-example-for-asynchronous-programming/
Async and Await -- Stephen Cleary's famous intro
https://blog.stephencleary.com/2012/02/async-and-await.html
Async-Await - Best Practices in Asynchronous Programming -- Stephen Cleary
https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
-- end ASYNC/AWAIT ---------------------------------------------------------------------------------------------------------------------

View File

@@ -1,4 +1,4 @@
namespace LibationWinForm_Framework.Dialogs
namespace WinFormsDesigner.Dialogs
{
partial class IndexLibraryDialog
{

View File

@@ -8,7 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForm_Framework.Dialogs
namespace WinFormsDesigner.Dialogs
{
public partial class IndexLibraryDialog : Form
{

View File

@@ -1,4 +1,4 @@
namespace LibationWinForm_Framework.Dialogs.Login
namespace WinFormsDesigner.Dialogs.Login
{
partial class AudibleLoginDialog
{
@@ -93,7 +93,6 @@
this.MinimizeBox = false;
this.Name = "AudibleLoginDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Audible Login";
this.ResumeLayout(false);

View File

@@ -8,7 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForm_Framework.Dialogs.Login
namespace WinFormsDesigner.Dialogs.Login
{
public partial class AudibleLoginDialog : Form
{

View File

@@ -1,4 +1,4 @@
namespace LibationWinForm_Framework.Dialogs.Login
namespace WinFormsDesigner.Dialogs.Login
{
partial class CaptchaDialog
{
@@ -83,7 +83,6 @@
this.MinimizeBox = false;
this.Name = "CaptchaDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).EndInit();

View File

@@ -8,7 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForm_Framework.Dialogs.Login
namespace WinFormsDesigner.Dialogs.Login
{
public partial class CaptchaDialog : Form
{

View File

@@ -73,7 +73,6 @@
this.MinimizeBox = false;
this.Name = "_2faCodeDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "2FA Code";
this.ResumeLayout(false);

View File

@@ -12,6 +12,9 @@ namespace WinFormsDesigner
[Browsable(false)]
public IEnumerable<string> TagsEnumerated { get; set; }
[Browsable(false)]
public string Download_Status { get; set; }
public Image Cover { get; set; }
public string Title { get; set; }
public string Authors { get; set; }
@@ -24,6 +27,5 @@ namespace WinFormsDesigner
public DateTime? Purchase_Date { get; set; }
public string My_Rating { get; set; }
public string Misc { get; set; }
public string Download_Status { get; set; }
}
}

View File

@@ -28,35 +28,34 @@
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn12 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(WinFormsDesigner.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.components = new System.ComponentModel.Container();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(WinFormsDesigner.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2,
@@ -68,134 +67,125 @@
this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11,
this.dataGridViewTextBoxColumn12});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// dataGridViewTextBoxColumn12
//
this.dataGridViewTextBoxColumn12.DataPropertyName = "Download_Status";
this.dataGridViewTextBoxColumn12.HeaderText = "Download_Status";
this.dataGridViewTextBoxColumn12.Name = "dataGridViewTextBoxColumn12";
this.dataGridViewTextBoxColumn12.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
this.dataGridViewTextBoxColumn11});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn12;
}
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
}
}

View File

@@ -6,5 +6,5 @@
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="GridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>LibationWinForm.ProductGrids.GridEntry, LibationWinForm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
<TypeInfo>WinFormsDesigner.GridEntry, WinFormsDesigner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -166,7 +166,7 @@
<EmbeddedResource Include="ProductsGrid.resx">
<DependentUpon>ProductsGrid.cs</DependentUpon>
</EmbeddedResource>
<None Include="Properties\DataSources\LibationWinForm_Framework.ProductGrids.GridEntry.datasource" />
<None Include="Properties\DataSources\WinFormsDesigner.GridEntry.datasource" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon />
<StartupObject />

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>ffmpeg_decrypt</RootNamespace>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

View File

@@ -5,7 +5,9 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using AaxDecrypter;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using Dinah.Core.Windows.Forms;
using Serilog;
namespace inAudibleLite
{
@@ -23,18 +25,44 @@ namespace inAudibleLite
InitializeComponent();
this.btnConvert.Enabled = false;
initLogging();
initSerilog();
redirectWriteLine();
}
private void initLogging()
private static void initSerilog()
{
// default. for reference. output example:
// 2019-11-26 08:48:40.224 -05:00 [DBG] Begin Libation
var default_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}";
// with class and method info. output example:
// 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForm.Program.init()) Begin Libation
var code_outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
var logPath = Path.Combine(Path.GetTempPath(), "Log.log");
Log.Logger = new LoggerConfiguration()
.Enrich.WithCaller()
.MinimumLevel.Debug()
.WriteTo.File(logPath,
rollingInterval: RollingInterval.Month,
outputTemplate: code_outputTemplate)
.CreateLogger();
Log.Logger.Debug("Begin Libation");
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
}
private void redirectWriteLine()
{
// redirect Console.WriteLine to console, log file, textbox
var origOut = Console.Out;
var controlWriter = new RichTextBoxTextWriter(this.rtbLog);
var tempPath = Path.GetTempPath();
var logger1 = new FileLogger(Path.Combine(tempPath, APP_NAME));
var logger2 = new FileLoggerTextWriter(logger1);
var multiLogger = new MultiTextWriter(origOut, controlWriter, logger2);
var multiLogger = new MultiTextWriter(
Console.Out,
new RichTextBoxTextWriter(this.rtbLog),
new SerilogTextWriter());
Console.SetOut(multiLogger);
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>