Add SeriesViewDialog

This commit is contained in:
MBucari 2023-03-19 20:05:18 -06:00 committed by Mbucari
parent 784ab73a36
commit 9ae1f0399b
27 changed files with 1293 additions and 18 deletions

View file

@ -0,0 +1,13 @@
using LibationFileManager;
using System;
namespace LibationUiBase
{
public static class BaseUtil
{
/// <summary>A delegate that loads image bytes into the the UI framework's image format.</summary>
public static Func<byte[], PictureSize, object> LoadImage { get; private set; }
public static void SetLoadImageDelegate(Func<byte[], PictureSize, object> tryLoadImage)
=> LoadImage = tryLoadImage;
}
}

View file

@ -0,0 +1,133 @@
using ApplicationServices;
using AudibleApi;
using AudibleApi.Common;
using DataLayer;
using FileLiberator;
using System;
using System.Linq;
using Dinah.Core;
using System.Threading.Tasks;
namespace LibationUiBase.SeriesView
{
internal class AyceButton : SeriesButton
{
//Making this event and field static prevents concurrent additions to the library.
//Search engine indexer does not support concurrent re-indexing.
private static event EventHandler ButtonEnabled;
private static bool globalEnabled = true;
public override bool HasButtonAction => true;
public override string DisplayText
=> InLibrary ? "Remove\r\nFrom\r\nLibrary"
: "FREE\r\n\r\nAdd to\r\nLibrary";
public override bool Enabled
{
get => globalEnabled;
protected set
{
if (globalEnabled != value)
{
globalEnabled = value;
ButtonEnabled?.Invoke(null, EventArgs.Empty);
}
}
}
internal AyceButton(Item item, bool inLibrary) : base(item, inLibrary)
{
ButtonEnabled += DownloadButton_ButtonEnabled;
}
public override async Task PerformClickAsync(LibraryBook accountBook)
{
if (!Enabled) return;
Enabled = false;
try
{
if (InLibrary)
await RemoveFromLibraryAsync(accountBook);
else
await AddToLibraryAsync(accountBook);
}
catch(Exception ex)
{
var addRemove = InLibrary ? "remove" : "add";
var toFrom = InLibrary ? "from" : "to";
Serilog.Log.Logger.Error(ex, $"Failed to {addRemove} {{book}} {toFrom} library", new { Item.ProductId, Item.TitleWithSubtitle });
}
finally { Enabled = true; }
}
private async Task RemoveFromLibraryAsync(LibraryBook accountBook)
{
Api api = await accountBook.GetApiAsync();
if (await api.RemoveItemFromLibraryAsync(Item.ProductId))
{
using var context = DbContexts.GetContext();
var lb = context.GetLibraryBook_Flat_NoTracking(Item.ProductId);
int result = await Task.Run((new[] { lb }).PermanentlyDeleteBooks);
InLibrary = result == 0;
}
}
private async Task AddToLibraryAsync(LibraryBook accountBook)
{
Api api = await accountBook.GetApiAsync();
if (!await api.AddItemToLibraryAsync(Item.ProductId)) return;
Item item = null;
for (int tryCount = 0; tryCount < 5 && item is null; tryCount++)
{
//Wait a half second to allow the server time to update
await Task.Delay(500);
item = await api.GetLibraryBookAsync(Item.ProductId, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
}
if (item is null) return;
if (item.IsEpisodes)
{
var seriesParent = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)
.Select(lb => lb.Book)
.FirstOrDefault(b => b.IsEpisodeParent() && b.AudibleProductId.In(item.Relationships.Select((Relationship r) => r.Asin)));
if (seriesParent is null) return;
item.Series = new[]
{
new AudibleApi.Common.Series
{
Asin = seriesParent.AudibleProductId,
Sequence = item.Relationships.FirstOrDefault(r => r.Asin == seriesParent.AudibleProductId)?.Sort?.ToString() ?? "0",
Title = seriesParent.Title
}
};
}
InLibrary = await LibraryCommands.ImportSingleToDbAsync(item, accountBook.Account, accountBook.Book.Locale) != 0;
}
private void DownloadButton_ButtonEnabled(object sender, EventArgs e)
=> OnPropertyChanged(nameof(Enabled));
public override int CompareTo(object ob)
{
if (ob is not AyceButton other) return 1;
return other.InLibrary.CompareTo(InLibrary);
}
~AyceButton()
{
ButtonEnabled -= DownloadButton_ButtonEnabled;
}
}
}

