scraping => api transition almost complete
This commit is contained in:
parent
df889a60a4
commit
591d84e719
41 changed files with 995 additions and 2494 deletions
|
|
@ -11,7 +11,7 @@
|
|||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<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="..\AudibleApiDomainService\AudibleApiDomainService.csproj" />
|
||||
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
|
||||
<ProjectReference Include="..\ScrapingDomainServices\ScrapingDomainServices.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ namespace LibationWinForm.BookLiberation
|
|||
// close form on DOWNLOAD completed, not final Completed. Else for BackupBook this form won't close until DECRYPT is also complete
|
||||
void fileDownloadCompleted(object _, string __) => downloadDialog.Close();
|
||||
|
||||
void downloadProgressChanged(object _, System.Net.DownloadProgressChangedEventArgs arg)
|
||||
=> downloadDialog.DownloadProgressChanged(arg.BytesReceived, arg.TotalBytesToReceive);
|
||||
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress)
|
||||
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value);
|
||||
|
||||
void unsubscribe(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using LibationWinForm.Dialogs.Login;
|
|||
|
||||
namespace LibationWinForm.Login
|
||||
{
|
||||
public class WinformResponder : AudibleApiDomainService.IAudibleApiResponder
|
||||
public class WinformResponder : AudibleApi.ILoginCallback
|
||||
{
|
||||
public string Get2faCode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -357,18 +357,48 @@ namespace LibationWinForm
|
|||
|
||||
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
// audible api
|
||||
var settings = new AudibleApiDomainService.Settings(config);
|
||||
var responder = new Login.WinformResponder();
|
||||
var client = await AudibleApiDomainService.AudibleApiLibationClient.CreateClientAsync(settings, responder);
|
||||
|
||||
await client.ImportLibraryAsync();
|
||||
|
||||
// scrape
|
||||
await indexDialog(new ScanLibraryDialog());
|
||||
// legacy/scraping method
|
||||
//await indexDialog(new ScanLibraryDialog());
|
||||
// new/api method
|
||||
await audibleApi();
|
||||
}
|
||||
|
||||
private async void reimportMostRecentLibraryScanToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private async Task audibleApi()
|
||||
{
|
||||
var identityFilePath = System.IO.Path.Combine(config.LibationFiles, "IdentityTokens.json");
|
||||
var callback = new Login.WinformResponder();
|
||||
var api = await AudibleApi.EzApiCreator.GetApiAsync(identityFilePath, callback, config.LocaleCountryCode);
|
||||
|
||||
int totalCount;
|
||||
int newCount;
|
||||
|
||||
// seems to be very common the 1st time after long absence. either figure out why, or run 2x before declaring error
|
||||
try
|
||||
{
|
||||
var items = await InternalUtilities.AudibleApiExtensions.GetAllLibraryItemsAsync(api);
|
||||
totalCount = items.Count;
|
||||
newCount = await Task.Run(() => new DtoImporterService.LibraryImporter().Import(items));
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = await InternalUtilities.AudibleApiExtensions.GetAllLibraryItemsAsync(api);
|
||||
totalCount = items.Count;
|
||||
newCount = await Task.Run(() => new DtoImporterService.LibraryImporter().Import(items));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("Error importing library.\r\n" + ex.Message, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await InternalUtilities.SearchEngineActions.FullReIndexAsync();
|
||||
await indexComplete(totalCount, newCount);
|
||||
}
|
||||
|
||||
private async void reimportMostRecentLibraryScanToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
// DO NOT ConfigureAwait(false)
|
||||
// this would result in index() => reloadGrid() => setGrid() => "gridPanel.Controls.Remove(currProductsGrid);"
|
||||
|
|
@ -377,15 +407,15 @@ await client.ImportLibraryAsync();
|
|||
|
||||
MessageBox.Show($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}");
|
||||
|
||||
await index(NewBooksAdded, TotalBooksProcessed);
|
||||
await indexComplete(TotalBooksProcessed, NewBooksAdded);
|
||||
}
|
||||
|
||||
private async Task indexDialog(IIndexLibraryDialog dialog)
|
||||
{
|
||||
if (!dialog.RunDialog().In(DialogResult.Abort, DialogResult.Cancel, DialogResult.None))
|
||||
await index(dialog.NewBooksAdded, dialog.TotalBooksProcessed);
|
||||
await indexComplete(dialog.TotalBooksProcessed, dialog.NewBooksAdded);
|
||||
}
|
||||
private async Task index(int newBooksAdded, int totalBooksProcessed)
|
||||
private async Task indexComplete(int totalBooksProcessed, int newBooksAdded)
|
||||
{
|
||||
// update backup counts if we have new library items
|
||||
if (newBooksAdded > 0)
|
||||
|
|
|
|||
|
|
@ -7,103 +7,132 @@ using DataLayer;
|
|||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
internal class GridEntry
|
||||
{
|
||||
private LibraryBook libraryBook;
|
||||
private Book book => libraryBook.Book;
|
||||
internal class GridEntry
|
||||
{
|
||||
private LibraryBook libraryBook;
|
||||
private Book book => libraryBook.Book;
|
||||
|
||||
public Book GetBook() => book;
|
||||
public Book GetBook() => book;
|
||||
|
||||
// this special case is obvious and ugly
|
||||
public void REPLACE_Library_Book(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
// this special case is obvious and ugly
|
||||
public void REPLACE_Library_Book(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
|
||||
public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
public GridEntry(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
|
||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
||||
|
||||
[Browsable(false)]
|
||||
public string Tags => book.UserDefinedItem.Tags;
|
||||
[Browsable(false)]
|
||||
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
|
||||
[Browsable(false)]
|
||||
public string Tags => book.UserDefinedItem.Tags;
|
||||
[Browsable(false)]
|
||||
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
|
||||
|
||||
private Dictionary<string, string> formatReplacements { get; } = new Dictionary<string, string>();
|
||||
public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value);
|
||||
// formatReplacements is what gets displayed
|
||||
// the value that gets returned from the property is the cell's value
|
||||
// this allows for the value to be sorted one way and displayed another
|
||||
// eg:
|
||||
// orig title: The Computer
|
||||
// formatReplacement: The Computer
|
||||
// value for sorting: Computer
|
||||
private Dictionary<string, string> formatReplacements { get; } = new Dictionary<string, string>();
|
||||
public bool TryGetFormatted(string key, out string value) => formatReplacements.TryGetValue(key, out value);
|
||||
|
||||
public Image Cover =>
|
||||
Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes(
|
||||
FileManager.PictureStorage.GetImage(book.PictureId, FileManager.PictureStorage.PictureSize._80x80)
|
||||
);
|
||||
public Image Cover =>
|
||||
Dinah.Core.Drawing.ImageConverter.GetPictureFromBytes(
|
||||
FileManager.PictureStorage.GetImage(book.PictureId, FileManager.PictureStorage.PictureSize._80x80)
|
||||
);
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Title)] = book.Title;
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Title)] = book.Title;
|
||||
|
||||
var sortName = book.Title
|
||||
.Replace("|", "")
|
||||
.Replace(":", "")
|
||||
.ToLowerInvariant();
|
||||
if (sortName.StartsWith("the ") || sortName.StartsWith("a ") || sortName.StartsWith("an "))
|
||||
sortName = sortName.Substring(sortName.IndexOf(" ") + 1);
|
||||
var sortName = book.Title
|
||||
.Replace("|", "")
|
||||
.Replace(":", "")
|
||||
.ToLowerInvariant();
|
||||
if (sortName.StartsWith("the ") || sortName.StartsWith("a ") || sortName.StartsWith("an "))
|
||||
sortName = sortName.Substring(sortName.IndexOf(" ") + 1);
|
||||
|
||||
return sortName;
|
||||
}
|
||||
}
|
||||
return sortName;
|
||||
}
|
||||
}
|
||||
|
||||
public string Authors => book.AuthorNames;
|
||||
public string Narrators => book.NarratorNames;
|
||||
public string Authors => book.AuthorNames;
|
||||
public string Narrators => book.NarratorNames;
|
||||
|
||||
public int Length
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Length)]
|
||||
= book.LengthInMinutes == 0
|
||||
? "[pre-release]"
|
||||
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
|
||||
public int Length
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Length)]
|
||||
= book.LengthInMinutes == 0
|
||||
? ""
|
||||
: $"{book.LengthInMinutes / 60} hr {book.LengthInMinutes % 60} min";
|
||||
|
||||
return book.LengthInMinutes;
|
||||
}
|
||||
}
|
||||
return book.LengthInMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
public string Series => book.SeriesNames;
|
||||
public string Series => book.SeriesNames;
|
||||
|
||||
public string Description
|
||||
=> book.Description == null ? ""
|
||||
: book.Description.Length < 63 ? book.Description
|
||||
: book.Description.Substring(0, 60) + "...";
|
||||
private string descriptionCache = null;
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
// HtmlAgilityPack is expensive. cache results
|
||||
if (descriptionCache is null)
|
||||
{
|
||||
if (book.Description is null)
|
||||
descriptionCache = "";
|
||||
else
|
||||
{
|
||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||
doc.LoadHtml(book.Description);
|
||||
var noHtml = doc.DocumentNode.InnerText;
|
||||
descriptionCache
|
||||
= noHtml.Length < 63
|
||||
? noHtml
|
||||
: noHtml.Substring(0, 60) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
public string Category => string.Join(" > ", book.CategoriesNames);
|
||||
return descriptionCache;
|
||||
}
|
||||
}
|
||||
|
||||
// star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly:
|
||||
// - star
|
||||
// - star star
|
||||
// - star 1/2
|
||||
public string Category => string.Join(" > ", book.CategoriesNames);
|
||||
|
||||
public string Product_Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
Rating rating = book.Rating;
|
||||
// star ratings retain numeric value but display star text. this is needed because just using star text doesn't sort correctly:
|
||||
// - star
|
||||
// - star star
|
||||
// - star 1/2
|
||||
|
||||
formatReplacements[nameof(Product_Rating)] = starString(rating);
|
||||
public string Product_Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Product_Rating)] = starString(book.Rating);
|
||||
return firstScore(book.Rating);
|
||||
}
|
||||
}
|
||||
|
||||
return firstScore(rating);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? Purchase_Date => libraryBook.DateAdded;
|
||||
public string Purchase_Date
|
||||
{
|
||||
get
|
||||
{
|
||||
formatReplacements[nameof(Purchase_Date)] = libraryBook.DateAdded.ToString("d");
|
||||
return libraryBook.DateAdded.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
||||
public string My_Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
Rating rating = book.UserDefinedItem.Rating;
|
||||
|
||||
formatReplacements[nameof(My_Rating)] = starString(rating);
|
||||
|
||||
return firstScore(rating);
|
||||
formatReplacements[nameof(My_Rating)] = starString(book.UserDefinedItem.Rating);
|
||||
return firstScore(book.UserDefinedItem.Rating);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Windows.Forms;
|
||||
using Dinah.Core.DataBinding;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace LibationWinForm
|
||||
{
|
||||
|
|
@ -76,11 +77,14 @@ namespace LibationWinForm
|
|||
|
||||
col.HeaderText = col.HeaderText.Replace("_", " ");
|
||||
|
||||
if (col.Name == nameof(GridEntry.Title))
|
||||
col.Width *= 2;
|
||||
|
||||
if (col.Name == nameof(GridEntry.Misc))
|
||||
col.Width = (int)(col.Width * 1.35);
|
||||
col.Width = col.Name switch
|
||||
{
|
||||
nameof(GridEntry.Cover) => 80,
|
||||
nameof(GridEntry.Title) => col.Width * 2,
|
||||
nameof(GridEntry.Misc) => (int)(col.Width * 1.35),
|
||||
var n when n.In(nameof(GridEntry.My_Rating), nameof(GridEntry.Product_Rating)) => col.Width + 8,
|
||||
_ => col.Width
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -88,12 +92,14 @@ namespace LibationWinForm
|
|||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
var lib = LibraryQueries.GetLibrary_Flat_NoTracking();
|
||||
var orderedGridEntries = lib
|
||||
var orderedGridEntries = lib
|
||||
.Select(lb => new GridEntry(lb)).ToList()
|
||||
// default load order: sort by author, then series, then title
|
||||
.OrderBy(ge => ge.Authors)
|
||||
.ThenBy(ge => ge.Series)
|
||||
.ThenBy(ge => ge.Title)
|
||||
// default load order
|
||||
.OrderByDescending(ge => ge.Purchase_Date)
|
||||
//// more advanced example: sort by author, then series, then title
|
||||
//.OrderBy(ge => ge.Authors)
|
||||
// .ThenBy(ge => ge.Series)
|
||||
// .ThenBy(ge => ge.Title)
|
||||
.ToList();
|
||||
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue