Compare commits
52 Commits
v3.0.1
...
v3.1-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1375da2065 | ||
|
|
d5d72a13f6 | ||
|
|
a1ba324166 | ||
|
|
b0139c47be | ||
|
|
80b0ef600d | ||
|
|
f3128b562d | ||
|
|
6734dec55c | ||
|
|
b9314ac678 | ||
|
|
e319326c30 | ||
|
|
5474446f62 | ||
|
|
d53a617bc8 | ||
|
|
9076fae6f6 | ||
|
|
5d4a97cdc4 | ||
|
|
bbe745f487 | ||
|
|
47360c036d | ||
|
|
e69df2abbc | ||
|
|
88d49acdad | ||
|
|
01a914c390 | ||
|
|
0b42b8ee49 | ||
|
|
c598576683 | ||
|
|
b126eed028 | ||
|
|
3020a116cf | ||
|
|
88b9ea2f2d | ||
|
|
159c04c4b1 | ||
|
|
fad0f021ed | ||
|
|
52f21dcab1 | ||
|
|
a6b89ca4c5 | ||
|
|
650c00cf66 | ||
|
|
089edf934e | ||
|
|
efe2b19e24 | ||
|
|
c41dc9a6db | ||
|
|
707cb78dbc | ||
|
|
fc0d97d8e7 | ||
|
|
1494a15a6e | ||
|
|
ac0de2a05e | ||
|
|
3cc80b6a24 | ||
|
|
38b04be6ba | ||
|
|
0c52d443b2 | ||
|
|
aa0ebac50e | ||
|
|
debebf6ee0 | ||
|
|
9034288e7c | ||
|
|
19ee02ced4 | ||
|
|
33723d7412 | ||
|
|
a01a67e34a | ||
|
|
ecdb510513 | ||
|
|
0b08bb3c4a | ||
|
|
22e5dbf83d | ||
|
|
3b33648267 | ||
|
|
8709518cd7 | ||
|
|
3da1dff4d8 | ||
|
|
6aa544b322 | ||
|
|
bd993b4e4d |
4
.gitignore
vendored
@@ -328,3 +328,7 @@ ASALocalRun/
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
|
||||
# manually ignored files
|
||||
/__TODO.txt
|
||||
|
||||
38
ApplicationServices/UNTESTED/LibraryCommands.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static async Task<(int totalCount, int newCount)> IndexLibraryAsync(ILoginCallback callback)
|
||||
{
|
||||
var audibleApiActions = new AudibleApiActions();
|
||||
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
|
||||
var totalCount = items.Count;
|
||||
|
||||
var libImporter = new LibraryImporter();
|
||||
var newCount = await Task.Run(() => libImporter.Import(items));
|
||||
|
||||
await Task.Run(() => SearchEngineCommands.FullReIndex());
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
|
||||
public static int UpdateTags(this LibationContext context, Book book, string newTags)
|
||||
{
|
||||
book.UserDefinedItem.Tags = newTags;
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineCommands.UpdateBookTags(book);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public class LibraryIndexer
|
||||
{
|
||||
public async Task<(int totalCount, int newCount)> IndexAsync(ILoginCallback callback)
|
||||
{
|
||||
var audibleApiActions = new AudibleApiActions();
|
||||
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
|
||||
var totalCount = items.Count;
|
||||
|
||||
var libImporter = new LibraryImporter();
|
||||
var newCount = await Task.Run(() => libImporter.Import(items));
|
||||
|
||||
await SearchEngineActions.FullReIndexAsync();
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class SearchEngineActions
|
||||
{
|
||||
public static async Task FullReIndexAsync()
|
||||
{
|
||||
var engine = new LibationSearchEngine.SearchEngine();
|
||||
await engine.CreateNewIndexAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static void UpdateBookTags(Book book)
|
||||
{
|
||||
var engine = new LibationSearchEngine.SearchEngine();
|
||||
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
ApplicationServices/UNTESTED/SearchEngineCommands.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class SearchEngineCommands
|
||||
{
|
||||
public static void FullReIndex()
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
engine.CreateNewIndex();
|
||||
}
|
||||
|
||||
public static SearchResultSet Search(string searchString)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
return engine.Search(searchString);
|
||||
}
|
||||
catch (System.IO.FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
return engine.Search(searchString);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateBookTags(Book book)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
}
|
||||
catch (System.IO.FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using DataLayer;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class TagUpdater
|
||||
{
|
||||
public static int IndexChangedTags(Book book)
|
||||
{
|
||||
// update disconnected entity
|
||||
using var context = LibationContext.Create();
|
||||
context.Update(book);
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
// this part is tags-specific
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineActions.UpdateBookTags(book);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
BIN
DataLayer/LibationContext.db
Normal file
@@ -1,341 +0,0 @@
|
||||
// <auto-generated />
|
||||
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("20191007202808_UpgradeToCore3")]
|
||||
partial class UpgradeToCore3
|
||||
{
|
||||
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);
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("HasBookDetails")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("tinyint");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AudibleAuthorId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DownloadBookLink")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class NoScraping : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
table: "Supplement");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DownloadBookLink",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HasBookDetails",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
table: "UserDefinedItem",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
table: "Supplement");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DownloadBookLink",
|
||||
table: "Library",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "HasBookDetails",
|
||||
table: "Books",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Supplement_Books_BookId",
|
||||
table: "Supplement",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_UserDefinedItem_Books_BookId",
|
||||
table: "UserDefinedItem",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("20191105183104_NoScraping")]
|
||||
partial class NoScraping
|
||||
[Migration("20191119144803_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,14 +116,13 @@ 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");
|
||||
|
||||
@@ -140,10 +134,10 @@ namespace DataLayer.Migrations
|
||||
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 +148,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 +166,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 +194,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 +217,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 +238,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 +253,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");
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class UpgradeToCore3 : Migration
|
||||
public partial class Fresh : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
@@ -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,13 +61,12 @@ 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),
|
||||
LengthInMinutes = table.Column<int>(nullable: false),
|
||||
PictureId = table.Column<string>(nullable: true),
|
||||
HasBookDetails = table.Column<bool>(nullable: false),
|
||||
IsAbridged = table.Column<bool>(nullable: false),
|
||||
DatePublished = table.Column<DateTime>(nullable: true),
|
||||
CategoryId = table.Column<int>(nullable: false),
|
||||
@@ -117,8 +116,7 @@ namespace DataLayer.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
BookId = table.Column<int>(nullable: false),
|
||||
DateAdded = table.Column<DateTime>(nullable: false),
|
||||
DownloadBookLink = table.Column<string>(nullable: true)
|
||||
DateAdded = table.Column<DateTime>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -161,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)
|
||||
},
|
||||
@@ -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,14 +114,13 @@ 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");
|
||||
|
||||
@@ -138,10 +132,10 @@ namespace DataLayer.Migrations
|
||||
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 +146,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 +164,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 +192,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 +215,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 +236,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 +251,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");
|
||||
|
||||
|
||||
@@ -61,12 +61,17 @@ namespace DataLayer
|
||||
string title,
|
||||
string description,
|
||||
int lengthInMinutes,
|
||||
IEnumerable<Contributor> authors)
|
||||
IEnumerable<Contributor> authors,
|
||||
IEnumerable<Contributor> narrators)
|
||||
{
|
||||
// validate
|
||||
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||
var productId = audibleProductId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
|
||||
|
||||
// assign as soon as possible. stuff below relies on this
|
||||
AudibleProductId = productId;
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
|
||||
|
||||
// non-ef-ctor init.s
|
||||
@@ -79,19 +84,13 @@ namespace DataLayer
|
||||
CategoryId = Category.GetEmpty().CategoryId;
|
||||
|
||||
// simple assigns
|
||||
AudibleProductId = productId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
|
||||
// assigns with biz logic
|
||||
ReplaceAuthors(authors);
|
||||
//ReplaceNarrators(narrators);
|
||||
|
||||
// import previously saved tags
|
||||
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
|
||||
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
|
||||
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
|
||||
ReplaceNarrators(narrators);
|
||||
}
|
||||
|
||||
#region contributors, authors, narrators
|
||||
@@ -207,7 +206,7 @@ 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)
|
||||
{
|
||||
@@ -247,5 +246,7 @@ namespace DataLayer
|
||||
context.Entry(this).Reference(s => s.Category).Load();
|
||||
Category = category;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {Title}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,7 @@ namespace DataLayer
|
||||
Role = role;
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,5 +48,7 @@ namespace DataLayer
|
||||
if (parentCategory != null)
|
||||
ParentCategory = parentCategory;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleCategoryId}] {Name}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace DataLayer
|
||||
private HashSet<BookContributor> _booksLink;
|
||||
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
|
||||
|
||||
public string AudibleContributorId { get; private set; }
|
||||
|
||||
private Contributor() { }
|
||||
public Contributor(string name)
|
||||
{
|
||||
@@ -34,49 +36,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,5 +17,7 @@ namespace DataLayer
|
||||
Book = book;
|
||||
DateAdded = dateAdded;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,5 +72,7 @@ namespace DataLayer
|
||||
|
||||
return string.Join("\r\n", items);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,7 @@ namespace DataLayer
|
||||
if (index.HasValue)
|
||||
Index = index.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"Series={Series} Book={Book}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,7 @@ namespace DataLayer
|
||||
Book = book;
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,29 +13,36 @@ namespace DataLayer
|
||||
|
||||
private UserDefinedItem() { }
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
}
|
||||
|
||||
// import previously saved tags
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
|
||||
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
private string _tags = "";
|
||||
public string Tags
|
||||
{
|
||||
get => _tags;
|
||||
set => _tags = sanitize(value);
|
||||
}
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
// only legal chars are letters numbers underscores and separating whitespace
|
||||
//
|
||||
// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
// it's easy to expand whitelist as needed
|
||||
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
//
|
||||
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
// full list of characters which must be escaped:
|
||||
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
// only legal chars are letters numbers underscores and separating whitespace
|
||||
//
|
||||
// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
// it's easy to expand whitelist as needed
|
||||
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
//
|
||||
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
// full list of characters which must be escaped:
|
||||
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
static Regex regex { get; } = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
|
||||
private static string sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
@@ -63,8 +70,6 @@ namespace DataLayer
|
||||
|
||||
return string.Join(" ", unique);
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
#endregion
|
||||
|
||||
// owned: not an optional one-to-one
|
||||
@@ -73,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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace DataLayer
|
||||
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
||||
{
|
||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlServer(connectionString);
|
||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder
|
||||
//.UseSqlServer
|
||||
.UseSqlite
|
||||
(connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DataLayer
|
||||
{
|
||||
public static class LibraryQueries
|
||||
{
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||
{
|
||||
public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
||||
=> context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||
{
|
||||
using var context = LibationContext.Create();
|
||||
return context
|
||||
.Library
|
||||
//.AsNoTracking()
|
||||
.AsNoTracking()
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
}
|
||||
@@ -21,7 +27,7 @@ namespace DataLayer
|
||||
using var context = LibationContext.Create();
|
||||
return context
|
||||
.Library
|
||||
//.AsNoTracking()
|
||||
.AsNoTracking()
|
||||
.GetLibraryBook(productId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,57 +4,35 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
internal class TagPersistenceInterceptor : IDbInterceptor
|
||||
{
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
doWork__EFCore(context);
|
||||
}
|
||||
|
||||
public void Executed(DbContext context) { }
|
||||
|
||||
static void doWork__EFCore(DbContext context)
|
||||
{
|
||||
// persist tags:
|
||||
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
|
||||
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
|
||||
foreach (var t in tagSets)
|
||||
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||
}
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
// persist tags:
|
||||
var modifiedEntities = context
|
||||
.ChangeTracker
|
||||
.Entries()
|
||||
.Where(p => p.State.In(EntityState.Modified, EntityState.Added))
|
||||
.ToList();
|
||||
|
||||
#region // notes: working with proxies, esp EF 6
|
||||
// EF 6: entities are proxied with lazy loading when collections are virtual
|
||||
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
|
||||
persistTags(modifiedEntities);
|
||||
}
|
||||
|
||||
//static void doWork_EF6(DbContext context)
|
||||
//{
|
||||
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
|
||||
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
|
||||
|
||||
// // persist tags
|
||||
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
|
||||
// foreach (var t in tagSets)
|
||||
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
|
||||
//}
|
||||
|
||||
//// https://stackoverflow.com/a/25774651
|
||||
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
|
||||
//{
|
||||
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
|
||||
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
|
||||
// try
|
||||
// {
|
||||
// context.Configuration.ProxyCreationEnabled = false;
|
||||
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
|
||||
// }
|
||||
//}
|
||||
#endregion
|
||||
}
|
||||
private static void persistTags(List<EntityEntry> modifiedEntities)
|
||||
{
|
||||
var tagSets = modifiedEntities
|
||||
.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)
|
||||
.ToList();
|
||||
foreach (var t in tagSets)
|
||||
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
DataLayer/UNTESTED/Utilities/LocalDatabaseInfo.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer.Utilities
|
||||
{
|
||||
public static class LocalDatabaseInfo
|
||||
{
|
||||
public static List<string> GetLocalDBInstances()
|
||||
{
|
||||
// Start the child process.
|
||||
using var p = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/C sqllocaldb info",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
|
||||
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
|
||||
? new List<string>()
|
||||
: output
|
||||
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"LibationContext": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
|
||||
"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": "",
|
||||
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
|
||||
|
||||
@@ -57,38 +57,40 @@ namespace DtoImporterService
|
||||
.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))
|
||||
.Entity;
|
||||
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++;
|
||||
}
|
||||
|
||||
// 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();
|
||||
// not all books have narrators. these will already be using author as narrator. don't undo this
|
||||
if (narrators.Any())
|
||||
book.ReplaceNarrators(narrators);
|
||||
|
||||
// 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);
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -67,11 +67,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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
@@ -7,7 +8,7 @@ using FileManager;
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
/// Download DRM book and decrypt audiobook files
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
@@ -20,35 +21,48 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> Completed;
|
||||
|
||||
public DownloadBook Download { get; } = new DownloadBook();
|
||||
public DecryptBook Decrypt { get; } = new DecryptBook();
|
||||
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 ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
// 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}";
|
||||
var productId = libraryBook.Book.AudibleProductId;
|
||||
var displayMessage = $"[{productId}] {libraryBook.Book.Title}";
|
||||
|
||||
Begin?.Invoke(this, displayMessage);
|
||||
|
||||
try
|
||||
{
|
||||
var aaxExists = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
if (!aaxExists)
|
||||
await Download.ProcessAsync(libraryBook);
|
||||
{
|
||||
{
|
||||
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
return await Decrypt.ProcessAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using FileManager;
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
/// Decrypt audiobook files
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
@@ -34,16 +34,13 @@ namespace FileLiberator
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<string> 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 ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
// 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}";
|
||||
|
||||
@@ -51,20 +48,17 @@ namespace FileLiberator
|
||||
|
||||
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" };
|
||||
|
||||
string proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
|
||||
|
||||
string outputAudioFilename;
|
||||
//outputAudioFilename = await inAudibleDecrypt(proposedOutputFile, aaxFilename);
|
||||
outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
|
||||
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
|
||||
var outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename == null)
|
||||
@@ -75,7 +69,7 @@ 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;
|
||||
@@ -164,148 +158,5 @@ namespace FileLiberator
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
}
|
||||
|
||||
#region legacy inAudible wire-up code
|
||||
//
|
||||
// instructions are in comments below for editing and interacting with inAudible. eg:
|
||||
// \_NET\Visual Studio 2017\inAudible197\decompiled - in progress\inAudible.csproj
|
||||
// first, add its project and put its exe path into inAudiblePath
|
||||
//
|
||||
#region placeholder code
|
||||
// this exists so the below legacy code will compile as-is. comment out placeholder code when actually connecting to inAudible
|
||||
|
||||
class Form
|
||||
{
|
||||
internal void Show() => throw new NotImplementedException();
|
||||
internal void Kill() => throw new NotImplementedException();
|
||||
}
|
||||
class TextBox
|
||||
{
|
||||
internal string Text { set => throw new NotImplementedException(); }
|
||||
}
|
||||
class Button
|
||||
{
|
||||
internal void PerformClick() => throw new NotImplementedException();
|
||||
}
|
||||
class AudibleConvertor
|
||||
{
|
||||
internal class GLOBALS
|
||||
{
|
||||
internal static string ExecutablePath { set => throw new NotImplementedException(); }
|
||||
}
|
||||
internal class Form1 : Form
|
||||
{
|
||||
internal Form1(Action<string> action) => throw new NotImplementedException();
|
||||
internal void LoadAudibleFiles(string[] arr) => throw new NotImplementedException();
|
||||
internal TextBox txtOutputFile { get => throw new NotImplementedException(); }
|
||||
internal Button btnConvert { get => throw new NotImplementedException(); }
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static string inAudiblePath { get; }
|
||||
= @"C:\"
|
||||
+ @"DEV_ROOT_EXAMPLE\"
|
||||
+ @"_NET\Visual Studio 2017\"
|
||||
+ @"inAudible197\decompiled - in progress\bin\Debug\inAudible.exe";
|
||||
private static async Task<string> inAudibleDecrypt(string proposedOutputFile, string aaxFilename)
|
||||
{
|
||||
#region // inAudible code to change:
|
||||
/*
|
||||
* Prevent "Path too long" error
|
||||
* =============================
|
||||
* BatchFiles.cs :: GenerateOutputFilepath()
|
||||
* Add this just before the bottom return statement
|
||||
*
|
||||
if (oneOff && !string.IsNullOrWhiteSpace(outputPath))
|
||||
return str + "\\" + Path.GetFileNameWithoutExtension(outputPath) + "." + fileType;
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#region init inAudible
|
||||
#region // suppress warnings
|
||||
// inAudible. project properties > Build > Warning level=2
|
||||
#endregion
|
||||
#region // instructions to create inAudible ExecutablePath
|
||||
/*
|
||||
* STEP 1
|
||||
* ======
|
||||
* do a PROJECT level find/replace within inAudible
|
||||
* find
|
||||
* Application.ExecutablePath
|
||||
* replace
|
||||
* AudibleConvertor.GLOBALS.ExecutablePath
|
||||
* STEP 2
|
||||
* ======
|
||||
* new inAudible root-level file
|
||||
* _GLOBALS.cs
|
||||
* contents:
|
||||
* namespace AudibleConvertor { public static class GLOBALS { public static string ExecutablePath { get; set; } = System.Windows.Forms.Application.ExecutablePath; } }
|
||||
*/
|
||||
#endregion
|
||||
AudibleConvertor.GLOBALS.ExecutablePath = inAudiblePath;
|
||||
// before using inAudible, set ini values
|
||||
setIniValues(new Dictionary<string, string> { ["selected_codec"] = "lossless", ["embed_cover"] = "True", ["copy_cover_art"] = "False", ["create_cue"] = "True", ["nfo"] = "True", ["strip_unabridged"] = "True", });
|
||||
#endregion
|
||||
|
||||
// this provides the async magic to keep all of the form calling code in one method instead of event callback pattern
|
||||
// TODO: error handling is not obvious:
|
||||
// https://deaddesk.top/don't-fall-for-TaskCompletionSource-traps/
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
|
||||
// to know when inAudible is complete. code to change:
|
||||
#region // code to preceed ctor
|
||||
/*
|
||||
Action<string> _conversionCompleteAction;
|
||||
public Form1(Action<string> conversionCompleteAction) : this() => _conversionCompleteAction = conversionCompleteAction;
|
||||
*/
|
||||
#endregion
|
||||
#region // code for the end of bgwAAX_Completed()
|
||||
/*
|
||||
if (this.myAdvancedOptions.beep && !this.myAdvancedOptions.cylon) this.SOXPlay(Form1.appPath + "\\beep.mp3", true);
|
||||
else if (myAdvancedOptions.cylon) SOXPlay(appPath + "\\inAudible-end.mp3", true);
|
||||
_conversionCompleteAction?.Invoke(outputFileName);
|
||||
}
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#region start inAudible
|
||||
var form = new AudibleConvertor.Form1(tcs.SetResult);
|
||||
form.Show();
|
||||
form.LoadAudibleFiles(new string[] { aaxFilename }); // inAudible: make public
|
||||
|
||||
// change output info to include asin. put in temp
|
||||
form.txtOutputFile.Text = proposedOutputFile; // inAudible: make public
|
||||
|
||||
// submit/process/decrypt
|
||||
form.btnConvert.PerformClick(); // inAudible: make public
|
||||
|
||||
// ta-da -- magic! we stop here until inAudible complete
|
||||
var outputAudioFilename = await tcs.Task;
|
||||
#endregion
|
||||
|
||||
#region when complete, close inAudible
|
||||
// use this instead of Dinah.Core.Windows.Forms.UIThread()
|
||||
form.Kill();
|
||||
#endregion
|
||||
|
||||
return outputAudioFilename;
|
||||
}
|
||||
|
||||
private static void setIniValues(Dictionary<string, string> settings)
|
||||
{
|
||||
// C:\Users\username\Documents\inAudible\config.ini
|
||||
var iniPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "inAudible", "config.ini");
|
||||
var iniContents = File.ReadAllText(iniPath);
|
||||
|
||||
foreach (var kvp in settings)
|
||||
iniContents = System.Text.RegularExpressions.Regex.Replace(
|
||||
iniContents,
|
||||
$@"\r\n{kvp.Key} = [^\r\n]+\r\n",
|
||||
$"\r\n{kvp.Key} = {kvp.Value}\r\n");
|
||||
|
||||
File.WriteAllText(iniPath, iniContents);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FileManager;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book and decrypt audiobook files.
|
||||
/// Download DRM book
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
@@ -17,59 +17,50 @@ 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)
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var tempAaxFilename = FileUtility.GetValidFilename(
|
||||
var tempAaxFilename = getDownloadPath(libraryBook);
|
||||
var actualFilePath = await downloadBookAsync(libraryBook, tempAaxFilename);
|
||||
moveBook(libraryBook, actualFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
|
||||
private static string getDownloadPath(LibraryBook libraryBook)
|
||||
=> FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsInProgress,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
|
||||
// if getting from full title:
|
||||
// '?' is allowed
|
||||
// colons are inconsistent but not problematic to just leave them
|
||||
// - 1 colon: sometimes full title is used. sometimes only the part before the colon is used
|
||||
// - multple colons: only the part before the final colon is used
|
||||
// e.g. Alien: Out of the Shadows: An Audible Original Drama => Alien: Out of the Shadows
|
||||
// in cases where title includes '&', just use everything before the '&' and ignore the rest
|
||||
//// var adhTitle = product.Title.Split('&')[0]
|
||||
private async Task<string> downloadBookAsync(LibraryBook libraryBook, string tempAaxFilename)
|
||||
{
|
||||
var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile);
|
||||
|
||||
// new/api method
|
||||
tempAaxFilename = await performApiDownloadAsync(libraryBook, tempAaxFilename);
|
||||
var actualFilePath = await PerformDownloadAsync(
|
||||
tempAaxFilename,
|
||||
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
|
||||
|
||||
// move
|
||||
var aaxFilename = FileUtility.GetValidFilename(
|
||||
return actualFilePath;
|
||||
}
|
||||
|
||||
private void moveBook(LibraryBook libraryBook, string actualFilePath)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsFinal,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
File.Move(tempAaxFilename, aaxFilename);
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
var isDownloaded = await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
if (isDownloaded)
|
||||
Invoke_StatusUpdate($"Downloaded: {aaxFilename}");
|
||||
else
|
||||
statusHandler.AddError("Downloaded AAX file cannot be found");
|
||||
return statusHandler;
|
||||
File.Move(actualFilePath, newAaxFilename);
|
||||
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
|
||||
}
|
||||
|
||||
private async Task<string> performApiDownloadAsync(LibraryBook libraryBook, string tempAaxFilename)
|
||||
{
|
||||
var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile);
|
||||
|
||||
var progress = new Progress<Dinah.Core.Net.Http.DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => Invoke_DownloadProgressChanged(this, e);
|
||||
|
||||
Invoke_DownloadBegin(tempAaxFilename);
|
||||
var actualFilePath = await api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, progress);
|
||||
Invoke_DownloadCompleted(this, $"Completed: {actualFilePath}");
|
||||
|
||||
return actualFilePath;
|
||||
}
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded AAX file cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : DownloadableBase
|
||||
public class DownloadPdf : DownloadableBase
|
||||
{
|
||||
static DownloadPdf()
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !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)
|
||||
{
|
||||
// https://stackoverflow.com/a/15483698
|
||||
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
|
||||
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var product = libraryBook.Book;
|
||||
|
||||
if (!product.Supplements.Any())
|
||||
return false;
|
||||
|
||||
return !await AudibleFileStorage.PDF.ExistsAsync(product.AudibleProductId);
|
||||
}
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var product = libraryBook.Book;
|
||||
|
||||
if (product == null)
|
||||
return new StatusHandler { "Book not found" };
|
||||
|
||||
var urls = product.Supplements.Select(d => d.Url).ToList();
|
||||
if (urls.Count == 0)
|
||||
return new StatusHandler { "PDF download url not found" };
|
||||
|
||||
// sanity check
|
||||
if (urls.Count > 1)
|
||||
throw new Exception("Multiple PDF downloads are not currently supported. typically indicates an error");
|
||||
|
||||
var url = urls.Single();
|
||||
|
||||
var destinationDir = await getDestinationDirectory(product.AudibleProductId);
|
||||
if (destinationDir == null)
|
||||
return new StatusHandler { "Destination directory not found for PDF download" };
|
||||
|
||||
var destinationFilename = Path.Combine(destinationDir, Path.GetFileName(url));
|
||||
|
||||
using var webClient = GetWebClient(destinationFilename);
|
||||
await webClient.DownloadFileTaskAsync(url, destinationFilename);
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
var exists = await AudibleFileStorage.PDF.ExistsAsync(product.AudibleProductId);
|
||||
if (!exists)
|
||||
statusHandler.AddError("Downloaded PDF cannot be found");
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
private async Task<string> getDestinationDirectory(string productId)
|
||||
{
|
||||
// if audio file exists, get it's dir
|
||||
var audioFile = await AudibleFileStorage.Audio.GetAsync(productId);
|
||||
if (audioFile != null)
|
||||
return Path.GetDirectoryName(audioFile);
|
||||
|
||||
// else return base Book dir
|
||||
return AudibleFileStorage.PDF.StorageDirectory;
|
||||
}
|
||||
|
||||
// other user agents from my chrome. from: https://www.whoishostingthis.com/tools/user-agent/
|
||||
private static string[] userAgents { get; } = new[]
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
|
||||
};
|
||||
private WebClient GetWebClient(string downloadMessage)
|
||||
{
|
||||
var webClient = new WebClient();
|
||||
// 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(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId))
|
||||
?? AudibleFileStorage.PDF.StorageDirectory;
|
||||
|
||||
var userAgentIndex = new Random().Next(0, userAgents.Length); // upper bound is exclusive
|
||||
webClient.Headers["User-Agent"] = userAgents[userAgentIndex];
|
||||
webClient.Headers["Referer"] = "https://google.com";
|
||||
webClient.Headers["Upgrade-Insecure-Requests"] = "1";
|
||||
webClient.Headers["DNT"] = "1";
|
||||
webClient.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";
|
||||
webClient.Headers["Accept-Language"] = "en-US,en;q=0.9";
|
||||
|
||||
webClient.DownloadProgressChanged += (s, e) => Invoke_DownloadProgressChanged(s, new Dinah.Core.Net.Http.DownloadProgress { BytesReceived = e.BytesReceived, ProgressPercentage = e.ProgressPercentage, TotalBytesToReceive = e.TotalBytesToReceive });
|
||||
webClient.DownloadFileCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}");
|
||||
webClient.DownloadDataCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}");
|
||||
webClient.DownloadStringCompleted += (s, e) => Invoke_DownloadCompleted(s, $"Completed: {downloadMessage}");
|
||||
|
||||
Invoke_DownloadBegin(downloadMessage);
|
||||
|
||||
return webClient;
|
||||
return Path.Combine(destinationDir, Path.GetFileName(getdownloadUrl(libraryBook)));
|
||||
}
|
||||
|
||||
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
||||
proposedDownloadFilePath,
|
||||
(p) => client.DownloadFileAsync(getdownloadUrl(libraryBook), proposedDownloadFilePath, p));
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.PDF.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -10,23 +11,20 @@ namespace FileLiberator
|
||||
public event EventHandler<string> Begin;
|
||||
public event EventHandler<string> Completed;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> DownloadBegin;
|
||||
public event EventHandler<Dinah.Core.Net.Http.DownloadProgress> DownloadProgressChanged;
|
||||
public event EventHandler<DownloadProgress> DownloadProgressChanged;
|
||||
public event EventHandler<string> DownloadCompleted;
|
||||
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
|
||||
protected void Invoke_DownloadBegin(string downloadMessage) => DownloadBegin?.Invoke(this, downloadMessage);
|
||||
protected void Invoke_DownloadProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress progress) => DownloadProgressChanged?.Invoke(sender, progress);
|
||||
protected void Invoke_DownloadCompleted(object sender, string str) => DownloadCompleted?.Invoke(sender, str);
|
||||
|
||||
|
||||
public abstract Task<bool> ValidateAsync(LibraryBook libraryBook);
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessUnregistered()
|
||||
// often does a lot with forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
// 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}";
|
||||
|
||||
@@ -41,5 +39,25 @@ namespace FileLiberator
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
progress.ProgressChanged += (_, e) => DownloadProgressChanged?.Invoke(this, e);
|
||||
|
||||
DownloadBegin?.Invoke(this, proposedDownloadFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await func(progress);
|
||||
StatusUpdate?.Invoke(this, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DownloadCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace FileLiberator
|
||||
|
||||
event EventHandler<string> 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);
|
||||
|
||||
@@ -9,8 +9,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,26 +17,32 @@ 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.GetNextValid();
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
|
||||
var status = await processable.ProcessAsync(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 commans
|
||||
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)
|
||||
public static LibraryBook GetNextValid(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)
|
||||
=> processable.Validate(libraryBook)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly" Version="7.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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,21 +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,
|
||||
|
||||
[".aax"] = FileType.AAX,
|
||||
|
||||
[".pdf"] = FileType.PDF,
|
||||
[".zip"] = FileType.PDF,
|
||||
};
|
||||
|
||||
public static AudibleFileStorage Audio { get; }
|
||||
public static AudibleFileStorage AAX { get; }
|
||||
public static AudibleFileStorage PDF { get; }
|
||||
@@ -79,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
|
||||
|
||||
@@ -90,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:
|
||||
@@ -100,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FileManager
|
||||
[Description("Location of the configuration file where these settings are saved. Please do not edit this file directly while Libation is running.")]
|
||||
public string Filepath { get; }
|
||||
|
||||
[Description("Your user-specific key used to decrypt your audible files (*.aax) into audio files you can use anywhere (*.m4b)")]
|
||||
[Description("[Advanced. Leave alone in most cases.] Your user-specific key used to decrypt your audible files (*.aax) into audio files you can use anywhere (*.m4b)")]
|
||||
public string DecryptKey
|
||||
{
|
||||
get => persistentDictionary[nameof(DecryptKey)];
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FileManager
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
static List<CacheEntry> inMemoryCache = new List<CacheEntry>();
|
||||
static List<CacheEntry> inMemoryCache { get; } = new List<CacheEntry>();
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
|
||||
|
||||
@@ -70,10 +70,5 @@ namespace FileManager
|
||||
property = property.Replace(ch.ToString(), "");
|
||||
return property;
|
||||
}
|
||||
|
||||
public static string TitleCompressed(string title)
|
||||
=> new string(title
|
||||
.Where(c => (char.IsLetterOrDigit(c)))
|
||||
.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,77 +2,112 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Files are small. Entire file is read from disk every time. No volitile storage. Paths are well known
|
||||
/// </summary>
|
||||
public enum PictureSize { _80x80, _300x300, _500x500 }
|
||||
public struct PictureDefinition
|
||||
{
|
||||
public string PictureId { get; }
|
||||
public PictureSize Size { get; }
|
||||
|
||||
public PictureDefinition(string pictureId, PictureSize pictureSize)
|
||||
{
|
||||
PictureId = pictureId;
|
||||
Size = pictureSize;
|
||||
}
|
||||
}
|
||||
public static class PictureStorage
|
||||
{
|
||||
public enum PictureSize { _80x80, _300x300, _500x500 }
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string ImagesDirectory { get; }
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Images").FullName;
|
||||
|
||||
private static string getPath(string pictureId, PictureSize size)
|
||||
=> Path.Combine(ImagesDirectory, $"{pictureId}{size}.jpg");
|
||||
private static string getPath(PictureDefinition def)
|
||||
=> Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg");
|
||||
|
||||
public static byte[] GetImage(string pictureId, PictureSize size)
|
||||
{
|
||||
var path = getPath(pictureId, size);
|
||||
if (!FileUtility.FileExists(path))
|
||||
DownloadImages(pictureId);
|
||||
private static System.Timers.Timer timer { get; }
|
||||
static PictureStorage()
|
||||
{
|
||||
timer = new System.Timers.Timer(700)
|
||||
{
|
||||
AutoReset = true,
|
||||
Enabled = true
|
||||
};
|
||||
timer.Elapsed += (_, __) => timerDownload();
|
||||
}
|
||||
|
||||
return File.ReadAllBytes(path);
|
||||
}
|
||||
public static event EventHandler<string> PictureCached;
|
||||
|
||||
public static void DownloadImages(string pictureId)
|
||||
{
|
||||
var path80 = getPath(pictureId, PictureSize._80x80);
|
||||
var path300 = getPath(pictureId, PictureSize._300x300);
|
||||
var path500 = getPath(pictureId, PictureSize._500x500);
|
||||
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
|
||||
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
|
||||
{
|
||||
if (!cache.ContainsKey(def))
|
||||
{
|
||||
var path = getPath(def);
|
||||
cache[def]
|
||||
= FileUtility.FileExists(path)
|
||||
? File.ReadAllBytes(path)
|
||||
: null;
|
||||
}
|
||||
return (cache[def] == null, cache[def] ?? getDefaultImage(def.Size));
|
||||
}
|
||||
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
using var webClient = new System.Net.WebClient();
|
||||
// download any that don't exist
|
||||
{
|
||||
if (!FileUtility.FileExists(path80))
|
||||
{
|
||||
var bytes = webClient.DownloadData(
|
||||
"https://images-na.ssl-images-amazon.com/images/I/" + pictureId + "._SL80_.jpg");
|
||||
File.WriteAllBytes(path80, bytes);
|
||||
}
|
||||
}
|
||||
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
|
||||
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
|
||||
=> defaultImages[pictureSize] = bytes;
|
||||
private static byte[] getDefaultImage(PictureSize size)
|
||||
=> defaultImages.ContainsKey(size)
|
||||
? defaultImages[size]
|
||||
: new byte[0];
|
||||
|
||||
{
|
||||
if (!FileUtility.FileExists(path300))
|
||||
{
|
||||
var bytes = webClient.DownloadData(
|
||||
"https://images-na.ssl-images-amazon.com/images/I/" + pictureId + "._SL300_.jpg");
|
||||
File.WriteAllBytes(path300, bytes);
|
||||
}
|
||||
}
|
||||
// necessary to avoid IO errors. ReadAllBytes and WriteAllBytes can conflict in some cases, esp when debugging
|
||||
private static bool isProcessing;
|
||||
private static void timerDownload()
|
||||
{
|
||||
// must live outside try-catch, else 'finally' can reset another thread's lock
|
||||
if (isProcessing)
|
||||
return;
|
||||
|
||||
{
|
||||
if (!FileUtility.FileExists(path500))
|
||||
{
|
||||
var bytes = webClient.DownloadData(
|
||||
"https://m.media-amazon.com/images/I/" + pictureId + "._SL500_.jpg");
|
||||
File.WriteAllBytes(path500, bytes);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
isProcessing = true;
|
||||
|
||||
break;
|
||||
}
|
||||
catch { retry++; }
|
||||
}
|
||||
while (retry < 3);
|
||||
}
|
||||
}
|
||||
var def = cache
|
||||
.Where(kvp => kvp.Value is null)
|
||||
.Select(kvp => kvp.Key)
|
||||
// 80x80 should be 1st since it's enum value == 0
|
||||
.OrderBy(d => d.PictureId)
|
||||
.FirstOrDefault();
|
||||
|
||||
// no more null entries. all requsted images are cached
|
||||
if (string.IsNullOrWhiteSpace(def.PictureId))
|
||||
return;
|
||||
|
||||
var bytes = downloadBytes(def);
|
||||
saveFile(def, bytes);
|
||||
cache[def] = bytes;
|
||||
|
||||
PictureCached?.Invoke(nameof(PictureStorage), def.PictureId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
||||
private static byte[] downloadBytes(PictureDefinition def)
|
||||
{
|
||||
var sz = def.Size.ToString().Split('x')[1];
|
||||
return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
|
||||
}
|
||||
|
||||
private static void saveFile(PictureDefinition def, byte[] bytes)
|
||||
{
|
||||
var path = getPath(def);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,64 +3,57 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
|
||||
/// json is only read when a product is first loaded
|
||||
/// json is only read when a product is first loaded into the db
|
||||
/// json is only written to when tags are edited
|
||||
/// json access is infrequent and one-off
|
||||
/// all other reads happen against db. No volitile storage
|
||||
/// </summary>
|
||||
public static class TagsPersistence
|
||||
{
|
||||
public static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
|
||||
private static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
|
||||
|
||||
private static object locker { get; } = new object();
|
||||
|
||||
// if failed, retry only 1 time after a wait of 100 ms
|
||||
// 1st save attempt sometimes fails with
|
||||
// The requested operation cannot be performed on a file with a user-mapped section open.
|
||||
private static RetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
|
||||
|
||||
public static void Save(string productId, string tags)
|
||||
=> System.Threading.Tasks.Task.Run(() => save_fireAndForget(productId, tags));
|
||||
|
||||
private static void save_fireAndForget(string productId, string tags)
|
||||
{
|
||||
ensureCache();
|
||||
|
||||
cache[productId] = tags;
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
// get all
|
||||
var allDictionary = retrieve();
|
||||
|
||||
// update/upsert tag list
|
||||
allDictionary[productId] = tags;
|
||||
|
||||
// re-save:
|
||||
// this often fails the first time with
|
||||
// The requested operation cannot be performed on a file with a user-mapped section open.
|
||||
// 2nd immediate attempt failing was rare. So I added sleep. We'll see...
|
||||
void resave() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(allDictionary, Formatting.Indented));
|
||||
try { resave(); }
|
||||
catch (IOException debugEx)
|
||||
{
|
||||
// 1000 was always reliable but very slow. trying other values
|
||||
var waitMs = 100;
|
||||
|
||||
System.Threading.Thread.Sleep(waitMs);
|
||||
resave();
|
||||
}
|
||||
}
|
||||
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> cache;
|
||||
|
||||
public static string GetTags(string productId)
|
||||
{
|
||||
var dic = retrieve();
|
||||
return dic.ContainsKey(productId) ? dic[productId] : null;
|
||||
ensureCache();
|
||||
|
||||
cache.TryGetValue(productId, out string value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> retrieve()
|
||||
{
|
||||
if (!FileUtility.FileExists(TagsFile))
|
||||
return new Dictionary<string, string>();
|
||||
lock (locker)
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
|
||||
}
|
||||
}
|
||||
private static void ensureCache()
|
||||
{
|
||||
if (cache is null)
|
||||
lock (locker)
|
||||
cache = !FileUtility.FileExists(TagsFile)
|
||||
? new Dictionary<string, string>()
|
||||
: JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,25 @@ using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApiDTOs;
|
||||
using FileManager;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace InternalUtilities
|
||||
{
|
||||
public class AudibleApiActions
|
||||
{
|
||||
private AsyncRetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
// 2 retries == 3 total
|
||||
.RetryAsync(2);
|
||||
|
||||
public async Task<List<Item>> GetAllLibraryItemsAsync(ILoginCallback callback)
|
||||
{
|
||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
|
||||
// worse, this 1st dummy call doesn't seem to help:
|
||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||
|
||||
try
|
||||
{
|
||||
return await getItemsAsync(callback);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return await getItemsAsync(callback);
|
||||
}
|
||||
return await policy.ExecuteAsync(() => getItemsAsync(callback));
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(ILoginCallback callback)
|
||||
@@ -46,7 +45,7 @@ namespace InternalUtilities
|
||||
// foreach (var childId in childIds)
|
||||
// {
|
||||
// var bookResult = await api.GetLibraryBookAsync(childId, AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
// var bookItem = AudibleApiDTOs.LibraryApiV10.FromJson(bookResult.ToString()).Item;
|
||||
// var bookItem = AudibleApiDTOs.LibraryDtoV10.FromJson(bookResult.ToString()).Item;
|
||||
// items.Add(bookItem);
|
||||
// }
|
||||
#endregion
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace InternalUtilities
|
||||
});
|
||||
|
||||
// important! use this convert method
|
||||
var libResult = LibraryApiV10.FromJson(page.ToString());
|
||||
var libResult = LibraryDtoV10.FromJson(page.ToString());
|
||||
|
||||
if (!libResult.Items.Any())
|
||||
break;
|
||||
|
||||
29
Libation.sln
@@ -7,7 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
__TODO.txt = __TODO.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
lucenenet source code.txt = lucenenet source code.txt
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
@@ -61,10 +60,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "inAudibleLite", "_Demos\inA
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Drawing", "..\Dinah.Core\Dinah.Core.Drawing\Dinah.Core.Drawing.csproj", "{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Windows.Forms", "..\Dinah.Core\Dinah.Core.Windows.Forms\Dinah.Core.Windows.Forms.csproj", "{1306F62D-CDAC-4269-982A-2EED51F0E318}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj", "{35803735-B669-4090-9681-CC7F7FABDC71}"
|
||||
@@ -81,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsDesktopUtilities", "WindowsDesktopUtilities\WindowsDesktopUtilities.csproj", "{E7EFD64D-6630-4426-B09C-B6862A92E3FD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -155,14 +154,6 @@ Global
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1306F62D-CDAC-4269-982A-2EED51F0E318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1306F62D-CDAC-4269-982A-2EED51F0E318}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1306F62D-CDAC-4269-982A-2EED51F0E318}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1306F62D-CDAC-4269-982A-2EED51F0E318}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -195,6 +186,14 @@ Global
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -217,8 +216,6 @@ Global
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{74D02251-898E-4CAF-80C7-801820622903} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{2CAAD73E-E2F9-4888-B04A-3F3803DABDAE} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{1306F62D-CDAC-4269-982A-2EED51F0E318} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
@@ -227,6 +224,8 @@ Global
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -160,8 +160,7 @@ namespace LibationSearchEngine
|
||||
|
||||
private Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
public async Task CreateNewIndexAsync() => await Task.Run(() => createNewIndex(true));
|
||||
private void createNewIndex(bool overwrite)
|
||||
public void CreateNewIndex(bool overwrite = true)
|
||||
{
|
||||
// 300 products
|
||||
// 1st run after app is started: 400ms
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.Drawing\Dinah.Core.Drawing.csproj" />
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.Windows.Forms\Dinah.Core.Windows.Forms.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\WindowsDesktopUtilities\WindowsDesktopUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
30
LibationWinForm/Properties/Resources.Designer.cs
generated
@@ -60,6 +60,36 @@ namespace LibationWinForm.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap default_cover_300x300 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("default_cover_300x300", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap default_cover_500x500 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("default_cover_500x500", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap default_cover_80x80 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("default_cover_80x80", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
|
||||
@@ -118,6 +118,15 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="default_cover_500x500" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\img-coverart-prod-unavailable_500x500.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="default_cover_80x80" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\img-coverart-prod-unavailable_80x80.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="edit_tags_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\edit-tags-25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -56,10 +56,10 @@ namespace LibationWinForm.BookLiberation
|
||||
=> bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
|
||||
public void SetCoverImage(byte[] coverBytes)
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageConverter.GetPictureFromBytes(coverBytes));
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes));
|
||||
|
||||
public void AppendError(Exception ex) => AppendText("ERROR: " + ex.Message);
|
||||
public void AppendText(string text) =>
|
||||
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}"))
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace LibationWinForm.BookLiberation
|
||||
return;
|
||||
|
||||
var backupBook = new BackupBook();
|
||||
backupBook.Download.Completed += SetBackupCountsAsync;
|
||||
backupBook.Decrypt.Completed += SetBackupCountsAsync;
|
||||
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))
|
||||
if (!processable.Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
return await processable.ProcessAsync(libraryBook);
|
||||
}
|
||||
@@ -55,8 +55,8 @@ namespace LibationWinForm.BookLiberation
|
||||
async Task BackupFirstBookAsync()
|
||||
{
|
||||
var backupBook = ProcessorAutomationController.GetWiredUpBackupBook();
|
||||
backupBook.Download.Completed += SetBackupCountsAsync;
|
||||
backupBook.Decrypt.Completed += SetBackupCountsAsync;
|
||||
backupBook.DownloadBook.Completed += SetBackupCountsAsync;
|
||||
backupBook.DecryptBook.Completed += SetBackupCountsAsync;
|
||||
await backupBook.ProcessFirstValidAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ namespace LibationWinForm.BookLiberation
|
||||
{
|
||||
var backupBook = new BackupBook();
|
||||
|
||||
backupBook.Download.Begin += (_, __) => wireUpDownloadable(backupBook.Download);
|
||||
backupBook.Decrypt.Begin += (_, __) => wireUpDecryptable(backupBook.Decrypt);
|
||||
backupBook.DownloadBook.Begin += (_, __) => wireUpDownloadable(backupBook.DownloadBook);
|
||||
backupBook.DecryptBook.Begin += (_, __) => wireUpDecryptable(backupBook.DecryptBook);
|
||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpDecryptable(backupBook.DecryptBook);
|
||||
|
||||
return backupBook;
|
||||
return backupBook;
|
||||
}
|
||||
public static DecryptBook GetWiredUpDecryptBook()
|
||||
{
|
||||
@@ -191,34 +192,43 @@ namespace LibationWinForm.BookLiberation
|
||||
#endregion
|
||||
|
||||
#region define how model actions will affect form behavior
|
||||
void downloadBegin(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Begin: " + str);
|
||||
void downloadBookBegin(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Begin: " + str);
|
||||
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
|
||||
void downloadCompleted(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Completed: " + str);
|
||||
void decryptBegin(object _, string str) => automatedBackupsForm.AppendText("DecryptStep_Begin: " + str);
|
||||
void downloadBookCompleted(object _, string str) => automatedBackupsForm.AppendText("DownloadStep_Completed: " + str);
|
||||
void decryptBookBegin(object _, string str) => automatedBackupsForm.AppendText("DecryptStep_Begin: " + str);
|
||||
// extra line after book is completely finished
|
||||
void decryptCompleted(object _, string str) => automatedBackupsForm.AppendText("DecryptStep_Completed: " + str + Environment.NewLine);
|
||||
#endregion
|
||||
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
|
||||
|
||||
#region subscribe new form to model's events
|
||||
backupBook.Download.Begin += downloadBegin;
|
||||
backupBook.Download.StatusUpdate += statusUpdate;
|
||||
backupBook.Download.Completed += downloadCompleted;
|
||||
backupBook.Decrypt.Begin += decryptBegin;
|
||||
backupBook.Decrypt.StatusUpdate += statusUpdate;
|
||||
backupBook.Decrypt.Completed += decryptCompleted;
|
||||
#endregion
|
||||
#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
|
||||
|
||||
#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.Download.Begin -= downloadBegin;
|
||||
backupBook.Download.StatusUpdate -= statusUpdate;
|
||||
backupBook.Download.Completed -= downloadCompleted;
|
||||
backupBook.Decrypt.Begin -= decryptBegin;
|
||||
backupBook.Decrypt.StatusUpdate -= statusUpdate;
|
||||
backupBook.Decrypt.Completed -= decryptCompleted;
|
||||
};
|
||||
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
|
||||
|
||||
await runBackupLoop(backupBook, automatedBackupsForm);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace LibationWinForm
|
||||
{
|
||||
public interface IIndexLibraryDialog : IRunnableDialog
|
||||
{
|
||||
int TotalBooksProcessed { get; }
|
||||
int NewBooksAdded { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
public interface IRunnableDialog
|
||||
{
|
||||
IButtonControl AcceptButton { get; set; }
|
||||
Control.ControlCollection Controls { get; }
|
||||
Task DoMainWorkAsync();
|
||||
string SuccessMessage { get; }
|
||||
DialogResult ShowDialog();
|
||||
DialogResult DialogResult { get; set; }
|
||||
void Close();
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
public static class IRunnableDialogExt
|
||||
{
|
||||
public static DialogResult RunDialog(this IRunnableDialog dialog)
|
||||
{
|
||||
// hook up runner before dialog.ShowDialog for all
|
||||
var acceptButton = (ButtonBase)dialog.AcceptButton;
|
||||
acceptButton.Click += acceptButton_Click;
|
||||
|
||||
return dialog.ShowDialog();
|
||||
}
|
||||
|
||||
// running/workflow logic is in IndexDialogRunner.Run()
|
||||
private static async void acceptButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = ((Control)sender).FindForm();
|
||||
var iRunnableDialog = form as IRunnableDialog;
|
||||
|
||||
try
|
||||
{
|
||||
await iRunnableDialog.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Did the database get created correctly? Including seed data. Eg: Update-Database", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task Run(this IRunnableDialog dialog)
|
||||
{
|
||||
// get top level controls only. If Enabled, disable and push on stack
|
||||
var disabledStack = disable(dialog);
|
||||
|
||||
// lazy-man's async. also violates the intent of async/await.
|
||||
// use here for now simply for UI responsiveness
|
||||
await dialog.DoMainWorkAsync().ConfigureAwait(true);
|
||||
|
||||
// after running, unwind and re-enable
|
||||
enable(disabledStack);
|
||||
|
||||
MessageBox.Show(dialog.SuccessMessage);
|
||||
|
||||
dialog.DialogResult = DialogResult.OK;
|
||||
dialog.Close();
|
||||
}
|
||||
static Stack<Control> disable(IRunnableDialog dialog)
|
||||
{
|
||||
var disableStack = new Stack<Control>();
|
||||
foreach (Control ctrl in dialog.Controls)
|
||||
{
|
||||
if (ctrl.Enabled)
|
||||
{
|
||||
disableStack.Push(ctrl);
|
||||
ctrl.Enabled = false;
|
||||
}
|
||||
}
|
||||
return disableStack;
|
||||
}
|
||||
static void enable(Stack<Control> disabledStack)
|
||||
{
|
||||
while (disabledStack.Count > 0)
|
||||
{
|
||||
var ctrl = disabledStack.Pop();
|
||||
ctrl.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "IndexLibraryDialog";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Scan Library";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
public partial class IndexLibraryDialog : Form, IIndexLibraryDialog
|
||||
public partial class IndexLibraryDialog : Form
|
||||
{
|
||||
public IndexLibraryDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var btn = new Button();
|
||||
AcceptButton = btn;
|
||||
|
||||
btn.Location = new System.Drawing.Point(this.Size.Width + 10, 0);
|
||||
// required for FindForm() to work
|
||||
this.Controls.Add(btn);
|
||||
|
||||
this.Shown += (_, __) => AcceptButton.PerformClick();
|
||||
}
|
||||
|
||||
List<string> successMessages { get; } = new List<string>();
|
||||
public string SuccessMessage => string.Join("\r\n", successMessages);
|
||||
|
||||
public int NewBooksAdded { get; private set; }
|
||||
public int TotalBooksProcessed { get; private set; }
|
||||
|
||||
public async Task DoMainWorkAsync()
|
||||
public IndexLibraryDialog()
|
||||
{
|
||||
var callback = new Login.WinformResponder();
|
||||
var indexer = new LibraryIndexer();
|
||||
(TotalBooksProcessed, NewBooksAdded) = await indexer.IndexAsync(callback);
|
||||
InitializeComponent();
|
||||
this.Shown += IndexLibraryDialog_Shown;
|
||||
}
|
||||
|
||||
successMessages.Add($"Total processed: {TotalBooksProcessed}");
|
||||
successMessages.Add($"New: {NewBooksAdded}");
|
||||
private async void IndexLibraryDialog_Shown(object sender, System.EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
|
||||
}
|
||||
catch
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
//
|
||||
// libationFilesMyDocsRb
|
||||
//
|
||||
this.libationFilesMyDocsRb.AutoSize = true;
|
||||
this.libationFilesMyDocsRb.AutoSize = true;
|
||||
this.libationFilesMyDocsRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.libationFilesMyDocsRb.Location = new System.Drawing.Point(9, 68);
|
||||
this.libationFilesMyDocsRb.Name = "libationFilesMyDocsRb";
|
||||
|
||||
@@ -1,110 +1,117 @@
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
public partial class SettingsDialog : Form
|
||||
{
|
||||
Configuration config { get; } = Configuration.Instance;
|
||||
Func<string, string> desc { get; } = Configuration.GetDescription;
|
||||
string exeRoot { get; }
|
||||
string myDocs { get; }
|
||||
public partial class SettingsDialog : Form
|
||||
{
|
||||
Configuration config { get; } = Configuration.Instance;
|
||||
Func<string, string> desc { get; } = Configuration.GetDescription;
|
||||
string exeRoot { get; }
|
||||
string myDocs { get; }
|
||||
|
||||
public SettingsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
audibleLocaleCb.SelectedIndex = 0;
|
||||
bool isFirstLoad;
|
||||
|
||||
public SettingsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.libationFilesCustomTb.TextChanged += (_, __) =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(libationFilesCustomTb.Text))
|
||||
this.libationFilesCustomRb.Checked = true;
|
||||
};
|
||||
|
||||
exeRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), "Libation"));
|
||||
myDocs = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
}
|
||||
myDocs = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
}
|
||||
|
||||
private void SettingsDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
this.settingsFileTb.Text = config.Filepath;
|
||||
this.settingsFileDescLbl.Text = desc(nameof(config.Filepath));
|
||||
private void SettingsDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
isFirstLoad = string.IsNullOrWhiteSpace(config.Books);
|
||||
|
||||
this.decryptKeyTb.Text = config.DecryptKey;
|
||||
this.decryptKeyDescLbl.Text = desc(nameof(config.DecryptKey));
|
||||
this.settingsFileTb.Text = config.Filepath;
|
||||
this.settingsFileDescLbl.Text = desc(nameof(config.Filepath));
|
||||
|
||||
this.booksLocationTb.Text = config.Books;
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
this.decryptKeyTb.Text = config.DecryptKey;
|
||||
this.decryptKeyDescLbl.Text = desc(nameof(config.DecryptKey));
|
||||
|
||||
this.audibleLocaleCb.Text = config.LocaleCountryCode;
|
||||
this.booksLocationTb.Text
|
||||
= !string.IsNullOrWhiteSpace(config.Books)
|
||||
? config.Books
|
||||
: Path.GetDirectoryName(Exe.FileLocationOnDisk);
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
|
||||
libationFilesDescLbl.Text = desc(nameof(config.LibationFiles));
|
||||
this.libationFilesRootRb.Text = "In the same folder that Libation is running from\r\n" + exeRoot;
|
||||
this.libationFilesMyDocsRb.Text = "In My Documents\r\n" + myDocs;
|
||||
if (config.LibationFiles == exeRoot)
|
||||
libationFilesRootRb.Checked = true;
|
||||
else if (config.LibationFiles == myDocs)
|
||||
libationFilesMyDocsRb.Checked = true;
|
||||
else
|
||||
{
|
||||
libationFilesCustomRb.Checked = true;
|
||||
libationFilesCustomTb.Text = config.LibationFiles;
|
||||
}
|
||||
this.audibleLocaleCb.Text
|
||||
= !string.IsNullOrWhiteSpace(config.LocaleCountryCode)
|
||||
? config.LocaleCountryCode
|
||||
: "us";
|
||||
|
||||
this.downloadsInProgressDescLbl.Text = desc(nameof(config.DownloadsInProgressEnum));
|
||||
var winTempDownloadsInProgress = Path.Combine(config.WinTemp, "DownloadsInProgress");
|
||||
this.downloadsInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDownloadsInProgress;
|
||||
switch (config.DownloadsInProgressEnum)
|
||||
{
|
||||
case "LibationFiles":
|
||||
downloadsInProgressLibationFilesRb.Checked = true;
|
||||
break;
|
||||
case "WinTemp":
|
||||
default:
|
||||
downloadsInProgressWinTempRb.Checked = true;
|
||||
break;
|
||||
}
|
||||
libationFilesDescLbl.Text = desc(nameof(config.LibationFiles));
|
||||
this.libationFilesRootRb.Text = "In the same folder that Libation is running from\r\n" + exeRoot;
|
||||
this.libationFilesMyDocsRb.Text = "In My Documents\r\n" + myDocs;
|
||||
if (config.LibationFiles == exeRoot)
|
||||
libationFilesRootRb.Checked = true;
|
||||
else if (config.LibationFiles == myDocs)
|
||||
libationFilesMyDocsRb.Checked = true;
|
||||
else
|
||||
{
|
||||
libationFilesCustomRb.Checked = true;
|
||||
libationFilesCustomTb.Text = config.LibationFiles;
|
||||
}
|
||||
|
||||
this.decryptInProgressDescLbl.Text = desc(nameof(config.DecryptInProgressEnum));
|
||||
var winTempDecryptInProgress = Path.Combine(config.WinTemp, "DecryptInProgress");
|
||||
this.decryptInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDecryptInProgress;
|
||||
switch (config.DecryptInProgressEnum)
|
||||
{
|
||||
case "LibationFiles":
|
||||
decryptInProgressLibationFilesRb.Checked = true;
|
||||
break;
|
||||
case "WinTemp":
|
||||
default:
|
||||
decryptInProgressWinTempRb.Checked = true;
|
||||
break;
|
||||
}
|
||||
this.downloadsInProgressDescLbl.Text = desc(nameof(config.DownloadsInProgressEnum));
|
||||
var winTempDownloadsInProgress = Path.Combine(config.WinTemp, "DownloadsInProgress");
|
||||
this.downloadsInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDownloadsInProgress;
|
||||
switch (config.DownloadsInProgressEnum)
|
||||
{
|
||||
case "LibationFiles":
|
||||
downloadsInProgressLibationFilesRb.Checked = true;
|
||||
break;
|
||||
case "WinTemp":
|
||||
default:
|
||||
downloadsInProgressWinTempRb.Checked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.decryptInProgressDescLbl.Text = desc(nameof(config.DecryptInProgressEnum));
|
||||
var winTempDecryptInProgress = Path.Combine(config.WinTemp, "DecryptInProgress");
|
||||
this.decryptInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDecryptInProgress;
|
||||
switch (config.DecryptInProgressEnum)
|
||||
{
|
||||
case "LibationFiles":
|
||||
decryptInProgressLibationFilesRb.Checked = true;
|
||||
break;
|
||||
case "WinTemp":
|
||||
default:
|
||||
decryptInProgressWinTempRb.Checked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
libationFiles_Changed(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void libationFiles_Changed(object sender, EventArgs e)
|
||||
{
|
||||
var libationFilesDir
|
||||
= libationFilesRootRb.Checked ? exeRoot
|
||||
: libationFilesMyDocsRb.Checked ? myDocs
|
||||
: libationFilesCustomTb.Text;
|
||||
private void libationFiles_Changed(object sender, EventArgs e)
|
||||
{
|
||||
var libationFilesDir
|
||||
= libationFilesRootRb.Checked ? exeRoot
|
||||
: libationFilesMyDocsRb.Checked ? myDocs
|
||||
: libationFilesCustomTb.Text;
|
||||
|
||||
var downloadsInProgress = Path.Combine(libationFilesDir, "DownloadsInProgress");
|
||||
this.downloadsInProgressLibationFilesRb.Text = $"In your Libation Files (ie: program-created files)\r\n{downloadsInProgress}";
|
||||
var downloadsInProgress = Path.Combine(libationFilesDir, "DownloadsInProgress");
|
||||
this.downloadsInProgressLibationFilesRb.Text = $"In your Libation Files (ie: program-created files)\r\n{downloadsInProgress}";
|
||||
|
||||
var decryptInProgress = Path.Combine(libationFilesDir, "DecryptInProgress");
|
||||
this.decryptInProgressLibationFilesRb.Text = $"In your Libation Files (ie: program-created files)\r\n{decryptInProgress}";
|
||||
}
|
||||
var decryptInProgress = Path.Combine(libationFilesDir, "DecryptInProgress");
|
||||
this.decryptInProgressLibationFilesRb.Text = $"In your Libation Files (ie: program-created files)\r\n{decryptInProgress}";
|
||||
}
|
||||
|
||||
private void booksLocationSearchBtn_Click(object sender, EventArgs e) => selectFolder("Search for books location", this.booksLocationTb);
|
||||
private void booksLocationSearchBtn_Click(object sender, EventArgs e) => selectFolder("Search for books location", this.booksLocationTb);
|
||||
|
||||
private void libationFilesCustomBtn_Click(object sender, EventArgs e) => selectFolder("Search for Libation Files location", this.libationFilesCustomTb);
|
||||
|
||||
private static void selectFolder(string desc, TextBox textbox)
|
||||
private static void selectFolder(string desc, TextBox textbox)
|
||||
{
|
||||
using var dialog = new FolderBrowserDialog { Description = desc, SelectedPath = "" };
|
||||
dialog.ShowDialog();
|
||||
@@ -143,7 +150,7 @@ namespace LibationWinForm
|
||||
config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
|
||||
if (pathsChanged)
|
||||
if (!isFirstLoad && pathsChanged)
|
||||
{
|
||||
var shutdownResult = MessageBox.Show(
|
||||
"You have changed a file path important for this program. All files will remain in their original location; nothing will be moved. It is highly recommended that you restart this program so these changes are handled correctly."
|
||||
@@ -164,5 +171,5 @@ namespace LibationWinForm
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e) => this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
LibationWinForm/UNTESTED/Form1.Designer.cs
generated
@@ -113,14 +113,14 @@
|
||||
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.scanLibraryToolStripMenuItem});
|
||||
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
|
||||
this.importToolStripMenuItem.Size = new System.Drawing.Size(47, 20);
|
||||
this.importToolStripMenuItem.Text = "&Import";
|
||||
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
|
||||
this.importToolStripMenuItem.Text = "&Import";
|
||||
//
|
||||
// scanLibraryToolStripMenuItem
|
||||
//
|
||||
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
|
||||
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(277, 22);
|
||||
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
|
||||
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(138, 22);
|
||||
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
|
||||
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
|
||||
//
|
||||
// liberateToolStripMenuItem
|
||||
@@ -135,16 +135,16 @@
|
||||
// beginBookBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(201, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book Backups: {0}";
|
||||
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
|
||||
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// beginPdfBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(201, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Backups: {0}";
|
||||
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
|
||||
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// quickFiltersToolStripMenuItem
|
||||
//
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.Drawing;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using FileManager;
|
||||
|
||||
@@ -32,16 +33,24 @@ namespace LibationWinForm
|
||||
pdfsCountsLbl_Format = pdfsCountsLbl.Text;
|
||||
visibleCountLbl_Format = visibleCountLbl.Text;
|
||||
|
||||
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
|
||||
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
|
||||
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
|
||||
}
|
||||
|
||||
private async 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;
|
||||
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;
|
||||
|
||||
reloadGrid();
|
||||
// load default/missing cover images. this will also initiate the background image downloader
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
setVisibleCount(null, 0);
|
||||
|
||||
reloadGrid();
|
||||
|
||||
// also applies filter. ONLY call AFTER loading grid
|
||||
loadInitialQuickFilterState();
|
||||
@@ -50,128 +59,10 @@ namespace LibationWinForm
|
||||
backupsCountsLbl.Text = "[Calculating backed up book quantities]";
|
||||
pdfsCountsLbl.Text = "[Calculating backed up PDFs]";
|
||||
|
||||
await setBackupCountsAsync();
|
||||
setBackupCounts();
|
||||
}
|
||||
}
|
||||
|
||||
#region bottom: qty books visible
|
||||
public void SetVisibleCount(int qty, string str = null)
|
||||
{
|
||||
visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
visibleCountLbl.Text += " | " + str;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region bottom: backup counts
|
||||
private async Task setBackupCountsAsync()
|
||||
{
|
||||
var books = LibraryQueries.GetLibrary_Flat_NoTracking()
|
||||
.Select(sp => sp.Book)
|
||||
.ToList();
|
||||
|
||||
await setBookBackupCountsAsync(books).ConfigureAwait(false);
|
||||
await setPdfBackupCountsAsync(books).ConfigureAwait(false);
|
||||
}
|
||||
enum AudioFileState { full, aax, none }
|
||||
private async Task setBookBackupCountsAsync(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)
|
||||
{
|
||||
if (await AudibleFileStorage.Audio.ExistsAsync(productId))
|
||||
return AudioFileState.full;
|
||||
if (await AudibleFileStorage.AAX.ExistsAsync(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);
|
||||
|
||||
// update bottom numbers
|
||||
var pending = noProgress + downloadedOnly;
|
||||
var text
|
||||
= !results.Any() ? "No books. Begin by indexing your library"
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, noProgress, downloadedOnly, fullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(fullyBackedUp)} backed up";
|
||||
statusStrip1.UIThread(() => backupsCountsLbl.Text = text);
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= pending > 0
|
||||
? $"{pending} remaining"
|
||||
: "All books have been liberated";
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
private async Task setPdfBackupCountsAsync(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);
|
||||
|
||||
// update bottom numbers
|
||||
var text
|
||||
= !boolResults.Any() ? ""
|
||||
: notDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, notDownloaded, downloaded)
|
||||
: $"| All {downloaded} PDFs downloaded";
|
||||
statusStrip1.UIThread(() => pdfsCountsLbl.Text = text);
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= notDownloaded > 0
|
||||
? $"{notDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = notDownloaded > 0);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region reload grid
|
||||
bool isProcessingGridSelect = false;
|
||||
private void reloadGrid()
|
||||
@@ -194,19 +85,98 @@ namespace LibationWinForm
|
||||
if (currProductsGrid != null)
|
||||
{
|
||||
gridPanel.Controls.Remove(currProductsGrid);
|
||||
currProductsGrid.VisibleCountChanged -= setVisibleCount;
|
||||
currProductsGrid.Dispose();
|
||||
}
|
||||
|
||||
currProductsGrid = new ProductsGrid(this) { Dock = DockStyle.Fill };
|
||||
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
||||
currProductsGrid.VisibleCountChanged += setVisibleCount;
|
||||
gridPanel.Controls.Add(currProductsGrid);
|
||||
currProductsGrid.Display();
|
||||
}
|
||||
ResumeLayout();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region bottom: qty books visible
|
||||
private void setVisibleCount(object _, int qty) => visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
|
||||
#endregion
|
||||
|
||||
#region bottom: backup counts
|
||||
private void setBackupCounts()
|
||||
{
|
||||
var books = LibraryQueries.GetLibrary_Flat_NoTracking()
|
||||
.Select(sp => sp.Book)
|
||||
.ToList();
|
||||
|
||||
setBookBackupCounts(books);
|
||||
setPdfBackupCounts(books);
|
||||
}
|
||||
enum AudioFileState { full, aax, none }
|
||||
private void setBookBackupCounts(IEnumerable<Book> books)
|
||||
{
|
||||
AudioFileState getAudioFileState(string productId)
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(productId))
|
||||
return AudioFileState.full;
|
||||
if (AudibleFileStorage.AAX.Exists(productId))
|
||||
return AudioFileState.aax;
|
||||
return AudioFileState.none;
|
||||
}
|
||||
|
||||
var results = books
|
||||
.AsParallel()
|
||||
.Select(b => getAudioFileState(b.AudibleProductId))
|
||||
.ToList();
|
||||
var fullyBackedUp = results.Count(r => r == AudioFileState.full);
|
||||
var downloadedOnly = results.Count(r => r == AudioFileState.aax);
|
||||
var noProgress = results.Count(r => r == AudioFileState.none);
|
||||
|
||||
// update bottom numbers
|
||||
var pending = noProgress + downloadedOnly;
|
||||
var text
|
||||
= !results.Any() ? "No books. Begin by importing your library"
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, noProgress, downloadedOnly, fullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(fullyBackedUp)} backed up";
|
||||
statusStrip1.UIThread(() => backupsCountsLbl.Text = text);
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= pending > 0
|
||||
? $"{pending} remaining"
|
||||
: "All books have been liberated";
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
private void setPdfBackupCounts(IEnumerable<Book> books)
|
||||
{
|
||||
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
|
||||
= !boolResults.Any() ? ""
|
||||
: notDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, notDownloaded, downloaded)
|
||||
: $"| All {downloaded} PDFs downloaded";
|
||||
statusStrip1.UIThread(() => pdfsCountsLbl.Text = text);
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= notDownloaded > 0
|
||||
? $"{notDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = notDownloaded > 0);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
#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)
|
||||
{
|
||||
@@ -247,44 +217,47 @@ namespace LibationWinForm
|
||||
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
|
||||
// re-apply last good filter
|
||||
filterSearchTb.Text = lastGoodFilter;
|
||||
doFilter();
|
||||
doFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region index menu
|
||||
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var dialog = new IndexLibraryDialog();
|
||||
using var dialog = new IndexLibraryDialog();
|
||||
dialog.ShowDialog();
|
||||
|
||||
if (dialog.RunDialog().In(DialogResult.Abort, DialogResult.Cancel, DialogResult.None))
|
||||
return;
|
||||
var totalProcessed = dialog.TotalBooksProcessed;
|
||||
var newAdded = dialog.NewBooksAdded;
|
||||
|
||||
// update backup counts if we have new library items
|
||||
if (dialog.NewBooksAdded > 0)
|
||||
await setBackupCountsAsync();
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
|
||||
if (dialog.TotalBooksProcessed > 0)
|
||||
// update backup counts if we have new library items
|
||||
if (newAdded > 0)
|
||||
setBackupCounts();
|
||||
|
||||
if (totalProcessed > 0)
|
||||
reloadGrid();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region liberate menu
|
||||
private async void setBackupCountsAsync(object _, string __) => await setBackupCountsAsync();
|
||||
#region liberate menu
|
||||
private void setBackupCounts(object _, string __) => setBackupCounts();
|
||||
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var backupBook = BookLiberation.ProcessorAutomationController.GetWiredUpBackupBook();
|
||||
backupBook.Download.Completed += setBackupCountsAsync;
|
||||
backupBook.Decrypt.Completed += setBackupCountsAsync;
|
||||
await BookLiberation.ProcessorAutomationController.RunAutomaticBackup(backupBook);
|
||||
backupBook.DownloadBook.Completed += setBackupCounts;
|
||||
backupBook.DecryptBook.Completed += setBackupCounts;
|
||||
backupBook.DownloadPdf.Completed += setBackupCounts;
|
||||
await BookLiberation.ProcessorAutomationController.RunAutomaticBackup(backupBook);
|
||||
}
|
||||
|
||||
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var downloadPdf = BookLiberation.ProcessorAutomationController.GetWiredUpDownloadPdf();
|
||||
downloadPdf.Completed += setBackupCountsAsync;
|
||||
downloadPdf.Completed += setBackupCounts;
|
||||
await BookLiberation.ProcessorAutomationController.RunAutomaticDownload(downloadPdf);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -37,9 +37,7 @@ namespace LibationWinForm
|
||||
public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value);
|
||||
|
||||
public Image Cover =>
|
||||
Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes(
|
||||
FileManager.PictureStorage.GetImage(book.PictureId, FileManager.PictureStorage.PictureSize._80x80)
|
||||
);
|
||||
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(book.PictureId, FileManager.PictureSize._80x80);
|
||||
|
||||
public string Title
|
||||
{
|
||||
@@ -148,8 +146,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)
|
||||
@@ -170,8 +168,8 @@ namespace LibationWinForm
|
||||
get
|
||||
{
|
||||
var print
|
||||
= FileManager.AudibleFileStorage.Audio.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult() ? "Liberated"
|
||||
: FileManager.AudibleFileStorage.AAX.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult() ? "DRM"
|
||||
= FileManager.AudibleFileStorage.Audio.Exists(book.AudibleProductId) ? "Liberated"
|
||||
: FileManager.AudibleFileStorage.AAX.Exists(book.AudibleProductId) ? "DRM"
|
||||
: "NOT d/l'ed";
|
||||
|
||||
if (!book.Supplements.Any())
|
||||
@@ -180,7 +178,7 @@ namespace LibationWinForm
|
||||
print += "\r\n";
|
||||
|
||||
var downloadStatuses = book.Supplements
|
||||
.Select(d => FileManager.AudibleFileStorage.PDF.ExistsAsync(book.AudibleProductId).GetAwaiter().GetResult())
|
||||
.Select(d => FileManager.AudibleFileStorage.PDF.Exists(book.AudibleProductId))
|
||||
// break delayed execution right now!
|
||||
.ToList();
|
||||
var count = downloadStatuses.Count;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.DataBinding;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
@@ -22,16 +25,22 @@ namespace LibationWinForm
|
||||
// - click on Data Sources > ProductItem. drowdown: DataGridView
|
||||
// - drag/drop ProductItem on design surface
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
private DataGridView dataGridView;
|
||||
{
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
|
||||
private Form1 parent;
|
||||
private DataGridView dataGridView;
|
||||
private LibationContext context;
|
||||
|
||||
// this is a simple ctor for loading library and wish list. can expand later for other options. eg: overload ctor
|
||||
public ProductsGrid(Form1 parent) : this() => this.parent = parent;
|
||||
public ProductsGrid() => InitializeComponent();
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
private bool hasBeenDisplayed = false;
|
||||
Disposed += (_, __) => context?.Dispose();
|
||||
|
||||
manageLiveImageUpdateSubscriptions();
|
||||
}
|
||||
|
||||
private bool hasBeenDisplayed = false;
|
||||
public void Display()
|
||||
{
|
||||
if (hasBeenDisplayed)
|
||||
@@ -54,7 +63,7 @@ namespace LibationWinForm
|
||||
dataGridView.CellFormatting += replaceFormatted;
|
||||
dataGridView.CellFormatting += hiddenFormatting;
|
||||
// sorting breaks filters. must reapply filters after sorting
|
||||
dataGridView.Sorted += (_, __) => Filter();
|
||||
dataGridView.Sorted += (_, __) => filter();
|
||||
|
||||
{ // add tag buttons
|
||||
var editUserTagsButton = new DataGridViewButtonColumn { HeaderText = "Edit Tags" };
|
||||
@@ -88,10 +97,20 @@ namespace LibationWinForm
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
var lib = LibraryQueries.GetLibrary_Flat_NoTracking();
|
||||
//
|
||||
// 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
|
||||
.Select(lb => new GridEntry(lb)).ToList()
|
||||
// default load order
|
||||
@@ -107,36 +126,10 @@ namespace LibationWinForm
|
||||
//
|
||||
gridEntryBindingSource.DataSource = orderedGridEntries.ToSortableBindingList();
|
||||
|
||||
//
|
||||
// AFTER BINDING, BEFORE FILTERING
|
||||
//
|
||||
// now that we have data, remove/hide text columns with blank data. don't search image and button columns.
|
||||
// simplifies the interface in general. also distinuishes library from wish list etc w/o explicit filters.
|
||||
// must be AFTER BINDING, BEFORE FILTERING because we don't want to remove rows when valid data is simply not visible due to filtering.
|
||||
for (var c = dataGridView.ColumnCount - 1; c >= 0; c--)
|
||||
{
|
||||
if (!(dataGridView.Columns[c] is DataGridViewTextBoxColumn textCol))
|
||||
continue;
|
||||
|
||||
bool hasData = false;
|
||||
for (var r = 0; r < dataGridView.RowCount; r++)
|
||||
{
|
||||
var value = dataGridView[c, r].Value;
|
||||
if (value != null && value.ToString() != "")
|
||||
{
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData)
|
||||
dataGridView.Columns.Remove(textCol);
|
||||
}
|
||||
|
||||
//
|
||||
// FILTER
|
||||
//
|
||||
Filter();
|
||||
filter();
|
||||
}
|
||||
|
||||
private void paintEditTag_TextAndImage(object sender, DataGridViewCellPaintingEventArgs e)
|
||||
@@ -192,8 +185,8 @@ namespace LibationWinForm
|
||||
if (editTagsForm.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var qtyChanges = saveChangedTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
return;
|
||||
|
||||
// force a re-draw, and re-apply filters
|
||||
@@ -201,15 +194,7 @@ namespace LibationWinForm
|
||||
// needed to update text colors
|
||||
dataGridView.InvalidateRow(e.RowIndex);
|
||||
|
||||
Filter();
|
||||
}
|
||||
|
||||
private static int saveChangedTags(Book book, string newTags)
|
||||
{
|
||||
book.UserDefinedItem.Tags = newTags;
|
||||
|
||||
var qtyChanges = ApplicationServices.TagUpdater.IndexChangedTags(book);
|
||||
return qtyChanges;
|
||||
filter();
|
||||
}
|
||||
|
||||
#region Cell Formatting
|
||||
@@ -231,30 +216,17 @@ namespace LibationWinForm
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void UpdateRow(string productId)
|
||||
{
|
||||
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
|
||||
{
|
||||
var gridEntry = getGridEntry(r);
|
||||
if (gridEntry.GetBook().AudibleProductId == productId)
|
||||
{
|
||||
var libBook = LibraryQueries.GetLibraryBook_Flat_NoTracking(productId);
|
||||
gridEntry.REPLACE_Library_Book(libBook);
|
||||
dataGridView.InvalidateRow(r);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region filter
|
||||
string _filterSearchString;
|
||||
public void Filter() => Filter(_filterSearchString);
|
||||
private void filter() => Filter(_filterSearchString);
|
||||
public void Filter(string searchString)
|
||||
{
|
||||
_filterSearchString = searchString;
|
||||
|
||||
var searchResults = new LibationSearchEngine.SearchEngine().Search(searchString);
|
||||
if (dataGridView.Rows.Count == 0)
|
||||
return;
|
||||
|
||||
var searchResults = SearchEngineCommands.Search(searchString);
|
||||
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
|
||||
|
||||
// https://stackoverflow.com/a/18942430
|
||||
@@ -265,13 +237,38 @@ namespace LibationWinForm
|
||||
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).GetBook().AudibleProductId);
|
||||
}
|
||||
currencyManager.ResumeBinding();
|
||||
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
|
||||
|
||||
|
||||
// after applying filters, display new visible count
|
||||
parent.SetVisibleCount(dataGridView.Rows.Cast<DataGridViewRow>().Count(r => r.Visible), searchResults.SearchString);
|
||||
var luceneSearchString_debug = searchResults.SearchString;
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
private GridEntry getGridEntry(int rowIndex) => (GridEntry)dataGridView.Rows[rowIndex].DataBoundItem;
|
||||
}
|
||||
#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 GridEntry getGridEntry(int rowIndex) => (GridEntry)dataGridView.Rows[rowIndex].DataBoundItem;
|
||||
|
||||
private int getRowId(Func<GridEntry, bool> func)
|
||||
{
|
||||
for (var r = 0; r < dataGridView.RowCount; r++)
|
||||
if (func(getGridEntry(r)))
|
||||
return r;
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
if (!createSettings())
|
||||
return;
|
||||
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
private static bool createSettings()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(FileManager.Configuration.Instance.Books))
|
||||
return true;
|
||||
|
||||
var welcomeText = @"
|
||||
This appears to be your first time using Libation. Welcome.
|
||||
Please fill in a few settings on the following page. You can also change these settings later.
|
||||
|
||||
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);
|
||||
var dialogResult = new SettingsDialog().ShowDialog();
|
||||
if (dialogResult != DialogResult.OK)
|
||||
{
|
||||
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
186
README.md
@@ -1,14 +1,180 @@
|
||||
# Libation
|
||||
Libation: Liberate your Library
|
||||
# Libation: Liberate your Library
|
||||
|
||||
Audible audiobook manager
|
||||
# Table of Contents
|
||||
|
||||
1. [Audible audiobook manager](#audible-audiobook-manager)
|
||||
- [The good](#the-good)
|
||||
- [The bad](#the-bad)
|
||||
- [The ugly](#the-ugly)
|
||||
2. [Getting started](#getting-started)
|
||||
- [Import your library](#import-your-library)
|
||||
- [Download your books -- DRM-free!](#download-your-books----drm-free)
|
||||
- [Download PDF attachments](#download-pdf-attachments)
|
||||
- [Details of downloaded files](#details-of-downloaded-files)
|
||||
3. [Searching and filtering](#searching-and-filtering)
|
||||
- [Tags](#tags)
|
||||
- [Searches](#searches)
|
||||
- [Search examples](#search-examples)
|
||||
- [Filters](#filters)
|
||||
|
||||
## Audible audiobook manager
|
||||
|
||||
### The good
|
||||
|
||||
* Import library from audible, including cover art
|
||||
* Download and remove DRM from all books
|
||||
* Download accompanying PDFs
|
||||
* Add tags to books for better organization
|
||||
* Powerful advanced search built on the Lucene search engine
|
||||
* Customizable saved filters for common searches
|
||||
* Open source
|
||||
* Tested on US Audible only. Should theoretically also work for Canada, UK, Germany, and France
|
||||
|
||||
<a name="theBad"/>
|
||||
|
||||
### The bad
|
||||
|
||||
* Download
|
||||
* Decrypt. Remove DRM
|
||||
* Organize
|
||||
* Advanced search
|
||||
* Tags
|
||||
* Open-source
|
||||
* Windows only
|
||||
* Several known speed/performance issues
|
||||
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
|
||||
|
||||
Current version is functional but is built around a fragile scraping engine. The next version will replace this part with API calls which will make it significantly more robust.
|
||||
### The ugly
|
||||
|
||||
* Documentation? Yer lookin' at it
|
||||
* This is a single-developer personal passion project. Support, response, updates, enhancements, bug fixes etc are as my free time allows
|
||||
* I have a full-time job, a life, and a finite attention span. Therefore a lot of time can potentially go by with no improvements of any kind
|
||||
|
||||
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
|
||||
|
||||
I made this for myself and I want to share it with the great programming and audible/audiobook communiites which have been so generous with their time and help.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Import your library
|
||||
|
||||
Select Import > Scan Library:
|
||||
|
||||

|
||||
|
||||
You'll see this window while it's scanning:
|
||||
|
||||

|
||||
|
||||
Success! We see how many new titles are imported:
|
||||
|
||||

|
||||
|
||||
### Download your books -- DRM-free!
|
||||
|
||||
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
|
||||
|
||||

|
||||
|
||||
Select Liberate > Begin Book Backups
|
||||
|
||||

|
||||
|
||||
First the original book with DRM is downloaded
|
||||
|
||||

|
||||
|
||||
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
|
||||
|
||||

|
||||
|
||||
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
|
||||
|
||||

|
||||
|
||||
### Download PDF attachments
|
||||
|
||||
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
|
||||
|
||||

|
||||
|
||||
Select Liberate > Begin PDF Backups
|
||||
|
||||

|
||||
|
||||
The downloads work just like with books, only with no additional decryption needed.
|
||||
|
||||

|
||||
|
||||
Ta da!
|
||||
|
||||

|
||||
|
||||
### Details of downloaded files
|
||||
|
||||

|
||||
|
||||
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
|
||||
|
||||
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
|
||||
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
|
||||
* .nfo: This is just some general info about the book and includes some technical stats about the audiofile.
|
||||
|
||||
## Searching and filtering
|
||||
|
||||
### Tags
|
||||
|
||||
To add tags to a title, click the tags button
|
||||
|
||||

|
||||
|
||||
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
|
||||
|
||||

|
||||
|
||||
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
|
||||
|
||||

|
||||
|
||||
To edit tags, just click the button again.
|
||||
|
||||
### Searches
|
||||
|
||||
Libation's advanced searching is built on the powerful Lucene search engine. Simple searches are effortless and powerful searches are simple. To search, just type and click Filter or press enter
|
||||
|
||||
* Type anything in the search box to search common fields: title, authors, narrators, and the book's audible id
|
||||
* Use Lucene's "Query Parser Syntax" for advanced searching.
|
||||
* Easy tutorial: http://www.lucenetutorial.com/lucene-query-syntax.html
|
||||
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
|
||||
* Tons of search fields, specific to audiobooks
|
||||
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
|
||||
* Click [?] button for a full list of search fields and synonyms 
|
||||
* Search by tag like \[this\]
|
||||
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.
|
||||
|
||||
### Search examples
|
||||
|
||||
Search for anything with the word potter
|
||||
|
||||

|
||||
|
||||
If you only want to see Harry Potter
|
||||
|
||||

|
||||
|
||||
If you only want to see potter except for Harry Potter
|
||||
|
||||

|
||||
|
||||
Only books written by Neil Gaiman where he also narrates his own book. (If you don't include AND, you'll see everything written by Neil Gaiman and also all books in your library which are self-narrated.)
|
||||
|
||||

|
||||
|
||||
I tagged autobiographies as auto_bio and biographies written by someone else as bio. I can get only autobiographies with \[auto_bio\] or get both by searching \[bio\]
|
||||
|
||||
![Search example: \[bio\]](images/SearchExampleBio.png)
|
||||
![Search example: \[auto_bio\]](images/SearchExampleAutoBio.png)
|
||||
|
||||
### Filters
|
||||
|
||||
If you have a search you want to save, click Add To Quick Filters to save it in your Quick Filters list. To use it again, select it from the Quick Filters list.
|
||||
|
||||
To edit this list go to Quick Filters > Edit quick filters. Here you can re-order the list, delete filters, double-click a filter to edit it, or double-click the bottom blank box to add a new filter.
|
||||
|
||||
Check "Quick Filters > Start Libation with 1st filter Default" to have your top filter automatically applied when Libation starts. In this top example, I want to always start without these: at books I've tagged hidden, books I've tagged as free_audible_originals, and books which I have rated.
|
||||
|
||||

|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
|
||||
https://github.com/rmcrackan/Libation/releases
|
||||
|
||||
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
|
||||
v3.0.1 : Legacy inAudible wire-up code is still present but is commented out. All future check-ins are not guaranteed to have inAudible wire-up code
|
||||
v3.0 : This version is fully powered by the Audible API. Legacy scraping code is still present but is commented out. All future check-ins are not guaranteed to have any scraping code
|
||||
v2 : new library page scraping. still chrome cookies. all decryption is handled natively. no inAudible dependency
|
||||
v1 : old library ajax scraping. wish list scraping. chrome cookies. directly call local inAudible. .net framework
|
||||
-- end VERSIONING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
|
||||
OPTION 1: UI
|
||||
rt-clk project project > Publish...
|
||||
click Publish
|
||||
|
||||
OPTION 2: cmd line
|
||||
change dir to folder containing project
|
||||
cd C:\[full...path]\Libation\LibationWinForm
|
||||
this will use the parameters specified in csproj
|
||||
dotnet publish -c Release
|
||||
|
||||
OPTION 3: cmd line, custom
|
||||
open csproj
|
||||
remove: PublishTrimmed, PublishReadyToRun, PublishSingleFile, RuntimeIdentifier
|
||||
run customized publish. examples:
|
||||
publish all platforms
|
||||
dotnet publish -c Release
|
||||
publish win64 platform only
|
||||
dotnet publish -r win-x64 -c Release
|
||||
publish win64 platform, single-file
|
||||
dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
|
||||
-- end HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- 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
|
||||
@@ -44,31 +62,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 ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "IndexLibraryDialog";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Scan Library";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
392
WinFormsDesigner/Form1.Designer.cs
generated
@@ -28,219 +28,219 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
|
||||
this.gridPanel = new System.Windows.Forms.Panel();
|
||||
this.filterHelpBtn = new System.Windows.Forms.Button();
|
||||
this.filterBtn = new System.Windows.Forms.Button();
|
||||
this.filterSearchTb = new System.Windows.Forms.TextBox();
|
||||
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
|
||||
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.addFilterBtn = new System.Windows.Forms.Button();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridPanel
|
||||
//
|
||||
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
|
||||
this.gridPanel = new System.Windows.Forms.Panel();
|
||||
this.filterHelpBtn = new System.Windows.Forms.Button();
|
||||
this.filterBtn = new System.Windows.Forms.Button();
|
||||
this.filterSearchTb = new System.Windows.Forms.TextBox();
|
||||
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
|
||||
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.addFilterBtn = new System.Windows.Forms.Button();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridPanel
|
||||
//
|
||||
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.gridPanel.Location = new System.Drawing.Point(12, 56);
|
||||
this.gridPanel.Name = "gridPanel";
|
||||
this.gridPanel.Size = new System.Drawing.Size(839, 386);
|
||||
this.gridPanel.TabIndex = 5;
|
||||
//
|
||||
// filterHelpBtn
|
||||
//
|
||||
this.filterHelpBtn.Location = new System.Drawing.Point(12, 27);
|
||||
this.filterHelpBtn.Name = "filterHelpBtn";
|
||||
this.filterHelpBtn.Size = new System.Drawing.Size(22, 23);
|
||||
this.filterHelpBtn.TabIndex = 3;
|
||||
this.filterHelpBtn.Text = "?";
|
||||
this.filterHelpBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterBtn
|
||||
//
|
||||
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterBtn.Location = new System.Drawing.Point(776, 27);
|
||||
this.filterBtn.Name = "filterBtn";
|
||||
this.filterBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.filterBtn.TabIndex = 2;
|
||||
this.filterBtn.Text = "Filter";
|
||||
this.filterBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterSearchTb
|
||||
//
|
||||
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.gridPanel.Location = new System.Drawing.Point(12, 56);
|
||||
this.gridPanel.Name = "gridPanel";
|
||||
this.gridPanel.Size = new System.Drawing.Size(839, 386);
|
||||
this.gridPanel.TabIndex = 5;
|
||||
//
|
||||
// filterHelpBtn
|
||||
//
|
||||
this.filterHelpBtn.Location = new System.Drawing.Point(12, 27);
|
||||
this.filterHelpBtn.Name = "filterHelpBtn";
|
||||
this.filterHelpBtn.Size = new System.Drawing.Size(22, 23);
|
||||
this.filterHelpBtn.TabIndex = 3;
|
||||
this.filterHelpBtn.Text = "?";
|
||||
this.filterHelpBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterBtn
|
||||
//
|
||||
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterBtn.Location = new System.Drawing.Point(776, 27);
|
||||
this.filterBtn.Name = "filterBtn";
|
||||
this.filterBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.filterBtn.TabIndex = 2;
|
||||
this.filterBtn.Text = "Filter";
|
||||
this.filterBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterSearchTb
|
||||
//
|
||||
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterSearchTb.Location = new System.Drawing.Point(186, 29);
|
||||
this.filterSearchTb.Name = "filterSearchTb";
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(584, 20);
|
||||
this.filterSearchTb.TabIndex = 1;
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.filterSearchTb.Location = new System.Drawing.Point(186, 29);
|
||||
this.filterSearchTb.Name = "filterSearchTb";
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(584, 20);
|
||||
this.filterSearchTb.TabIndex = 1;
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.importToolStripMenuItem,
|
||||
this.liberateToolStripMenuItem,
|
||||
this.quickFiltersToolStripMenuItem,
|
||||
this.settingsToolStripMenuItem});
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Size = new System.Drawing.Size(863, 24);
|
||||
this.menuStrip1.TabIndex = 0;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
// importToolStripMenuItem
|
||||
//
|
||||
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Size = new System.Drawing.Size(863, 24);
|
||||
this.menuStrip1.TabIndex = 0;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
// importToolStripMenuItem
|
||||
//
|
||||
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.scanLibraryToolStripMenuItem});
|
||||
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
|
||||
this.importToolStripMenuItem.Size = new System.Drawing.Size(47, 20);
|
||||
this.importToolStripMenuItem.Text = "&Import";
|
||||
//
|
||||
// scanLibraryToolStripMenuItem
|
||||
//
|
||||
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
|
||||
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(277, 22);
|
||||
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
|
||||
//
|
||||
// liberateToolStripMenuItem
|
||||
//
|
||||
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
|
||||
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
|
||||
this.importToolStripMenuItem.Text = "&Import";
|
||||
//
|
||||
// scanLibraryToolStripMenuItem
|
||||
//
|
||||
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
|
||||
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(138, 22);
|
||||
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
|
||||
//
|
||||
// liberateToolStripMenuItem
|
||||
//
|
||||
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.beginBookBackupsToolStripMenuItem,
|
||||
this.beginPdfBackupsToolStripMenuItem});
|
||||
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
|
||||
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.liberateToolStripMenuItem.Text = "&Liberate";
|
||||
//
|
||||
// beginBookBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(201, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book Backups: {0}";
|
||||
//
|
||||
// beginPdfBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(201, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Backups: {0}";
|
||||
//
|
||||
// quickFiltersToolStripMenuItem
|
||||
//
|
||||
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
|
||||
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.liberateToolStripMenuItem.Text = "&Liberate";
|
||||
//
|
||||
// beginBookBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
|
||||
//
|
||||
// beginPdfBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
|
||||
//
|
||||
// quickFiltersToolStripMenuItem
|
||||
//
|
||||
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.firstFilterIsDefaultToolStripMenuItem,
|
||||
this.editQuickFiltersToolStripMenuItem,
|
||||
this.toolStripSeparator1});
|
||||
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
|
||||
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
|
||||
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
|
||||
//
|
||||
// firstFilterIsDefaultToolStripMenuItem
|
||||
//
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
|
||||
//
|
||||
// editQuickFiltersToolStripMenuItem
|
||||
//
|
||||
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
|
||||
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
|
||||
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters";
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
|
||||
//
|
||||
// settingsToolStripMenuItem
|
||||
//
|
||||
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.settingsToolStripMenuItem.Text = "&Settings";
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
|
||||
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
|
||||
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
|
||||
//
|
||||
// firstFilterIsDefaultToolStripMenuItem
|
||||
//
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
|
||||
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
|
||||
//
|
||||
// editQuickFiltersToolStripMenuItem
|
||||
//
|
||||
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
|
||||
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
|
||||
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters";
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
|
||||
//
|
||||
// settingsToolStripMenuItem
|
||||
//
|
||||
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
|
||||
this.settingsToolStripMenuItem.Text = "&Settings";
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.visibleCountLbl,
|
||||
this.springLbl,
|
||||
this.backupsCountsLbl,
|
||||
this.pdfsCountsLbl});
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 445);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Size = new System.Drawing.Size(863, 22);
|
||||
this.statusStrip1.TabIndex = 6;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
// visibleCountLbl
|
||||
//
|
||||
this.visibleCountLbl.Name = "visibleCountLbl";
|
||||
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
|
||||
this.visibleCountLbl.Text = "Visible: {0}";
|
||||
//
|
||||
// springLbl
|
||||
//
|
||||
this.springLbl.Name = "springLbl";
|
||||
this.springLbl.Size = new System.Drawing.Size(232, 17);
|
||||
this.springLbl.Spring = true;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
//
|
||||
this.backupsCountsLbl.Name = "backupsCountsLbl";
|
||||
this.backupsCountsLbl.Size = new System.Drawing.Size(336, 17);
|
||||
this.backupsCountsLbl.Text = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
|
||||
//
|
||||
// pdfsCountsLbl
|
||||
//
|
||||
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
|
||||
this.pdfsCountsLbl.Size = new System.Drawing.Size(219, 17);
|
||||
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
|
||||
//
|
||||
// addFilterBtn
|
||||
//
|
||||
this.addFilterBtn.Location = new System.Drawing.Point(40, 27);
|
||||
this.addFilterBtn.Name = "addFilterBtn";
|
||||
this.addFilterBtn.Size = new System.Drawing.Size(140, 23);
|
||||
this.addFilterBtn.TabIndex = 4;
|
||||
this.addFilterBtn.Text = "Add To Quick Filters";
|
||||
this.addFilterBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(863, 467);
|
||||
this.Controls.Add(this.filterBtn);
|
||||
this.Controls.Add(this.addFilterBtn);
|
||||
this.Controls.Add(this.filterSearchTb);
|
||||
this.Controls.Add(this.filterHelpBtn);
|
||||
this.Controls.Add(this.statusStrip1);
|
||||
this.Controls.Add(this.gridPanel);
|
||||
this.Controls.Add(this.menuStrip1);
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
this.MainMenuStrip = this.menuStrip1;
|
||||
this.Name = "Form1";
|
||||
this.Text = "Libation: Liberate your Library";
|
||||
this.menuStrip1.ResumeLayout(false);
|
||||
this.menuStrip1.PerformLayout();
|
||||
this.statusStrip1.ResumeLayout(false);
|
||||
this.statusStrip1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 445);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Size = new System.Drawing.Size(863, 22);
|
||||
this.statusStrip1.TabIndex = 6;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
// visibleCountLbl
|
||||
//
|
||||
this.visibleCountLbl.Name = "visibleCountLbl";
|
||||
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
|
||||
this.visibleCountLbl.Text = "Visible: {0}";
|
||||
//
|
||||
// springLbl
|
||||
//
|
||||
this.springLbl.Name = "springLbl";
|
||||
this.springLbl.Size = new System.Drawing.Size(232, 17);
|
||||
this.springLbl.Spring = true;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
//
|
||||
this.backupsCountsLbl.Name = "backupsCountsLbl";
|
||||
this.backupsCountsLbl.Size = new System.Drawing.Size(336, 17);
|
||||
this.backupsCountsLbl.Text = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
|
||||
//
|
||||
// pdfsCountsLbl
|
||||
//
|
||||
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
|
||||
this.pdfsCountsLbl.Size = new System.Drawing.Size(219, 17);
|
||||
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
|
||||
//
|
||||
// addFilterBtn
|
||||
//
|
||||
this.addFilterBtn.Location = new System.Drawing.Point(40, 27);
|
||||
this.addFilterBtn.Name = "addFilterBtn";
|
||||
this.addFilterBtn.Size = new System.Drawing.Size(140, 23);
|
||||
this.addFilterBtn.TabIndex = 4;
|
||||
this.addFilterBtn.Text = "Add To Quick Filters";
|
||||
this.addFilterBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(863, 467);
|
||||
this.Controls.Add(this.filterBtn);
|
||||
this.Controls.Add(this.addFilterBtn);
|
||||
this.Controls.Add(this.filterSearchTb);
|
||||
this.Controls.Add(this.filterHelpBtn);
|
||||
this.Controls.Add(this.statusStrip1);
|
||||
this.Controls.Add(this.gridPanel);
|
||||
this.Controls.Add(this.menuStrip1);
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
this.MainMenuStrip = this.menuStrip1;
|
||||
this.Name = "Form1";
|
||||
this.Text = "Libation: Liberate your Library";
|
||||
this.menuStrip1.ResumeLayout(false);
|
||||
this.menuStrip1.PerformLayout();
|
||||
this.statusStrip1.ResumeLayout(false);
|
||||
this.statusStrip1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -170,11 +170,4 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
28
WindowsDesktopUtilities/UNTESTED/WinAudibleImageServer.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using Dinah.Core.Drawing;
|
||||
using FileManager;
|
||||
|
||||
namespace WindowsDesktopUtilities
|
||||
{
|
||||
public static class WinAudibleImageServer
|
||||
{
|
||||
private static Dictionary<PictureDefinition, Image> cache { get; } = new Dictionary<PictureDefinition, Image>();
|
||||
|
||||
public static Image GetImage(string pictureId, PictureSize size)
|
||||
{
|
||||
var def = new PictureDefinition(pictureId, size);
|
||||
if (!cache.ContainsKey(def))
|
||||
{
|
||||
(var isDefault, var bytes) = PictureStorage.GetPicture(def);
|
||||
|
||||
var image = ImageReader.ToImage(bytes);
|
||||
if (isDefault)
|
||||
return image;
|
||||
cache[def] = image;
|
||||
}
|
||||
return cache[def];
|
||||
}
|
||||
}
|
||||
}
|
||||
16
WindowsDesktopUtilities/WindowsDesktopUtilities.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -29,36 +29,13 @@ since we have mult contexts, must use -context:
|
||||
Update-Database -context LibationContext
|
||||
|
||||
|
||||
QUICK VIEW
|
||||
==========
|
||||
declare @productId nvarchar(450) = 'B075Y4SWJ8'
|
||||
declare @bookId int
|
||||
declare @catId int
|
||||
select @bookId = b.BookId from Books b where b.AudibleProductId = @productId
|
||||
select @catId = b.CategoryId from Books b where b.AudibleProductId = @productId
|
||||
select * from Books b where b.AudibleProductId = @productId
|
||||
select *
|
||||
from BookContributor bc
|
||||
join Contributors c on bc.ContributorId = c.ContributorId
|
||||
where bc.BookId = @bookId order by role, [order]
|
||||
select *
|
||||
from SeriesBook sb
|
||||
join Series s on sb.SeriesId = s.SeriesId
|
||||
where sb.BookId = @bookId
|
||||
select *
|
||||
from Categories c1
|
||||
left join Categories c2 on c1.ParentCategoryCategoryId = c2.CategoryId
|
||||
where c1.CategoryId = @catId
|
||||
|
||||
|
||||
ERROR
|
||||
=====
|
||||
Add-Migration : The term 'Add-Migration' is not recognized as the name of a cmdlet, function, script file, or operable program
|
||||
|
||||
SOLUTION
|
||||
--------
|
||||
dependencies > manage nuget packages
|
||||
add: Microsoft.EntityFrameworkCore.Tools
|
||||
add nuget pkg: Microsoft.EntityFrameworkCore.Tools
|
||||
|
||||
|
||||
SQLite
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace inAudibleLite
|
||||
this.txtInputFile.Text = openFileDialog.FileName;
|
||||
converter = await AaxToM4bConverter.CreateAsync(this.txtInputFile.Text, this.decryptKeyTb.Text);
|
||||
|
||||
pictureBox1.Image = Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes(converter.coverBytes);
|
||||
pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(converter.coverBytes);
|
||||
|
||||
this.txtOutputFile.Text = converter.outputFileName;
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Dinah.Core\Dinah.Core.Drawing\Dinah.Core.Drawing.csproj" />
|
||||
<ProjectReference Include="..\..\..\Dinah.Core\Dinah.Core.Windows.Forms\Dinah.Core.Windows.Forms.csproj" />
|
||||
<ProjectReference Include="..\..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
|
||||
<ProjectReference Include="..\..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
169
__TODO.txt
@@ -1,169 +0,0 @@
|
||||
-- begin REPLACE SCRAPING WITH API ---------------------------------------------------------------------------------------------------------------------
|
||||
remove "legacy inAudible wire-up code"
|
||||
-- end REPLACE SCRAPING WITH API ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
|
||||
imports are PAINFULLY slow for just a few hundred items. wtf is taking so long?
|
||||
-- end ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
|
||||
when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow
|
||||
https://stackoverflow.com/a/12046333
|
||||
https://codereview.stackexchange.com/a/135074
|
||||
// do NOT use lock() or Monitor with async/await
|
||||
private static int _lockFlag = 0; // 0 - free
|
||||
if (Interlocked.CompareExchange(ref _lockFlag, 1, 0) != 0) return;
|
||||
// only 1 thread will enter here without locking the object/put the other threads to sleep
|
||||
try { await DoWorkAsync(); }
|
||||
// free the lock
|
||||
finally { Interlocked.Decrement(ref _lockFlag); }
|
||||
|
||||
use stop light icons for liberated state: red=none, yellow=downloaded encrypted, green=liberated
|
||||
|
||||
need a way to liberate ad hoc books and pdf.s
|
||||
|
||||
use pdf icon with and without and X over it to indicate status
|
||||
-- end ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
|
||||
Audible API. GET /1.0/library , GET /1.0/library/{asin}
|
||||
TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryApiV10
|
||||
-- end ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
datalayer stuff (eg: Book) need better ToString
|
||||
-- end ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
with libation closed, move files
|
||||
start libation
|
||||
can get error below
|
||||
fixed on restart
|
||||
|
||||
Form1_Load ... await setBackupCountsAsync();
|
||||
Collection was modified; enumeration operation may not execute.
|
||||
stack trace
|
||||
at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
|
||||
at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
|
||||
at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at FileManager.FilePathCache.GetPath(String id, FileType type) in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\FilePathCache.cs:line 33
|
||||
at FileManager.AudibleFileStorage.<getAsync>d__32.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 112
|
||||
at FileManager.AudibleFileStorage.<GetAsync>d__31.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 107
|
||||
at FileManager.AudibleFileStorage.<ExistsAsync>d__30.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 104
|
||||
at LibationWinForm.Form1.<<setBookBackupCountsAsync>g__getAudioFileStateAsync|15_1>d.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 110
|
||||
at LibationWinForm.Form1.<setBookBackupCountsAsync>d__15.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 117
|
||||
at LibationWinForm.Form1.<setBackupCountsAsync>d__13.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 81
|
||||
at LibationWinForm.Form1.<Form1_Load>d__11.MoveNext() in C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\Libation\LibationWinForm\UNTESTED\Form1.cs:line 60
|
||||
-- end BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
.\appsettings.json should only be a pointer to the real settings file location: LibationSettings.json
|
||||
replace complex config saving throughout with new way in my ConsoleDependencyInjection solution
|
||||
all settings should be strongly typed
|
||||
re-create my shortcuts and bak
|
||||
|
||||
multiple files named "appsettings.json" will overwrite each other
|
||||
libraries should avoid this generic name. ok for applications to use them
|
||||
|
||||
Audible API
|
||||
C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\audible api\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\L1
|
||||
C:\Dropbox\Dinah's folder\coding\_NET\Visual Studio 2019\audible api\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\ComputedTestValues
|
||||
14+ json files
|
||||
these can go in a shared solution folder
|
||||
BasePath => recursively search directories upward-only until fild dir with .sln
|
||||
from here can set up a shared dir anywhere. use recursive upward search to find it. store shared files here. eg: identityTokens.json, ComputedTestValues
|
||||
-- end CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
pulling previous tags into new Books. think: reloading db
|
||||
move out of Book and into DtoMapper?
|
||||
|
||||
Extract file and tag stuff from domain objects. This should exist only in data layer. If domain objects are able to call EF context, it should go through data layer
|
||||
Why are tags in file AND database?
|
||||
|
||||
extract FileManager dependency from data layer
|
||||
-- end TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
|
||||
add support for multiple categories
|
||||
when i do this, learn about the different CategoryLadder.Root enums. probably only need Root.Genres
|
||||
-- end ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
|
||||
my ui sucks. it's also tightly coupled with biz logic. can't replace ui until biz logic is extracted and loosely. remove all biz logic from presentation/winforms layer
|
||||
-- end CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
|
||||
all "UNTESTED" code needs unit tests
|
||||
Turn into unit tests or demos
|
||||
TextBoxBaseTextWriter.cs
|
||||
EnumerationExamples.cs
|
||||
EnumExt.cs
|
||||
IEnumerable[T]Ext.cs
|
||||
MultiTextWriter.cs
|
||||
Selenium.Examples.cs
|
||||
ScratchPad.cs
|
||||
ProcessorAutomationController.Examples.cs
|
||||
ScraperRules.Examples.cs
|
||||
ByFactory.Example.cs
|
||||
search 'example code' on: LibationWinForm\...\Form1.cs
|
||||
EnumerationFlagsExtensions.EXAMPLES()
|
||||
// examples
|
||||
scratchpad
|
||||
scratch pad
|
||||
scratch_pad
|
||||
-- end UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin DECRYPTING ---------------------------------------------------------------------------------------------------------------------
|
||||
replace inaudible/inaudible lite with pure ffmpeg
|
||||
benefits of inaudible:
|
||||
highly configurable
|
||||
embedded cover image
|
||||
chapter-ized
|
||||
cue and nfo files
|
||||
can hopefully get most of this with simple decrypt. possibly including the new chapter titles
|
||||
|
||||
better chapters in many m4b files. to see, try re-downloading. examples: bobiverse, sharp objects
|
||||
|
||||
raw ffmpeg decrypting is significantly faster than inAudible/AaxDecrypter method:
|
||||
inAudible/AaxDecrypter
|
||||
tiny file
|
||||
40 sec decrypt
|
||||
40 sec chapterize + tag
|
||||
huge file
|
||||
60 sec decrypt
|
||||
120 sec chapterize + tag
|
||||
directly call ffmpeg (decrypt only)
|
||||
tiny file
|
||||
17 sec decrypt
|
||||
huge file
|
||||
39 sec decrypt
|
||||
-- end DECRYPTING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, UI: LONG RUNNING TASKS ---------------------------------------------------------------------------------------------------------------------
|
||||
long running tasks are appropriately async. however there's no way for the user to see that the task is running (vs nothing happened) except to wait and see if the final notification ever comes
|
||||
need events to update UI with progress
|
||||
-- end ENHANCEMENT, UI: LONG RUNNING TASKS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
tag edits still take forever and block UI
|
||||
unlikely to be an issue with file write. in fact, should probably roll back this change
|
||||
also touches parts of code which: db write via a hook, search engine re-index
|
||||
-- end ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
|
||||
how to remove a book?
|
||||
previously difficult due to implementation details regarding scraping and importing. should be trivial after api replaces scraping
|
||||
-- end ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
|
||||
menu views. filter could work for grid display; just use the lucene query language
|
||||
1) menu to show all tags and count of each. click on tag so see only those books
|
||||
2) tree to show all categories and subcategories. click on category so see only those books
|
||||
-- end ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: LOGGING, ERROR HANDLING ---------------------------------------------------------------------------------------------------------------------
|
||||
LibationWinForm and Audible API need better logging and error handling
|
||||
incl log levels, db query logging
|
||||
see AaxDecryptorWinForms.initLogging()
|
||||
-- end ENHANCEMENT: LOGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -1,55 +0,0 @@
|
||||
proposed extensible schema to generalize beyond audible
|
||||
|
||||
problems
|
||||
0) reeks of premature optimization
|
||||
- i'm currently only doing audible audiobooks. this adds several layers of abstraction for the sake of possible expansion
|
||||
- there's a good chance that supporting another platform may not conform to this schema, in which case i'd have done this for nothing. genres are one likely pain point
|
||||
- libation is currently single-user. hopefully the below would suffice for adding users, but if i'm wrong it might be all pain and no gain
|
||||
1) very thorough == very complex
|
||||
2) there are some books which would still be difficult to taxonimize
|
||||
- joy of cooking. has become more of a brand
|
||||
- the bible. has different versions that aren't just editions
|
||||
- dictionary. authored by a publisher
|
||||
3) "books" vs "editions" is a confusing problem waiting to happen
|
||||
|
||||
[AIPK=auto increm PK]
|
||||
|
||||
(libation) users [AIPK id, name, join date]
|
||||
audible users [AIPK id, AUDIBLE-PK username]
|
||||
libation audible users [PK user id, PK audible user id -- cluster PK across all FKs]
|
||||
- potential danger in multi-user environment. wouldn't want one libation user getting access to a different libation user's audible info
|
||||
contributors [AIPK id, name]. prev people. incl publishers
|
||||
audible authors [PK/FK contributor id, AUDIBLE-PK author id]
|
||||
roles [AIPK id, name]. seeded: author, narrator, publisher. could expand (eg: translator, editor) without each needing a new table
|
||||
books [AIPK id, title, desc]
|
||||
book contributors [FK book id, FK contributor id, FK role id, order -- cluster PK across all FKs]
|
||||
- likely only authors
|
||||
editions [AIPK id, FK book id, title]. could expand to include year, is first edition, is abridged
|
||||
- reasons for optional different title: "Ender's Game: Special 20th Anniversary Edition", "Harry Potter and the Sorcerer's Stone" vs "Harry Potter and the Philosopher's Stone" vs "Harry Potter y la piedra filosofal", "Midnight Riot" vs "Rivers of London"
|
||||
edition contributors [FK edition id, FK contributor id, FK role id, order -- cluster PK across all FKs]
|
||||
- likely everything except authors. eg narrators, publisher
|
||||
audiobooks [PK/FK edition id, lengthInMinutes]
|
||||
- could expand to other formats by adding other similar tables. eg: print with #pages and isbn, ebook with mb
|
||||
audible origins [AIPK id, name]. seeded: library. detail. json. series
|
||||
audible books [PK/FK edition id, AUDIBLE-PK product id, picture id, sku, 3 ratings, audible category id, audible origin id]
|
||||
- could expand to other vendors by adding other similar tables
|
||||
audible user ratings [PK/FK edition id, audible user id, 3 ratings]
|
||||
audible supplements [AIPK id, FK edition id, download url]
|
||||
- pdfs only. although book download info could be the same format, they're substantially different and subject to change
|
||||
audible book downloads [PK/FK edition id, audible user id, bookdownloadlink]
|
||||
pictures [AIPK id, FK edition id, filename (xyz.jpg -- not incl path)]
|
||||
audible categories [AIPK id, AUDIBLE-PK category id, name, parent]. may only nest 1 deep
|
||||
(libation) library [FK libation user id, FK edition id, date added -- cluster PK across all FKs]
|
||||
(libation) user defined [FK libation user id, FK edition id, tagsRaw (, notes...) -- cluster PK across all FKs]
|
||||
- there's no reason to restrict tags to library items, so don't combine/link this table with library
|
||||
series [AIPK id, name]
|
||||
audible series [FK series id, AUDIBLE-PK series id/asin, audible origin id]
|
||||
- could also include a 'name' field for what audible calls this series
|
||||
series books [FK series id, FK book id (NOT edition id), index -- cluster PK across all FKs]
|
||||
- "index" not "order". display this number; don't just put in this sequence
|
||||
- index is float instead of int to allow for in-between books. eg 2.5
|
||||
- if only using "editions" (ie: getting rid of the "books" table), to show 2 editions as the same book in a series, give them the same index
|
||||
(libation) user shelves [AIPK id, FK libation user id, name, desc]
|
||||
- custom shelf. similar to library but very different in philosophy. likely different in evolving details
|
||||
(libation) shelf books [AIPK id, FK user shelf id, date added, order]
|
||||
- technically, it's no violation to list a book more than once so use AIPK
|
||||
BIN
chromedriver.exe
BIN
images/FilterOptionsButton.png
Normal file
|
After Width: | Height: | Size: 336 B |
BIN
images/FiltersDefault.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
images/Import1.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
images/Import2.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
images/Import3.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
images/LiberateBook1.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
images/LiberateBook2.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
images/LiberateBook3.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
images/LiberateBook4.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
images/LiberateBook5.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
images/PdfDownload1.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
images/PdfDownload2.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |