Compare commits

..

43 Commits

Author SHA1 Message Date
Robert McRackan
5474446f62 Minor stablizing changes before the switch to sqlite 2019-11-15 15:58:21 -05:00
Robert McRackan
d53a617bc8 Download logic in DownloadPdf should look more like DownloadBook. Extract common d/l pattern to base class 2019-11-15 12:50:00 -05:00
Robert McRackan
9076fae6f6 - add retry logic to library get
- UI bug fix when no library yet
- publishing related xml added to data and UI projects
- 'how to publish' notes
2019-11-14 14:17:20 -05:00
Robert McRackan
5d4a97cdc4 Download PDF included with backup book. Update README 2019-11-13 11:24:38 -05:00
Robert McRackan
bbe745f487 'download book' now includes pdf 2019-11-13 11:20:37 -05:00
Robert McRackan
47360c036d Pre-beta: picture storage should be more responsive if on disk 2019-11-13 11:11:00 -05:00
Robert McRackan
e69df2abbc Pre-beta: BackupBook now includes downloading pdf. This replaces the need for throttling pdf downloads 2019-11-13 09:49:23 -05:00
Robert McRackan
88d49acdad Pre-beta: image download throttling 2019-11-13 08:37:57 -05:00
Robert McRackan
01a914c390 streamline indexing ui workflow 2019-11-12 12:54:54 -05:00
Robert McRackan
0b42b8ee49 Re-index if search engine files get deleted 2019-11-11 16:16:17 -05:00
Robert McRackan
c598576683 - Change name LibationWinForm.exe => Libation.exe
- lots of pre-beta bug fixes
2019-11-11 11:03:38 -05:00
Robert McRackan
b126eed028 update readme search sample images 2019-11-07 14:41:25 -05:00
rmcrackan
3020a116cf Update README.md
add Filters to readme
2019-11-07 13:43:17 -05:00
Robert McRackan
88b9ea2f2d Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-07 13:43:02 -05:00
Robert McRackan
159c04c4b1 add readme image 2019-11-07 13:42:53 -05:00
rmcrackan
fad0f021ed Update README.md
format readme searches
2019-11-07 13:27:22 -05:00
rmcrackan
52f21dcab1 Update README.md 2019-11-07 13:26:01 -05:00
Robert McRackan
a6b89ca4c5 Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-07 13:25:20 -05:00
Robert McRackan
650c00cf66 add readme images 2019-11-07 13:24:48 -05:00
rmcrackan
089edf934e Update README.md 2019-11-07 11:25:06 -05:00
rmcrackan
efe2b19e24 Update README.md
add Search to readme
2019-11-07 11:22:50 -05:00
Robert McRackan
c41dc9a6db add readme img.s 2019-11-07 11:15:42 -05:00
Robert McRackan
707cb78dbc Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-07 11:06:48 -05:00
Robert McRackan
fc0d97d8e7 add readme img 2019-11-07 11:06:44 -05:00
rmcrackan
1494a15a6e Update README.md
readme formatting
2019-11-07 09:12:39 -05:00
rmcrackan
ac0de2a05e Update README.md
manually add table of contents
2019-11-07 09:10:50 -05:00
rmcrackan
3cc80b6a24 Update README.md
add tags to readme
2019-11-06 22:43:50 -05:00
Robert McRackan
38b04be6ba Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-06 22:38:27 -05:00
Robert McRackan
0c52d443b2 more readme images 2019-11-06 22:37:54 -05:00
rmcrackan
aa0ebac50e Update README.md
Added book and pdf download instructions
2019-11-06 22:03:40 -05:00
Robert McRackan
debebf6ee0 update readme images 2019-11-06 22:02:54 -05:00
Robert McRackan
9034288e7c updated screenshots 2019-11-06 16:58:35 -05:00
Robert McRackan
19ee02ced4 Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-06 16:53:43 -05:00
Robert McRackan
33723d7412 new screenshots 2019-11-06 16:53:39 -05:00
rmcrackan
a01a67e34a Update README.md
Add import instructions to readme
2019-11-06 15:16:34 -05:00
Robert McRackan
ecdb510513 add files for github readme 2019-11-06 15:10:45 -05:00
Robert McRackan
0b08bb3c4a Display settings wizard on first run 2019-11-06 13:30:23 -05:00
Robert McRackan
22e5dbf83d blank grid if no products 2019-11-06 09:01:57 -05:00
Robert McRackan
3b33648267 Merge branch 'master' of https://github.com/rmcrackan/Libation 2019-11-06 08:40:37 -05:00
Robert McRackan
8709518cd7 hide lucene debug search string 2019-11-06 08:40:32 -05:00
rmcrackan
3da1dff4d8 Update README 2019-11-05 22:15:20 -05:00
Robert McRackan
6aa544b322 Minor changes 2019-11-05 21:48:02 -05:00
Robert McRackan
bd993b4e4d Removed "legacy inAudible wire-up code" 2019-11-05 13:47:56 -05:00
85 changed files with 1197 additions and 1601 deletions

