Compare commits

..

34 Commits

Author SHA1 Message Date
Robert McRackan
5a80a0cc06 Second bug fix for issue 263 2022-05-27 07:15:15 -04:00
rmcrackan
aebefac7e6 Merge pull request #266 from Mbucari/master
Fix my own screwup
2022-05-27 07:10:02 -04:00
Michael Bucari-Tovo
b2d0ee41f2 Fix my own screwup 2022-05-26 21:26:56 -06:00
Robert McRackan
9c20250b0a increm ver 2022-05-26 21:10:12 -04:00
rmcrackan
b196836fca Merge pull request #264 from Mbucari/master
Fix for episodes with no series link
2022-05-26 20:41:55 -04:00
Michael Bucari-Tovo
d9fbcc615a Change flow 2022-05-26 18:06:44 -06:00
Michael Bucari-Tovo
fb247fb33f Add better handling for parents and series with no children. 2022-05-26 17:29:55 -06:00
Michael Bucari-Tovo
61f4dbd896 No need to make a new list. 2022-05-26 16:50:43 -06:00
Michael Bucari-Tovo
2c86571818 Better identification of Chilv vs Parent from SeriesBook.Order 2022-05-26 16:49:03 -06:00
Michael Bucari-Tovo
1b2ec67726 Add series info for parent will null order. 2022-05-26 16:43:56 -06:00
Michael Bucari-Tovo
845af854bd Add exception handling to products display 2022-05-26 16:29:40 -06:00
Mbucari
15b6a66d98 Merge branch 'rmcrackan:master' into master 2022-05-26 16:13:13 -06:00
Michael Bucari-Tovo
c95ba0764b Fix bug and add groundwork for future feature 2022-05-26 16:11:52 -06:00
Robert McRackan
42c0648ba7 Bug fix #262 : 'file not found' after moved dir 2022-05-26 16:11:03 -04:00
Robert McRackan
0a6e55dcb7 * Much faster library scans
* Libraries of unlimited size now supported (prev limit was 10k)
2022-05-26 11:33:05 -04:00
rmcrackan
99b77decff Merge pull request #260 from Mbucari/master
Throttle episode scanning to 10 concurrent scans.
2022-05-26 11:29:18 -04:00
Robert McRackan
9e2ca4e586 update dependencies 2022-05-26 10:45:57 -04:00
Michael Bucari-Tovo
2e8acfdeef Limnit episode concurrency to 5 2022-05-26 08:44:39 -06:00
Michael Bucari-Tovo
630096e06d Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-26 08:43:33 -06:00
Michael Bucari-Tovo
d92d892dc7 logging 2022-05-25 20:45:18 -06:00
Michael Bucari-Tovo
a8f41841bd Throttle episode scanning to 10 concurrent scans. 2022-05-25 20:43:12 -06:00
rmcrackan
76954b5a0a Merge pull request #258 from Mbucari/master
Add support for unlimited library size.
2022-05-25 22:00:22 -04:00
Michael Bucari-Tovo
c57b184a09 Remove test params 2022-05-25 16:51:25 -06:00
Michael Bucari-Tovo
20ca4e0739 Refactor for clarity. 2022-05-25 16:49:22 -06:00
Mbucari
a972ed5e2e Merge branch 'rmcrackan:master' into master 2022-05-25 16:05:31 -06:00
Michael Bucari-Tovo
2b15bc6ebb Count Items as they come in and log total. 2022-05-25 15:11:38 -06:00
Michael Bucari-Tovo
562496cfaa Add more logging 2022-05-24 21:36:56 -06:00
Michael Bucari-Tovo
8283f19d6b Parallelize getChildEpisodesAsync 2022-05-24 21:17:59 -06:00
Michael Bucari-Tovo
242909b542 Don't import empty episode 2022-05-24 18:39:47 -06:00
Michael Bucari-Tovo
a7b83ad5e0 Remove 10,000 book limitation and simplify episode import 2022-05-24 18:27:20 -06:00
Michael Bucari-Tovo
ed66019d9a Cleanup 2022-05-24 18:24:53 -06:00
Michael Bucari-Tovo
bc0009be6c Use event return value instead of passing a set delegate. 2022-05-24 15:47:30 -06:00
Michael Bucari-Tovo
c88f47eed4 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 15:34:09 -06:00
Michael Bucari-Tovo
59de048ced Error handling network error. 2022-05-24 15:33:52 -06:00
12 changed files with 121 additions and 128 deletions

View File

