Merge pull request #531 from Mbucari/master
Bug fixes and performance improvements
This commit is contained in:
commit
eb61ba3d69
@ -213,12 +213,13 @@ namespace ApplicationServices
|
|||||||
if (archiver is not null)
|
if (archiver is not null)
|
||||||
{
|
{
|
||||||
var fileName = $"{DateTime.Now:u} {account.MaskedLogEntry}.json";
|
var fileName = $"{DateTime.Now:u} {account.MaskedLogEntry}.json";
|
||||||
|
var items = await Task.Run(() => JArray.FromObject(dtoItems.Select(i => i.SourceJson)));
|
||||||
|
|
||||||
var scanFile = new JObject
|
var scanFile = new JObject
|
||||||
{
|
{
|
||||||
{ "Account", account.MaskedLogEntry },
|
{ "Account", account.MaskedLogEntry },
|
||||||
{ "ScannedDateTime", DateTime.Now.ToString("u") },
|
{ "ScannedDateTime", DateTime.Now.ToString("u") },
|
||||||
{ "Items", await Task.Run(() => JArray.FromObject(dtoItems)) }
|
{ "Items", items}
|
||||||
};
|
};
|
||||||
|
|
||||||
await archiver.AddFileAsync(fileName, scanFile);
|
await archiver.AddFileAsync(fileName, scanFile);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -8,10 +7,9 @@ using System.Diagnostics;
|
|||||||
using AudibleApi;
|
using AudibleApi;
|
||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Retry;
|
using Polly.Retry;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace AudibleUtilities
|
namespace AudibleUtilities
|
||||||
{
|
{
|
||||||
@ -91,49 +89,73 @@ namespace AudibleUtilities
|
|||||||
{
|
{
|
||||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
List<Item> items = new();
|
List<Item> items = new();
|
||||||
List<Item> seriesItems = new();
|
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
var totalTime = TimeSpan.Zero;
|
||||||
|
using var semaphore = new SemaphoreSlim(MaxConcurrency);
|
||||||
|
|
||||||
//Scan the library for all added books, and add any episode-type items to seriesItems to be scanned for episodes/parents
|
var episodeChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
|
||||||
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions, BatchSize, MaxConcurrency))
|
var batchReaderTask = readAllAsinsAsync(episodeChannel.Reader, semaphore);
|
||||||
|
|
||||||
|
//Scan the library for all added books.
|
||||||
|
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
|
||||||
|
await foreach (var item in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||||
{
|
{
|
||||||
if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes)
|
if (importEpisodes)
|
||||||
seriesItems.Add(item);
|
{
|
||||||
else if (!item.IsEpisodes && !item.IsSeriesParent)
|
var episodes = item.Where(i => i.IsEpisodes).ToList();
|
||||||
items.Add(item);
|
var series = item.Where(i => i.IsSeriesParent).ToList();
|
||||||
|
|
||||||
count++;
|
var parentAsins = episodes
|
||||||
|
.SelectMany(i => i.Relationships)
|
||||||
|
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||||
|
.Select(r => r.Asin);
|
||||||
|
|
||||||
|
var episodeAsins = series
|
||||||
|
.SelectMany(i => i.Relationships)
|
||||||
|
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||||
|
.Select(r => r.Asin);
|
||||||
|
|
||||||
|
foreach (var asin in parentAsins.Concat(episodeAsins))
|
||||||
|
episodeChannel.Writer.TryWrite(asin);
|
||||||
|
|
||||||
|
items.AddRange(episodes);
|
||||||
|
items.AddRange(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on series episode scans to complete.", count);
|
items.AddRange(item.Where(i => !i.IsSeriesParent && !i.IsEpisodes));
|
||||||
Serilog.Log.Logger.Debug("Beginning episode scan.");
|
|
||||||
|
|
||||||
count = 0;
|
|
||||||
|
|
||||||
//'get' Tasks are activated when they are written to the channel. To avoid more concurrency than is desired, the
|
|
||||||
//channel is bounded with a capacity of 1. Channel write operations are blocked until the current item is read
|
|
||||||
var episodeChannel = Channel.CreateBounded<Task<List<Item>>>(new BoundedChannelOptions(1) { SingleReader = true });
|
|
||||||
|
|
||||||
//Start scanning for all episodes. Episode batch 'get' Tasks are written to the channel.
|
|
||||||
var scanAllSeriesTask = scanAllSeries(seriesItems, episodeChannel.Writer);
|
|
||||||
|
|
||||||
//Read all episodes from the channel and add them to the import items.
|
|
||||||
//This method blocks until episodeChannel.Writer is closed by scanAllSeries()
|
|
||||||
await foreach (var ep in getAllEpisodesAsync(episodeChannel.Reader))
|
|
||||||
{
|
|
||||||
items.AddRange(ep);
|
|
||||||
count += ep.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Be sure to await the scanAllSeries Task so that any exceptions are thrown
|
|
||||||
await scanAllSeriesTask;
|
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
Serilog.Log.Logger.Debug("Episode scan complete. Found {count} episodes and series.", count);
|
totalTime += sw.Elapsed;
|
||||||
Serilog.Log.Logger.Debug($"Completed library scan in {sw.Elapsed.TotalMilliseconds:F0} ms.");
|
Serilog.Log.Logger.Debug("Library scan complete after {elappsed_ms} ms. Found {count} books and series. Waiting on series episode scans to complete.", sw.ElapsedMilliseconds, items.Count);
|
||||||
|
sw.Restart();
|
||||||
|
|
||||||
|
//Signal that we're done adding asins
|
||||||
|
episodeChannel.Writer.Complete();
|
||||||
|
|
||||||
|
//Wait for all episodes/parents to be retrived
|
||||||
|
var allEps = await batchReaderTask;
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
totalTime += sw.Elapsed;
|
||||||
|
Serilog.Log.Logger.Debug("Episode scan complete after {elappsed_ms} ms. Found {count} episodes and series .", sw.ElapsedMilliseconds, allEps.Count);
|
||||||
|
sw.Restart();
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Debug("Begin indexing series episodes");
|
||||||
|
items.AddRange(allEps);
|
||||||
|
|
||||||
|
//Set the Item.Series info for episodes and parents.
|
||||||
|
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
||||||
|
{
|
||||||
|
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
||||||
|
setSeries(parent, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
totalTime += sw.Elapsed;
|
||||||
|
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
|
||||||
|
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
|
||||||
|
|
||||||
var validators = new List<IValidator>();
|
var validators = new List<IValidator>();
|
||||||
validators.AddRange(getValidators());
|
validators.AddRange(getValidators());
|
||||||
@ -159,146 +181,55 @@ namespace AudibleUtilities
|
|||||||
#region episodes and podcasts
|
#region episodes and podcasts
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read get tasks from the <paramref name="channel"/> and await results. This method maintains
|
/// Read asins from the channel and request catalog item info in batches of <see cref="BatchSize"/>. Blocks until <paramref name="channelReader"/> is closed.
|
||||||
/// a list of up to <see cref="MaxConcurrency"/> get tasks. When any of the get tasks completes,
|
|
||||||
/// the Items are yielded, that task is removed from the list, and a new get task is read from
|
|
||||||
/// the channel.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async IAsyncEnumerable<List<Item>> getAllEpisodesAsync(ChannelReader<Task<List<Item>>> channel)
|
/// <param name="channelReader">Input asins to batch</param>
|
||||||
|
/// <param name="semaphore">Shared semaphore to limit concurrency</param>
|
||||||
|
/// <returns>All <see cref="Item"/>s of asins written to the channel.</returns>
|
||||||
|
private async Task<List<Item>> readAllAsinsAsync(ChannelReader<string> channelReader, SemaphoreSlim semaphore)
|
||||||
{
|
{
|
||||||
List<Task<List<Item>>> concurentGets = new();
|
int batchNum = 1;
|
||||||
|
List<Task<List<Item>>> getTasks = new();
|
||||||
|
|
||||||
for (int i = 0; i < MaxConcurrency && await channel.WaitToReadAsync(); i++)
|
while (await channelReader.WaitToReadAsync())
|
||||||
concurentGets.Add(await channel.ReadAsync());
|
|
||||||
|
|
||||||
while (concurentGets.Count > 0)
|
|
||||||
{
|
{
|
||||||
var completed = await Task.WhenAny(concurentGets);
|
List<string> asins = new();
|
||||||
concurentGets.Remove(completed);
|
|
||||||
|
|
||||||
if (await channel.WaitToReadAsync())
|
while (asins.Count < BatchSize && await channelReader.WaitToReadAsync())
|
||||||
concurentGets.Add(await channel.ReadAsync());
|
{
|
||||||
|
var asin = await channelReader.ReadAsync();
|
||||||
|
|
||||||
yield return completed.Result;
|
if (!asins.Contains(asin))
|
||||||
|
asins.Add(asin);
|
||||||
}
|
}
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
getTasks.Add(getProductsAsync(batchNum++, asins, semaphore));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var completed = await Task.WhenAll(getTasks);
|
||||||
/// Gets all child episodes and episode parents belonging to <paramref name="seriesItems"/> in batches and
|
//We only want Series parents and Series episodes. Explude other relationship types (e.g. 'season')
|
||||||
/// writes the get tasks to <paramref name="channel"/>.
|
return completed.SelectMany(l => l).Where(i => i.IsSeriesParent || i.IsEpisodes).ToList();
|
||||||
/// </summary>
|
}
|
||||||
private async Task scanAllSeries(IEnumerable<Item> seriesItems, ChannelWriter<Task<List<Item>>> channel)
|
|
||||||
|
private async Task<List<Item>> getProductsAsync(int batchNum, List<string> asins, SemaphoreSlim semaphore)
|
||||||
{
|
{
|
||||||
|
Serilog.Log.Logger.Debug($"Batch {batchNum} Begin: Fetching {asins.Count} asins");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<Task> episodeScanTasks = new();
|
var sw = Stopwatch.StartNew();
|
||||||
|
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
foreach (var item in seriesItems)
|
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
|
||||||
{
|
|
||||||
if (item.IsEpisodes)
|
|
||||||
await channel.WriteAsync(getEpisodeParentAsync(item));
|
|
||||||
else if (item.IsSeriesParent)
|
|
||||||
episodeScanTasks.Add(getParentEpisodesAsync(item, channel));
|
|
||||||
}
|
|
||||||
|
|
||||||
//episodeScanTasks complete only after all episode batch 'gets' have been written to the channel
|
return items;
|
||||||
await Task.WhenAll(episodeScanTasks);
|
|
||||||
}
|
|
||||||
finally { channel.Complete(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<Item>> getEpisodeParentAsync(Item episode)
|
|
||||||
{
|
|
||||||
//Item is a single episode that was added to the library.
|
|
||||||
//Get the episode's parent and add it to the database.
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", episode);
|
|
||||||
|
|
||||||
List<Item> children = new() { episode };
|
|
||||||
|
|
||||||
var parentAsins = episode.Relationships
|
|
||||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
|
||||||
.Select(p => p.Asin);
|
|
||||||
|
|
||||||
var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
|
||||||
|
|
||||||
int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent);
|
|
||||||
if (numSeriesParents != 1)
|
|
||||||
{
|
|
||||||
//There should only ever be 1 top-level parent per episode. If not, log
|
|
||||||
//so we can figure out what to do about those special cases, and don't
|
|
||||||
//import the episode.
|
|
||||||
JsonSerializerSettings Settings = new()
|
|
||||||
{
|
|
||||||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
|
||||||
DateParseHandling = DateParseHandling.None,
|
|
||||||
Converters = {
|
|
||||||
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Serilog.Log.Logger.Error($"Found {numSeriesParents} parents for {episode.Asin}\r\nEpisode Product:\r\n{JsonConvert.SerializeObject(episode, Formatting.None, Settings)}");
|
|
||||||
return new();
|
|
||||||
}
|
|
||||||
|
|
||||||
var parent = seriesParents.Single(p => p.IsSeriesParent);
|
|
||||||
parent.PurchaseDate = episode.PurchaseDate;
|
|
||||||
|
|
||||||
setSeries(parent, children);
|
|
||||||
children.Add(parent);
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug("Completed parent scan for {episode}", episode);
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all episodes belonging to <paramref name="parent"/> in batches of <see cref="BatchSize"/> and writes the batch get tasks to <paramref name="channel"/>
|
|
||||||
/// This method only completes after all episode batch 'gets' have been written to the channel
|
|
||||||
/// </summary>
|
|
||||||
private async Task getParentEpisodesAsync(Item parent, ChannelWriter<Task<List<Item>>> channel)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
|
||||||
|
|
||||||
var episodeIds = parent.Relationships
|
|
||||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
|
||||||
.Select(r => r.Asin);
|
|
||||||
|
|
||||||
for (int batchNum = 0; episodeIds.Any(); batchNum++)
|
|
||||||
{
|
|
||||||
var batch = episodeIds.Take(BatchSize);
|
|
||||||
|
|
||||||
await channel.WriteAsync(getEpisodeBatchAsync(batchNum, parent, batch));
|
|
||||||
|
|
||||||
episodeIds = episodeIds.Skip(BatchSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<Item>> getEpisodeBatchAsync(int batchNum, Item parent, IEnumerable<string> childrenIds)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
List<Item> episodeBatch = await Api.GetCatalogProductsAsync(childrenIds, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
|
||||||
|
|
||||||
setSeries(parent, episodeBatch);
|
|
||||||
|
|
||||||
if (batchNum == 0)
|
|
||||||
episodeBatch.Add(parent);
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug($"Batch {batchNum}: {episodeBatch.Count} results\t({{parent}})", parent);
|
|
||||||
|
|
||||||
return episodeBatch;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
|
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new { asins });
|
||||||
{
|
|
||||||
ParentId = parent.Asin,
|
|
||||||
ParentTitle = parent.Title,
|
|
||||||
BatchNumber = batchNum,
|
|
||||||
ChildIdBatch = childrenIds
|
|
||||||
});
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
finally { semaphore.Release(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setSeries(Item parent, IEnumerable<Item> children)
|
private static void setSeries(Item parent, IEnumerable<Item> children)
|
||||||
@ -314,6 +245,9 @@ namespace AudibleUtilities
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (parent.PurchaseDate == default)
|
||||||
|
parent.PurchaseDate = children.Select(c => c.PurchaseDate).Order().First();
|
||||||
|
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||||
|
|||||||
@ -91,29 +91,8 @@ namespace DtoImporterService
|
|||||||
return qtyNew;
|
return qtyNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Subscription Plan Names:
|
|
||||||
*
|
|
||||||
* US: "SpecialBenefit"
|
|
||||||
* IT: "Rodizio"
|
|
||||||
*
|
|
||||||
* Audible Plus Plan Names:
|
|
||||||
*
|
|
||||||
* US: "US Minerva"
|
|
||||||
* IT: "Audible-AYCL"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
//This SEEMS to work to detect plus titles which are no longer available.
|
|
||||||
//I have my doubts it won't yield false negatives, but I have more
|
|
||||||
//confidence that it won't yield many/any false positives.
|
|
||||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||||
=> item.DtoItem.IsAyce is true
|
=> item.DtoItem.IsAyce is true
|
||||||
&& item.DtoItem.Plans?.Any(p =>
|
&& item.DtoItem.Plans?.Any(p => p.IsAyce) is not true;
|
||||||
p.PlanName.ContainsInsensitive("Minerva") ||
|
|
||||||
p.PlanName.ContainsInsensitive("AYCL") ||
|
|
||||||
p.PlanName.ContainsInsensitive("Free")
|
|
||||||
) is not true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
using LibationAvalonia.Dialogs;
|
using LibationAvalonia.Dialogs;
|
||||||
|
using LibationFileManager;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
@ -20,5 +22,21 @@ namespace LibationAvalonia
|
|||||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||||
|
|
||||||
public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
|
public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
|
||||||
|
|
||||||
|
|
||||||
|
private static Bitmap defaultImage;
|
||||||
|
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var ms = new System.IO.MemoryStream(picture);
|
||||||
|
return new Bitmap(ms);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
using var ms = new System.IO.MemoryStream(PictureStorage.GetDefaultImage(defaultSize));
|
||||||
|
return defaultImage ??= new Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
@ -10,7 +9,6 @@ using LibationAvalonia.ViewModels;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
@ -112,8 +110,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
//init cover image
|
//init cover image
|
||||||
var picture = PictureStorage.GetPictureSynchronously(new PictureDefinition(libraryBook.Book.PictureId, PictureSize._80x80));
|
var picture = PictureStorage.GetPictureSynchronously(new PictureDefinition(libraryBook.Book.PictureId, PictureSize._80x80));
|
||||||
using var ms = new System.IO.MemoryStream(picture);
|
Cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||||
Cover = new Bitmap(ms);
|
|
||||||
|
|
||||||
//init book details
|
//init book details
|
||||||
DetailsText = @$"
|
DetailsText = @$"
|
||||||
|
|||||||
@ -2,7 +2,6 @@ using Avalonia.Markup.Xaml;
|
|||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
@ -29,17 +28,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public void SetCoverBytes(byte[] cover)
|
public void SetCoverBytes(byte[] cover)
|
||||||
{
|
{
|
||||||
try
|
_bitmapHolder.CoverImage = AvaloniaUtils.TryLoadImageOrDefault(cover);
|
||||||
{
|
|
||||||
var ms = new MemoryStream(cover);
|
|
||||||
_bitmapHolder.CoverImage = new Bitmap(ms);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error loading cover art for {file}", PictureFileName);
|
|
||||||
using var ms = App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg");
|
|
||||||
_bitmapHolder.CoverImage = new Bitmap(ms);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
|||||||
@ -8,28 +8,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||||
{
|
{
|
||||||
private static Bitmap _defaultImage;
|
|
||||||
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
|
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
|
||||||
|
|
||||||
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
||||||
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
|
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
|
||||||
|
|
||||||
protected override Bitmap LoadImage(byte[] picture)
|
protected override Bitmap LoadImage(byte[] picture)
|
||||||
{
|
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||||
try
|
|
||||||
{
|
|
||||||
using var ms = new System.IO.MemoryStream(picture);
|
|
||||||
return new Bitmap(ms);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
|
|
||||||
return _defaultImage ??= new Bitmap(App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Bitmap GetResourceImage(string rescName)
|
protected override Bitmap GetResourceImage(string rescName)
|
||||||
{
|
{
|
||||||
|
//These images are assest, so assume they will never corrupt.
|
||||||
using var stream = App.OpenAsset(rescName + ".png");
|
using var stream = App.OpenAsset(rescName + ".png");
|
||||||
return new Bitmap(stream);
|
return new Bitmap(stream);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,16 +115,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||||
|
|
||||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||||
using var ms = new System.IO.MemoryStream(picture);
|
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||||
_cover = new Bitmap(ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||||
{
|
{
|
||||||
using var ms = new System.IO.MemoryStream(e.Picture);
|
Cover = AvaloniaUtils.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
|
||||||
Cover = new Bitmap(ms);
|
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,7 +132,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
//Add absent entries to grid, or update existing entry
|
//Add absent entries to grid, or update existing entry
|
||||||
var allEntries = SOURCE.BookEntries().ToList();
|
var allEntries = SOURCE.BookEntries().ToList();
|
||||||
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
||||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToList();
|
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
@ -142,7 +142,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
if (libraryBook.Book.IsProduct())
|
if (libraryBook.Book.IsProduct())
|
||||||
UpsertBook(libraryBook, existingEntry);
|
UpsertBook(libraryBook, existingEntry);
|
||||||
else if (parentedEpisodes.Any(lb => lb == libraryBook))
|
else if (parentedEpisodes.Contains(libraryBook))
|
||||||
//Only try to add or update is this LibraryBook is a know child of a parent
|
//Only try to add or update is this LibraryBook is a know child of a parent
|
||||||
UpsertEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
UpsertEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
DownloadQueue.Add(def);
|
DownloadQueue.Add(def);
|
||||||
return (true, getDefaultImage(def.Size));
|
return (true, GetDefaultImage(def.Size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
|
public static void SetDefaultImage(PictureSize pictureSize, byte[] bytes)
|
||||||
=> defaultImages[pictureSize] = bytes;
|
=> defaultImages[pictureSize] = bytes;
|
||||||
private static byte[] getDefaultImage(PictureSize size)
|
public static byte[] GetDefaultImage(PictureSize size)
|
||||||
=> defaultImages.ContainsKey(size)
|
=> defaultImages.ContainsKey(size)
|
||||||
? defaultImages[size]
|
? defaultImages[size]
|
||||||
: new byte[0];
|
: new byte[0];
|
||||||
@ -120,7 +120,7 @@ namespace LibationFileManager
|
|||||||
private static byte[] downloadBytes(PictureDefinition def)
|
private static byte[] downloadBytes(PictureDefinition def)
|
||||||
{
|
{
|
||||||
if (def.PictureId is null)
|
if (def.PictureId is null)
|
||||||
return getDefaultImage(def.Size);
|
return GetDefaultImage(def.Size);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -135,7 +135,7 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return getDefaultImage(def.Size);
|
return GetDefaultImage(def.Size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
@ -42,7 +41,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
this.Text = Book.Title;
|
this.Text = Book.Title;
|
||||||
|
|
||||||
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||||
this.coverPb.Image = Dinah.Core.WindowsDesktop.Drawing.ImageReader.ToImage(picture);
|
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||||
|
|
||||||
var t = @$"
|
var t = @$"
|
||||||
Title: {Book.Title}
|
Title: {Book.Title}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@ -44,6 +45,17 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
var rect = new Rectangle(x, y, savedState.Width, savedState.Height);
|
var rect = new Rectangle(x, y, savedState.Width, savedState.Height);
|
||||||
|
|
||||||
|
if (savedState.IsMaximized)
|
||||||
|
{
|
||||||
|
//When a window is maximized, the client rectangle is not on a screen (y is negative).
|
||||||
|
form.StartPosition = FormStartPosition.Manual;
|
||||||
|
form.DesktopBounds = rect;
|
||||||
|
|
||||||
|
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
||||||
|
form.WindowState = FormWindowState.Maximized;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// is proposed rect on a screen?
|
// is proposed rect on a screen?
|
||||||
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
||||||
{
|
{
|
||||||
@ -56,8 +68,8 @@ namespace LibationWinForms
|
|||||||
form.Size = rect.Size;
|
form.Size = rect.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
form.WindowState = FormWindowState.Normal;
|
||||||
form.WindowState = savedState.IsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SaveSizeAndLocation(this Form form, Configuration config)
|
public static void SaveSizeAndLocation(this Form form, Configuration config)
|
||||||
|
|||||||
@ -19,15 +19,7 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
public void SetCoverArt(byte[] cover)
|
public void SetCoverArt(byte[] cover)
|
||||||
{
|
{
|
||||||
try
|
pictureBox1.Image = WinFormsUtil.TryLoadImageOrDefault(cover);
|
||||||
{
|
|
||||||
pictureBox1.Image = Dinah.Core.WindowsDesktop.Drawing.ImageReader.ToImage(cover);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error loading cover art for {file}", PictureFileName);
|
|
||||||
pictureBox1.Image = Properties.Resources.default_cover_500x500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Make the form's aspect ratio always match the picture's aspect ratio.
|
#region Make the form's aspect ratio always match the picture's aspect ratio.
|
||||||
|
|||||||
@ -265,9 +265,7 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
var allEntries = bindingList.AllItems().BookEntries();
|
var allEntries = bindingList.AllItems().BookEntries();
|
||||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToList();
|
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||||
|
|
||||||
var sw = new Stopwatch();
|
|
||||||
|
|
||||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||||
{
|
{
|
||||||
@ -278,14 +276,11 @@ namespace LibationWinForms.GridView
|
|||||||
AddOrUpdateBook(libraryBook, existingEntry);
|
AddOrUpdateBook(libraryBook, existingEntry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sw.Start();
|
if (parentedEpisodes.Contains(libraryBook))
|
||||||
if (parentedEpisodes.Any(lb => lb == libraryBook))
|
|
||||||
{
|
{
|
||||||
sw.Stop();
|
|
||||||
//Only try to add or update is this LibraryBook is a know child of a parent
|
//Only try to add or update is this LibraryBook is a know child of a parent
|
||||||
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||||
}
|
}
|
||||||
sw.Stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = false;
|
bindingList.SuspendFilteringOnUpdate = false;
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core.WindowsDesktop.Drawing;
|
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using System;
|
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
namespace LibationWinForms.GridView
|
||||||
@ -14,23 +12,12 @@ namespace LibationWinForms.GridView
|
|||||||
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
||||||
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);
|
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);
|
||||||
|
|
||||||
protected override object LoadImage(byte[] picture)
|
protected override Image LoadImage(byte[] picture)
|
||||||
{
|
=> WinFormsUtil.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||||
try
|
|
||||||
{
|
|
||||||
return ImageReader.ToImage(picture);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error loading cover art for {Book}", Book);
|
|
||||||
return Properties.Resources.default_cover_80x80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Image GetResourceImage(string rescName)
|
protected override Image GetResourceImage(string rescName)
|
||||||
{
|
{
|
||||||
var image = Properties.Resources.ResourceManager.GetObject(rescName);
|
var image = Properties.Resources.ResourceManager.GetObject(rescName);
|
||||||
|
|
||||||
return image as Bitmap;
|
return image as Bitmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ using AudibleApi;
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
using Dinah.Core.WindowsDesktop.Drawing;
|
|
||||||
using FileLiberator;
|
using FileLiberator;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
@ -87,7 +86,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
if (isDefault)
|
if (isDefault)
|
||||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||||
_cover = ImageReader.ToImage(picture);
|
_cover = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80); ;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +94,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
{
|
{
|
||||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||||
{
|
{
|
||||||
Cover = ImageReader.ToImage(e.Picture);
|
Cover = WinFormsUtil.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,7 +259,7 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
||||||
{
|
{
|
||||||
Cover = ImageReader.ToImage(coverArt);
|
Cover = WinFormsUtil.TryLoadImageOrDefault(coverArt, PictureSize._80x80);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
23
Source/LibationWinForms/WinFormsUtil.cs
Normal file
23
Source/LibationWinForms/WinFormsUtil.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Dinah.Core.WindowsDesktop.Drawing;
|
||||||
|
using LibationFileManager;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace LibationWinForms
|
||||||
|
{
|
||||||
|
internal static class WinFormsUtil
|
||||||
|
{
|
||||||
|
private static Bitmap defaultImage;
|
||||||
|
public static Image TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ImageReader.ToImage(picture);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
using var ms = new System.IO.MemoryStream(PictureStorage.GetDefaultImage(defaultSize));
|
||||||
|
return defaultImage ??= new Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user