View File

@@ -0,0 +1,40 @@
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 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)
SearchEngineCommands.UpdateBookTags(book);
return qtyChanges;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Threading.Tasks;
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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -32,6 +32,7 @@
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>

View 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
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -10,8 +10,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20191105183104_NoScraping")]
partial class NoScraping
[Migration("20191115193402_Fresh")]
partial class Fresh
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{

View File

@@ -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)
{
@@ -67,7 +67,6 @@ namespace DataLayer.Migrations
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 =>
{

View File

@@ -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,56 @@ 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)
// 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);
// 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 processAsync(libraryBook, AudibleFileStorage.AAX, DownloadBook);
if (statusHandler.HasErrors)
return statusHandler;
}
return await Decrypt.ProcessAsync(libraryBook);
}
finally
{
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.Audio, DecryptBook);
if (statusHandler.HasErrors)
return statusHandler;
}
{
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.PDF, DownloadPdf);
if (statusHandler.HasErrors)
return statusHandler;
}
return new StatusHandler();
}
finally
{
Completed?.Invoke(this, displayMessage);
}
}
}
private static async Task<StatusHandler> processAsync(LibraryBook libraryBook, AudibleFileStorage afs, IProcessable processable)
=> !await afs.ExistsAsync(libraryBook.Book.AudibleProductId)
? await processable.ProcessAsync(libraryBook)
: new StatusHandler();
}
}

View File

@@ -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
@@ -41,9 +41,9 @@ namespace FileLiberator
=> await AudibleFileStorage.AAX.ExistsAsync(productId)
&& !await AudibleFileStorage.Audio.ExistsAsync(productId);
// 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}";
@@ -60,11 +60,8 @@ namespace FileLiberator
if (await AudibleFileStorage.Audio.ExistsAsync(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)
@@ -164,148 +161,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
}
}

View File

@@ -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
@@ -21,55 +21,46 @@ namespace FileLiberator
=> !await AudibleFileStorage.Audio.ExistsAsync(libraryBook.Book.AudibleProductId)
&& !await AudibleFileStorage.AAX.ExistsAsync(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 await verifyDownloadAsync(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 async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.AAX.ExistsAsync(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded AAX file cannot be found" }
: new StatusHandler();
}
}

View File

@@ -1,105 +1,57 @@
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()
{
// https://stackoverflow.com/a/15483698
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
}
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[]
{
"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 (string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)))
return false;
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 !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
}
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var proposedDownloadFilePath = await getProposedDownloadFilePathAsync(libraryBook);
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
return await verifyDownloadAsync(libraryBook);
}
private static async Task<string> getProposedDownloadFilePathAsync(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var destinationDir =
// this is safe b/c GetDirectoryName(null) == null
Path.GetDirectoryName(await AudibleFileStorage.Audio.GetAsync(libraryBook.Book.AudibleProductId))
?? AudibleFileStorage.PDF.StorageDirectory;
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 async Task<StatusHandler> verifyDownloadAsync(LibraryBook libraryBook)
=> !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}
}

View File

@@ -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;
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 event EventHandler<string> StatusUpdate;
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
public abstract Task<bool> ValidateAsync(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);
}
}
}
}

View File

@@ -22,7 +22,8 @@ namespace FileLiberator
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");

View File