@@ -217,40 +217,47 @@ namespace AaxDecrypter
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
do
try
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
do
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
} while (downloadPosition < ContentLength && !IsCancelled);
downloadPosition += bytesRead;
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
downloadedPiece.Set();
downloadEnded.Set();
} while (downloadPosition < ContentLength && !IsCancelled);
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
IsCancelled = true;
}
}
#endregion

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.9.0.1</Version>
<Version>7.10.2.1</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Common;
@@ -118,31 +119,38 @@ namespace AudibleUtilities
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
{
var items = new List<Item>();
#if DEBUG
//// this will not work for multi accounts
//var library_json = "library.json";
//library_json = System.IO.Path.GetFullPath(library_json);
//if (System.IO.File.Exists(library_json))
//{
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
//}
#endif
Serilog.Log.Logger.Debug("Begin initial library scan");
Serilog.Log.Logger.Debug("Beginning library scan.");
if (!items.Any())
items = await Api.GetAllLibraryItemsAsync(libraryOptions);
List<Task<List<Item>>> getChildEpisodesTasks = new();
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
int count = 0, maxConcurrentEpisodeScans = 5;
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentEpisodeScans);
await manageEpisodesAsync(items, importEpisodes);
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
{
if (item.IsEpisodes && importEpisodes)
{
//Get child episodes asynchronously and await all at the end
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
}
else if (!item.IsEpisodes)
items.Add(item);
Serilog.Log.Logger.Debug("Episode scan complete");
count++;
}
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
//await and add all episides from all parents
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
items.AddRange(epList);
Serilog.Log.Logger.Debug("Completed library scan.");
#if DEBUG
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
#endif
var validators = new List<IValidator>();
validators.AddRange(getValidators());
foreach (var v in validators)
@@ -156,54 +164,24 @@ namespace AudibleUtilities
}
#region episodes and podcasts
private async Task manageEpisodesAsync(List<Item> items, bool importEpisodes)
private async Task<List<Item>> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent)
{
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
await concurrencySemaphore.WaitAsync();
try
{
// get parents
var parents = items.Where(i => i.IsEpisodes).ToList();
#if DEBUG
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("parents.json", parentsDebug);
#endif
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
if (!parents.Any())
return;
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
items.RemoveAll(i => i.IsEpisodes);
if (importEpisodes)
{
// add children
var children = await getEpisodesAsync(parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children);
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
}
}
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
{
var results = new List<Item>();
foreach (var parent in parents)
{
var children = await getEpisodeChildrenAsync(parent);
// actual individual episode, not the parent of a series.
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
if (!children.Any())
{
results.Add(parent);
continue;
//The parent is the only episode in the podcase series,
//so the parent is its own child.
parent.Series = new Series[] { new Series { Asin = parent.Asin, Sequence = RelationshipToProduct.Parent, Title = parent.TitleWithSubtitle } };
children.Add(parent);
return children;
}
foreach (var child in children)
@@ -217,7 +195,7 @@ namespace AudibleUtilities
{
Asin = parent.Asin,
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
Title = parent.TitleWithSubtitle
}
};
@@ -232,10 +210,14 @@ namespace AudibleUtilities
};
}
results.AddRange(children);
}
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
return results;
return children;
}
finally
{
concurrencySemaphore.Release();
}
}
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
@@ -261,8 +243,8 @@ namespace AudibleUtilities
{
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
#if DEBUG
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
#endif
}
catch (Exception ex)
@@ -277,7 +259,7 @@ namespace AudibleUtilities
throw;
}
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent);
// the service returned no results. probably indicates an error. stop running batches
if (!childrenBatch.Any())
break;
@@ -295,7 +277,7 @@ namespace AudibleUtilities
if (childrenIds.Count != results.Count)
{
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent);
throw ex;
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.8.4.1" />
<PackageReference Include="AudibleApi" Version="3.0.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,7 +4,8 @@ namespace FileLiberator
{
public abstract class AudioDecodable : Processable
{
public event EventHandler<Action<byte[]>> RequestCoverArt;
public delegate byte[] RequestCoverArtHandler(object sender, EventArgs eventArgs);
public event RequestCoverArtHandler RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
@@ -32,10 +33,10 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, narrators);
}
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
protected byte[] OnRequestCoverArt()
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
RequestCoverArt?.Invoke(this, setCoverArtDel);
return RequestCoverArt?.Invoke(this, new());
}
protected void OnCoverImageDiscovered(byte[] coverImage)

View File

@@ -247,7 +247,7 @@ namespace FileLiberator
if (e is not null)
OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
abDownloader.SetCoverArt(OnRequestCoverArt());
}
/// <summary>Move new files to 'Books' directory</summary>