View file

@ -0,0 +1,51 @@
using AudibleApi.Common;
using DataLayer;
using Dinah.Core.Threading;
using System;
using System.ComponentModel;
using System.Threading.Tasks;
namespace LibationUiBase.SeriesView
{
/// <summary>
/// base view model for the Series Viewer 'Availability' button column
/// </summary>
public abstract class SeriesButton : SynchronizeInvoker, IComparable, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool inLibrary;
protected Item Item { get; }
public abstract string DisplayText { get; }
public abstract bool HasButtonAction { get; }
public abstract bool Enabled { get; protected set; }
public bool InLibrary
{
get => inLibrary;
protected set
{
if (inLibrary != value)
{
inLibrary = value;
OnPropertyChanged(nameof(InLibrary));
OnPropertyChanged(nameof(DisplayText));
}
}
}
protected SeriesButton(Item item, bool inLibrary)
{
Item = item;
this.inLibrary = inLibrary;
}
public abstract Task PerformClickAsync(LibraryBook accountBook);
protected void OnPropertyChanged(string propertyName)
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
public override string ToString() => DisplayText;
public abstract int CompareTo(object ob);
}
}

View file

@ -0,0 +1,151 @@
using ApplicationServices;
using AudibleApi;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Threading;
using FileLiberator;
using LibationFileManager;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LibationUiBase.SeriesView
{
public class SeriesItem : SynchronizeInvoker, INotifyPropertyChanged
{
public object Cover { get; private set; }
public SeriesOrder Order { get; }
public string Title => Item.TitleWithSubtitle;
public SeriesButton Button { get; }
public Item Item { get; }
public event PropertyChangedEventHandler PropertyChanged;
private SeriesItem(Item item, string order, bool inLibrary, bool inWishList)
{
Item = item;
Order = new SeriesOrder(order);
Button = Item.Plans.Any(p => p.IsAyce) ? new AyceButton(item, inLibrary) : new WishlistButton(item, inLibrary, inWishList);
LoadCover(item.PictureId);
Button.PropertyChanged += DownloadButton_PropertyChanged;
}
public void ViewOnAudible(string localeString)
{
var locale = Localization.Get(localeString);
var link = $"https://www.audible.{locale.TopDomain}/pd/{Item.ProductId}";
Go.To.Url(link);
}
private void DownloadButton_PropertyChanged(object sender, PropertyChangedEventArgs e)
=> OnPropertyChanged(nameof(Button));
private void OnPropertyChanged(string propertyName)
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
private void LoadCover(string pictureId)
{
var (isDefault, picture) = PictureStorage.GetPicture(new PictureDefinition(pictureId, PictureSize._80x80));
if (isDefault)
{
PictureStorage.PictureCached += PictureStorage_PictureCached;
}
Cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e?.Definition.PictureId != null && Item?.PictureId != null)
{
byte[] picture = e.Picture;
if ((picture == null || picture.Length != 0) && e.Definition.PictureId == Item.PictureId)
{
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
OnPropertyChanged(nameof(Cover));
}
}
}
public static async Task<Dictionary<Item, List<SeriesItem>>> GetAllSeriesItemsAsync(LibraryBook libraryBook)
{
var api = await libraryBook.GetApiAsync();
//Get Item for each series that this book belong to
var seriesItemsTask = api.GetCatalogProductsAsync(libraryBook.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId), CatalogOptions.ResponseGroupOptions.Media | CatalogOptions.ResponseGroupOptions.Relationships);
using var semaphore = new SemaphoreSlim(10);
//Start getting the wishlist in the background
var wishlistTask = api.GetWishListProductsAsync(
new WishListOptions
{
PageNumber = 0,
NumberOfResultPerPage = 50,
ResponseGroups = WishListOptions.ResponseGroupOptions.None
},
numItemsPerRequest: 50,
semaphore);
var items = new Dictionary<Item, List<Item>>();
//Get all children of all series
foreach (var series in await seriesItemsTask)
{
//Books that are part of series have RelationshipType.Series
//Podcast episodes have RelationshipType.Episode
var childrenAsins = series.Relationships
.Where(r => r.RelationshipType is RelationshipType.Series or RelationshipType.Episode && r.RelationshipToProduct is RelationshipToProduct.Child)
.Select(r => r.Asin)
.ToList();
if (childrenAsins.Count > 0)
{
var children = await api.GetCatalogProductsAsync(childrenAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS, 50, semaphore);
//If the price is null, this item is not available to the user
var childrenWithPrices = children.Where(p => p.Price != null).ToList();
if (childrenWithPrices.Count > 0)
items[series] = childrenWithPrices;
}
}
//Await the wishlist asins
var wishlistAsins = (await wishlistTask).Select(w => w.Asin).ToHashSet();
var fullLib = DbContexts.GetLibrary_Flat_NoTracking();
var seriesEntries = new Dictionary<Item, List<SeriesItem>>();
//Create a SeriesItem liste for each series.
foreach (var series in items.Keys)
{
ApiExtended.SetSeries(series, items[series]);
seriesEntries[series] = new List<SeriesItem>();
foreach (var item in items[series].Where(i => !string.IsNullOrEmpty(i.PictureId)))
{
var order = item.Series.Single(s => s.Asin == series.Asin).Sequence;
//Match the account/book in the database
var inLibrary = fullLib.Any(lb => lb.Account == libraryBook.Account && lb.Book.AudibleProductId == item.ProductId && !lb.AbsentFromLastScan);
var inWishList = wishlistAsins.Contains(item.Asin);
seriesEntries[series].Add(new SeriesItem(item, order, inLibrary, inWishList));
}
}
return seriesEntries;
}
~SeriesItem()
{
PictureStorage.PictureCached -= PictureStorage_PictureCached;
Button.PropertyChanged -= DownloadButton_PropertyChanged;
}
}
}