@@ -29,8 +29,10 @@ namespace FileManager
[".aac"] = FileType.Audio,
[".mp4"] = FileType.Audio,
[".m4a"] = FileType.Audio,
[".ogg"] = FileType.Audio,
[".flac"] = FileType.Audio,
[".aax"] = FileType.AAX,
[".aax"] = FileType.AAX,
[".pdf"] = FileType.PDF,
[".zip"] = FileType.PDF,

View File

@@ -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)];

View File

@@ -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());
}
}

View File

@@ -2,77 +2,108 @@
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);
}
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));
}
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<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];
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);
}
}
// 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(path300))
{
var bytes = webClient.DownloadData(
"https://images-na.ssl-images-amazon.com/images/I/" + pictureId + "._SL300_.jpg");
File.WriteAllBytes(path300, bytes);
}
}
try
{
isProcessing = true;
{
if (!FileUtility.FileExists(path500))
{
var bytes = webClient.DownloadData(
"https://m.media-amazon.com/images/I/" + pictureId + "._SL500_.jpg");
File.WriteAllBytes(path500, bytes);
}
}
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();
break;
}
catch { retry++; }
}
while (retry < 3);
}
}
// 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;
}
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);
}
}
}

View File

@@ -4,6 +4,10 @@
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="7.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />

View File

@@ -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

View File

@@ -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;

View File

@@ -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}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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}"))

View File

@@ -23,8 +23,8 @@ 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);
}
@@ -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();
}

View File

@@ -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);

View File

@@ -1,8 +0,0 @@
namespace LibationWinForm
{
public interface IIndexLibraryDialog : IRunnableDialog
{
int TotalBooksProcessed { get; }
int NewBooksAdded { get; }
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);

View File

@@ -1,41 +1,24 @@
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)
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
this.Close();
}
}
}

View File

@@ -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";

View File

@@ -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();
}
}
}

View File

@@ -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
//

View File

@@ -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;
{
// 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();
@@ -54,18 +63,47 @@ namespace LibationWinForm
}
}
#region bottom: qty books visible
public void SetVisibleCount(int qty, string str = null)
#region reload grid
bool isProcessingGridSelect = false;
private void reloadGrid()
{
visibleCountLbl.Text = string.Format(visibleCountLbl_Format, qty);
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
setGrid();
isProcessingGridSelect = prev_isProcessingGridSelect;
if (!string.IsNullOrWhiteSpace(str))
visibleCountLbl.Text += " | " + str;
// UI init complete. now we can apply filter
doFilter(lastGoodFilter);
}
#endregion
#region bottom: backup counts
private async Task setBackupCountsAsync()
ProductsGrid currProductsGrid;
private void setGrid()
{
SuspendLayout();
{
if (currProductsGrid != null)
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.Dispose();
}
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 async Task setBackupCountsAsync()
{
var books = LibraryQueries.GetLibrary_Flat_NoTracking()
.Select(sp => sp.Book)
@@ -115,7 +153,7 @@ namespace LibationWinForm
// update bottom numbers
var pending = noProgress + downloadedOnly;
var text
= !results.Any() ? "No books. Begin by indexing your library"
= !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);
@@ -172,41 +210,8 @@ namespace LibationWinForm
}
#endregion
#region reload grid
bool isProcessingGridSelect = false;
private void reloadGrid()
{
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
setGrid();
isProcessingGridSelect = prev_isProcessingGridSelect;
// UI init complete. now we can apply filter
doFilter(lastGoodFilter);
}
ProductsGrid currProductsGrid;
private void setGrid()
{
SuspendLayout();
{
if (currProductsGrid != null)
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.Dispose();
}
currProductsGrid = new ProductsGrid(this) { Dock = DockStyle.Fill };
gridPanel.Controls.Add(currProductsGrid);
currProductsGrid.Display();
}
ResumeLayout();
}
#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,8 +252,7 @@ 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
@@ -256,29 +260,33 @@ namespace LibationWinForm
#region index menu
private async 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)
await setBackupCountsAsync();
if (totalProcessed > 0)
reloadGrid();
}
#endregion
}
#endregion
#region liberate menu
private async void setBackupCountsAsync(object _, string __) => await setBackupCountsAsync();
#region liberate menu
private async void setBackupCountsAsync(object _, string __) => await setBackupCountsAsync();
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 += setBackupCountsAsync;
backupBook.DecryptBook.Completed += setBackupCountsAsync;
backupBook.DownloadPdf.Completed += setBackupCountsAsync;
await BookLiberation.ProcessorAutomationController.RunAutomaticBackup(backupBook);
}
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)