View File

@@ -134,7 +134,8 @@ namespace FileManager
private void AddPath(string path)
{
if (!File.Exists(path)) return;
if (!File.Exists(path) && !Directory.Exists(path))
return;
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
else

View File

@@ -100,14 +100,15 @@ namespace LibationWinForms.GridView
{
#nullable enable
public static IEnumerable<SeriesEntry> Series(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.Where(i => i is SeriesEntry).Cast<SeriesEntry>();
=> gridEntries.OfType<SeriesEntry>();
public static IEnumerable<LibraryBookEntry> LibraryBooks(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.Where(i => i is LibraryBookEntry).Cast<LibraryBookEntry>();
=> gridEntries.OfType<LibraryBookEntry>();
public static LibraryBookEntry? FindBookByAsin(this IEnumerable<LibraryBookEntry> gridEntries, string audibleProductID)
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
public static SeriesEntry? FindBookSeriesEntry(this IEnumerable<GridEntry> gridEntries, IEnumerable<SeriesBook> matchSeries)
=> gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.Series().Where(i => i.Children.Count == 0);
public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode;
}
}

View File

@@ -30,10 +30,7 @@ namespace LibationWinForms.GridView
if (status.IsSeries)
{
var imageName = status.Expanded ? "minus" : "plus";
var bmp = (Bitmap)Properties.Resources.ResourceManager.GetObject(imageName);
DrawButtonImage(graphics, bmp, cellBounds);
DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus: Properties.Resources.plus, cellBounds);
ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand";
}

View File

@@ -84,18 +84,25 @@ namespace LibationWinForms.GridView
public void Display()
{
// don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking();
if (!hasBeenDisplayed)
try
{
// bind
productsGrid.BindToGrid(lib);
hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new());
// don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking();
if (!hasBeenDisplayed)
{
// bind
productsGrid.BindToGrid(lib);
hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new());
}
else
productsGrid.UpdateGrid(lib);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
}
else
productsGrid.UpdateGrid(lib);
}

View File

@@ -84,11 +84,12 @@ namespace LibationWinForms.GridView
{
var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
var episodes = dbBooks.Where(b => b.Book.ContentType is ContentType.Episode).ToList();
var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList();
foreach (var series in episodes.Select(lb => lb.Book.SeriesLink.First()).DistinctBy(s => s.Series))
var allSeries = episodes.SelectMany(lb => lb.Book.SeriesLink.Where(s => !s.Series.AudibleSeriesId.StartsWith("SERIES_"))).DistinctBy(s => s.Series).ToList();
foreach (var series in allSeries)
{
var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.First().Series == series.Book.SeriesLink.First().Series));
var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.Any(s => s.Series == series.Series)));
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
@@ -117,7 +118,7 @@ namespace LibationWinForms.GridView
// add new to top
if (existingItem is null)
{
if (libraryBook.Book.ContentType is ContentType.Episode)
if (libraryBook.IsEpisodeChild())
{
LibraryBookEntry lbe;
//Find the series that libraryBook belongs to, if it exists
@@ -148,7 +149,7 @@ namespace LibationWinForms.GridView
series.NotifyPropertyChanged();
}
else
else if (libraryBook.Book.ContentType is not ContentType.Episode)
//Add the new product
bindingList.Insert(0, new LibraryBookEntry(libraryBook));
}

View File

@@ -60,7 +60,6 @@ namespace LibationWinForms.ProcessQueue
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
private Processable NextProcessable() => _currentProcessable = null;
private Processable _currentProcessable;
private Func<byte[]> GetCoverArtDelegate;
private readonly Queue<Func<Processable>> Processes = new();
private readonly LogMe Logger;
@@ -232,11 +231,14 @@ namespace LibationWinForms.ProcessQueue
BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
}
public void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{
byte[] coverData = GetCoverArtDelegate();
setCoverArtDelegate(coverData);
byte[] coverData = PictureStorage
.GetPictureSynchronously(
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
AudioDecodable_CoverImageDiscovered(this, coverData);
return coverData;
}
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
@@ -273,11 +275,6 @@ namespace LibationWinForms.ProcessQueue
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
new PictureDefinition(
libraryBook.Book.PictureId,
PictureSize._500x500));
title = libraryBook.Book.Title;
authorNames = libraryBook.Book.AuthorNames();
narratorNames = libraryBook.Book.NarratorNames();
@@ -286,7 +283,6 @@ namespace LibationWinForms.ProcessQueue
private async void Processable_Completed(object sender, LibraryBook libraryBook)
{
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable((Processable)sender);