View file

@ -0,0 +1,23 @@
using System;
namespace LibationUiBase.SeriesView
{
public class SeriesOrder : IComparable
{
public float Order { get; }
public string OrderString { get; }
public SeriesOrder(string orderString)
{
OrderString = orderString;
Order = float.TryParse(orderString, out var o) ? o : -1f;
}
public override string ToString() => OrderString;
public int CompareTo(object obj)
{
if (obj is not SeriesOrder other) return 1;
return Order.CompareTo(other.Order);
}
}
}

View file

@ -0,0 +1,93 @@
using AudibleApi;
using AudibleApi.Common;
using DataLayer;
using FileLiberator;
using System;
using System.Threading.Tasks;
namespace LibationUiBase.SeriesView
{
internal class WishlistButton : SeriesButton
{
private bool instanceEnabled = true;
private bool inWishList;
public override bool HasButtonAction => !InLibrary;
public override string DisplayText
=> InLibrary ? "Already\r\nOwned"
: InWishList ? "Remove\r\nFrom\r\nWishlist"
: "Add to\r\nWishlist";
public override bool Enabled
{
get => instanceEnabled;
protected set
{
if (instanceEnabled != value)
{
instanceEnabled = value;
OnPropertyChanged(nameof(Enabled));
}
}
}
private bool InWishList
{
get => inWishList;
set
{
if (inWishList != value)
{
inWishList = value;
OnPropertyChanged(nameof(InWishList));
OnPropertyChanged(nameof(DisplayText));
}
}
}
internal WishlistButton(Item item, bool inLibrary, bool inWishList) : base(item, inLibrary)
{
this.inWishList = inWishList;
}
public override async Task PerformClickAsync(LibraryBook accountBook)
{
if (!Enabled || !HasButtonAction) return;
Enabled = false;
try
{
Api api = await accountBook.GetApiAsync();
if (InWishList)
{
await api.DeleteFromWishListAsync(Item.Asin);
InWishList = false;
}
else
{
await api.AddToWishListAsync(Item.Asin);
InWishList = true;
}
}
catch (Exception ex)
{
var addRemove = InWishList ? "remove" : "add";
var toFrom = InWishList ? "from" : "to";
Serilog.Log.Logger.Error(ex, $"Failed to {addRemove} {{book}} {toFrom} wish list", new { Item.ProductId, Item.TitleWithSubtitle });
}
finally { Enabled = true; }
}
public override int CompareTo(object ob)
{
if (ob is not WishlistButton other) return -1;
int libcmp = other.InLibrary.CompareTo(InLibrary);
return (libcmp == 0) ? other.InWishList.CompareTo(InWishList) : libcmp;
}
}
}