View File

@@ -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
{

View File

@@ -2,9 +2,10 @@
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;
namespace LibationWinForm
{
@@ -22,13 +23,11 @@ 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;
// 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();
private bool hasBeenDisplayed = false;
@@ -54,7 +53,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" };
@@ -92,6 +91,15 @@ namespace LibationWinForm
// transform into sorted GridEntry.s BEFORE binding
//
var lib = LibraryQueries.GetLibrary_Flat_NoTracking();
// 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 +115,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)
@@ -201,14 +183,14 @@ namespace LibationWinForm
// needed to update text colors
dataGridView.InvalidateRow(e.RowIndex);
Filter();
filter();
}
private static int saveChangedTags(Book book, string newTags)
{
book.UserDefinedItem.Tags = newTags;
var qtyChanges = ApplicationServices.TagUpdater.IndexChangedTags(book);
var qtyChanges = LibraryCommands.IndexChangedTags(book);
return qtyChanges;
}
@@ -249,12 +231,15 @@ namespace LibationWinForm
#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,10 +250,9 @@ namespace LibationWinForm
dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).GetBook().AudibleProductId);
}
currencyManager.ResumeBinding();
VisibleCountChanged?.Invoke(this, dataGridView.Rows.Cast<DataGridViewRow>().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

View File

@@ -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
View File

@@ -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:
![Import step 1](images/Import1.png)
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)
Success! We see how many new titles are imported:
![Import step 3](images/Import3.png)
### 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:
![Liberate book step 1](images/LiberateBook1.png)
Select Liberate > Begin Book Backups
![Liberate book step 2](images/LiberateBook2.png)
First the original book with DRM is downloaded
![Liberate book step 3](images/LiberateBook3.png)
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.
![Liberate book step 4](images/LiberateBook4.png)
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
![Liberate book step 5](images/LiberateBook5.png)
### 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.
![PDF download step 1](images/PdfDownload1.png)
Select Liberate > Begin PDF Backups
![PDF download step 2](images/PdfDownload2.png)
The downloads work just like with books, only with no additional decryption needed.
![PDF download step 3](images/PdfDownload3.png)
Ta da!
![PDF download step 4](images/PdfDownload4.png)
### Details of downloaded files
![Post download](images/PostDownload.png)
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
![Tags step 1](images/Tags1.png)
Add as many tags as you'd like. Tags are separated by a space. Each tag can contain letters, numbers, and underscores
![Tags step 2](images/Tags2.png)
Tags are saved non-case specific for easy search. There is one special tag "hidden" which will also grey-out the book
![Tags step 3](images/Tags3.png)
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 ![Filter options](images/FilterOptions.png)
* 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
![Search example: potter](images/SearchExamplePotter.png)
If you only want to see Harry Potter
![Search example: "harry potter"](images/SearchExampleHarryPotter.png)
If you only want to see potter except for Harry Potter
![Search example: "potter NOT harry"](images/SearchExamplePotterNotHarry.png)
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.)
![Search example: author:gaiman AND authornarrated](images/SearchExampleGaimanAuthorNarrated.png)
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.
![default filters](images/FiltersDefault.png)

View File

@@ -1,8 +1,33 @@
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
https://github.com/rmcrackan/Libation/releases
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
-- end VERSIONING ---------------------------------------------------------------------------------------------------------------------
-- begin HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
OPTION 1: UI
rt-clk 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 ---------------------------------------------------------------------------------------------------------------------

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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>

View 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];
}
}
}

View 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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -1,13 +1,51 @@
-- begin REPLACE SCRAPING WITH API ---------------------------------------------------------------------------------------------------------------------
remove "legacy inAudible wire-up code"
-- end REPLACE SCRAPING WITH API ---------------------------------------------------------------------------------------------------------------------
-- begin BETA ---------------------------------------------------------------------------------------------------------------------
TESTING BUG
dbl clk. long pause. exception:
System.ComponentModel.Win32Exception (2): The system cannot find the file specified.
"continue" button allows me to keep using
bottom #s do not update
login succeeded. IdentityTokens.json successfully created
received above error again during scan. continue
stuck on scan. force quit from task manager
only files:
Images -- empty dir
IdentityTokens.json -- populated
no mdf, ldf
REPLACE DB
need completely need db? replace LocalDb with sqlite? embedded document nosql?
CREATE INSTALLER
see REFERENCE.txt > HOW TO PUBLISH
RELEASE TO BETA
Warn of known performance issues
- Library import
- Tag add/edit
- Grid is slow to respond loading when books aren't liberated
- get decrypt key -- unavoidable
-- end BETA ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
scan library in background?
can include a notice somewhere that a scan is in-process
why block the UI at all?
what to do if new books? don't want to refresh grid when user isn't expecting it
-- end ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
-- begin BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
reproduce: try to do the api download with a bad codec
result: DownloadsFinal dir .aax file 1 kb
this resulted from an exception. we should not be keeping a file after exception
if error: show error. DownloadBook delete bad file
-- end BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
-- 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
when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow. use something similar to PictureStorage's timer to run on a separate thread
https://stackoverflow.com/a/12046333
https://codereview.stackexchange.com/a/135074
// do NOT use lock() or Monitor with async/await
@@ -27,7 +65,8 @@ use pdf icon with and without and X over it to indicate status
-- begin ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
Audible API. GET /1.0/library , GET /1.0/library/{asin}
TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryApiV10
TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryDtoV10
same for GetLibraryBookAsync > ... > BookDtoV10
-- end ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
@@ -46,14 +85,14 @@ 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
at FileManager.FilePathCache.GetPath(String id, FileType type) in \Libation\FileManager\UNTESTED\FilePathCache.cs:line 33
at FileManager.AudibleFileStorage.<getAsync>d__32.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 112
at FileManager.AudibleFileStorage.<GetAsync>d__31.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 107
at FileManager.AudibleFileStorage.<ExistsAsync>d__30.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 104
at LibationWinForm.Form1.<<setBookBackupCountsAsync>g__getAudioFileStateAsync|15_1>d.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 110
at LibationWinForm.Form1.<setBookBackupCountsAsync>d__15.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 117
at LibationWinForm.Form1.<setBackupCountsAsync>d__13.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 81
at LibationWinForm.Form1.<Form1_Load>d__11.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 60
-- end BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
-- begin CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
@@ -62,12 +101,16 @@ replace complex config saving throughout with new way in my ConsoleDependencyInj
all settings should be strongly typed
re-create my shortcuts and bak
for appsettings.json to get copied in the single-file release, project must incl <ExcludeFromSingleFile>true
multiple files named "appsettings.json" will overwrite each other
libraries should avoid this generic name. ok for applications to use them
libraries should avoid this generic name. in general: ok for applications to use them
there are exceptions: datalayer has appsettings which is copied to winform. if winform uses appsettings also, it will override datalayer's
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
\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\L1
\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
@@ -84,6 +127,12 @@ Why are tags in file AND database?
extract FileManager dependency from data layer
-- end TAGS ---------------------------------------------------------------------------------------------------------------------
-- 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, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
add support for multiple categories
when i do this, learn about the different CategoryLadder.Root enums. probably only need Root.Genres
@@ -140,20 +189,9 @@ directly call ffmpeg (decrypt only)
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
previously difficult due to implementation details regarding scraping and importing. should now be trivial
-- end ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------

View File

@@ -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

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

BIN
images/FiltersDefault.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
images/Import1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
images/Import2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
images/Import3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/LiberateBook1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/LiberateBook2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
images/LiberateBook3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
images/LiberateBook4.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
images/LiberateBook5.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
images/PdfDownload1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
images/PdfDownload2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/PdfDownload3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
images/PdfDownload4.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
images/PostDownload.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
images/SearchExampleBio.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
images/Tags1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
images/Tags2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/Tags3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/edit-tags-25x25.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

BIN
images/edit-tags-50x50.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B