Compare commits

...

13 Commits

Author SHA1 Message Date
Robert
cf571148bc incr ver 2025-11-25 23:19:49 -05:00
rmcrackan
2c2a720ba9 Merge pull request #1458 from Mbucari/master
Bug fixes in the downloader
2025-11-25 22:47:48 -05:00
Michael Bucari-Tovo
b577ef7187 Improve SetBeckupCounts
Change Avalonia's Task-based approach to WinForms' BackgroundWorker approach.
- Reduce number of calls to GetLibrary by adding the Library to the LibraryStats record.
2025-11-25 14:59:48 -07:00
Michael Bucari-Tovo
ffbb3c3516 Make namespace name match assembly name 2025-11-25 13:34:12 -07:00
Michael Bucari-Tovo
2a6cf38677 Fix book details dialog not saving 2025-11-25 13:33:40 -07:00
Michael Bucari-Tovo
d8104a4d7c Check is stream is disposed before reading position. 2025-11-25 12:34:20 -07:00
Michael Bucari-Tovo
af85ea9219 Fix exception being throw in Dispose() 2025-11-25 12:24:07 -07:00
rmcrackan
c30e149a36 Merge pull request #1456 from Mbucari/master
Fix database lock from -wal and -whm files
2025-11-24 22:57:12 -05:00
MBucari
050a4867b7 Fix database lock from -wal and -whm files
Delete LibationContext.db-shm and LibationContext.db-wal files as part of startup routine.
2025-11-24 20:45:55 -07:00
Robert
2bf6f7a4f2 incr ver 2025-11-24 15:46:01 -05:00
rmcrackan
788a768271 Merge pull request #1455 from Mbucari/master
Add liberate option `--license` to use license file.
2025-11-24 15:43:18 -05:00
Michael Bucari-Tovo
022a6e979d Add error handling for cookies 2025-11-24 12:01:10 -07:00
Michael Bucari-Tovo
9fc5a7d834 Add liberate option --license to use license file.
Added instructions for liberating using a license file.
2025-11-24 11:36:31 -07:00
16 changed files with 215 additions and 69 deletions

View File

@@ -101,6 +101,11 @@ libationcli liberate -p
libationcli liberate --force
libationcli liberate -f
```
#### Liberate using a license file from the `get-license` command
```console
libationcli liberate --license /path/to/license.lic
libationcli liberate --license - < /path/to/license.lic
```
#### List Libation Settings
```console
libationcli get-setting
@@ -153,7 +158,7 @@ foreach($q in $Qualities){
foreach($x in $xHE_AAC){
$license = ./libationcli get-license $asin --override FileDownloadQuality=$q --override Request_xHE_AAC=$x
echo $($license | ConvertFrom-Json).ContentMetadata.content_reference
echo $license | ./libationcli liberate --force
echo $license | ./libationcli liberate --force --license -
}
}
```

View File

@@ -26,7 +26,17 @@ namespace AaxDecrypter
protected string OutputDirectory { get; }
public IDownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => NfsPersister.NetworkFileStream;
protected virtual long InputFilePosition => InputFileStream.Position;
protected virtual long InputFilePosition
{
get
{
//Use try/catch instread of checking CanRead to avoid
//a race with the background download completing
//between the check and the Position call.
try { return InputFileStream.Position; }
catch { return InputFileStream.Length; }
}
}
private bool downloadFinished;
private NetworkFileStreamPersister? m_nfsPersister;

View File

@@ -209,6 +209,12 @@ namespace AaxDecrypter
}
}
}
catch (Exception ex)
{
//Don't throw from DownloadTask.
//This task gets awaited in Dispose() and we don't want to have an unhandled exception there.
Serilog.Log.Error(ex, "An error was encountered during the download process.");
}
finally
{
_writeFile.Dispose();

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>12.7.2.1</Version>
<Version>12.7.4.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />

View File

@@ -90,6 +90,7 @@ namespace AppScaffolding
{
config.LoadPersistentSettings(config.LibationFiles.SettingsFilePath);
}
DeleteOpenSqliteFiles(config);
AudibleApiStorage.EnsureAccountsSettingsFileExists();
//
@@ -102,6 +103,19 @@ namespace AppScaffolding
Migrations.migrate_to_v12_0_1(config);
}
/// <summary>
/// Delete shared memory and write-ahead log SQLite database files which may prevent access to the database.
/// </summary>
private static void DeleteOpenSqliteFiles(Configuration config)
{
var walFile = SqliteStorage.DatabasePath + "-wal";
var shmFile = SqliteStorage.DatabasePath + "-shm";
if (File.Exists(walFile))
FileManager.FileUtility.SaferDelete(walFile);
if (File.Exists(shmFile))
FileManager.FileUtility.SaferDelete(shmFile);
}
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
public static void RunPostMigrationScaffolding(Variety variety, Configuration config)
{

View File

@@ -586,7 +586,7 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable)
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable, IEnumerable<LibraryBook> LibraryBooks)
{
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
public bool HasPendingBooks => PendingBooks > 0;
@@ -655,7 +655,7 @@ namespace ApplicationServices
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = pdfResults.Count, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable });
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable);
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable, libraryBooks);
}
}
}

View File

@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Design;
namespace DataLayer.Postgres
namespace DataLayer.Sqlite
{
public class SqliteContextFactory : IDesignTimeDbContextFactory<LibationContext>
{

View File

@@ -73,8 +73,8 @@ namespace LibationAvalonia.Dialogs
}
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }

View File

@@ -3,6 +3,7 @@ using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Threading;
using Dinah.Core;
using LibationFileManager;
using LibationUiBase.Forms;
using System;
@@ -67,8 +68,17 @@ namespace LibationAvalonia.Dialogs.Login
{
if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager)
{
foreach (System.Net.Cookie c in shoiceIn.SignInCookies)
cookieManager.AddOrUpdateCookie(c);
foreach (System.Net.Cookie c in shoiceIn.SignInCookies ?? [])
{
try
{
cookieManager.AddOrUpdateCookie(c);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to set cookie {c.Name} for domain {c.Domain}");
}
}
}
//Set the source only after loading cookies
dialog.Source = new Uri(shoiceIn.LoginUrl);

View File

@@ -1,5 +1,4 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
using ReactiveUI;
@@ -11,7 +10,7 @@ namespace LibationAvalonia.ViewModels
{
partial class MainVM
{
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
@@ -46,20 +45,40 @@ namespace LibationAvalonia.ViewModels
//Pass null to the setup count to get the whole library.
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
=> await SetBackupCountsAsync(null);
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_Completed; ;
}
private bool runBackupCountsAgain;
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
{
if (updateCountsTask?.IsCompleted ?? true)
{
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
runBackupCountsAgain = true;
if (Configuration.Instance.AutoDownloadEpisodes
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
if (!updateCountsBw.IsBusy)
updateCountsBw.RunWorkerAsync(libraryBooks);
}
private void UpdateCountsBw_DoWork(object? sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable<LibraryBook>);
}
}
private void UpdateCountsBw_Completed(object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
if (e.Result is not LibraryCommands.LibraryStats stats)
return;
LibraryStats = stats;
if (Configuration.Instance.AutoDownloadEpisodes
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
BackupAllBooks(stats.LibraryBooks);
}
}
}

View File

@@ -7,6 +7,7 @@ using DataLayer;
using LibationUiBase.Forms;
using LibationUiBase;
using System.Collections.Generic;
using Avalonia.Threading;
#nullable enable
namespace LibationAvalonia.ViewModels
@@ -15,14 +16,24 @@ namespace LibationAvalonia.ViewModels
{
public void Configure_Liberate() { }
/// <summary> This gets called by the "Begin Book and PDF Backups" menu item. </summary>
public async Task BackupAllBooks()
{
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
BackupAllBooks(books);
}
private void BackupAllBooks(IEnumerable<LibraryBook> books)
{
try
{
var unliberated = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
var unliberated = books.UnLiberated().ToArray();
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
setQueueCollapseState(false);
Dispatcher.UIThread.Invoke(() =>
{
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
setQueueCollapseState(false);
});
}
catch (Exception ex)
{
@@ -30,9 +41,11 @@ namespace LibationAvalonia.ViewModels
}
}
/// <summary> This gets called by the "Begin PDF Only Backups" menu item. </summary>
public async Task BackupAllPdfs()
{
if (ProcessQueue.QueueDownloadPdf(await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking())))
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
if (ProcessQueue.QueueDownloadPdf(books))
setQueueCollapseState(false);
}

View File

@@ -23,6 +23,10 @@ namespace LibationCli
[Option(shortName: 'f', longName: "force", Required = false, Default = false, HelpText = "Force the book to re-download")]
public bool Force { get; set; }
[Option(shortName: 'l', longName: "license", Required = false, Default = null, HelpText = "A license file from the get-license command. Either a file path or dash ('-') to read from standard input.")]
public string? LicenseInput { get; set; }
protected override async Task ProcessAsync()
{
@@ -32,40 +36,9 @@ namespace LibationCli
return;
}
if (Console.IsInputRedirected)
if (LicenseInput is string licenseInput)
{
Console.WriteLine("Reading license file from standard input.");
using var reader = new StreamReader(Console.OpenStandardInput());
var stdIn = await reader.ReadToEndAsync();
try
{
var jsonSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
};
var licenseInfo = JsonConvert.DeserializeObject<DownloadOptions.LicenseInfo>(stdIn, jsonSettings);
if (licenseInfo?.ContentMetadata?.ContentReference?.Asin is not string asin)
{
Console.Error.WriteLine("Error: License file is missing ASIN information.");
return;
}
if (DbContexts.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook libraryBook)
{
Console.Error.WriteLine($"Book not found with asin={asin}");
return;
}
SetDownloadedStatus(libraryBook);
await ProcessOneAsync(GetProcessable(licenseInfo), libraryBook, true);
}
catch
{
Console.Error.WriteLine("Error: Failed to read license file from standard input. Please ensure the input is a valid license file in JSON format.");
}
await LiberateFromLicense(licenseInput);
}
else
{
@@ -73,6 +46,87 @@ namespace LibationCli
}
}
private async Task LiberateFromLicense(string licPath)
{
var licenseInfo = licPath is "-" ? ReadLicenseFromStdIn()
: ReadLicenseFromFile(licPath);
if (licenseInfo is null)
return;
if (licenseInfo?.ContentMetadata?.ContentReference?.Asin is not string asin)
{
Console.Error.WriteLine("Error: License file is missing ASIN information.");
return;
}
if (DbContexts.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook libraryBook)
{
Console.Error.WriteLine($"Book not found with asin={asin}");
return;
}
SetDownloadedStatus(libraryBook);
await ProcessOneAsync(GetProcessable(licenseInfo), libraryBook, true);
}
private static DownloadOptions.LicenseInfo? ReadLicenseFromFile(string licFile)
{
if (!File.Exists(licFile))
{
Console.Error.WriteLine("File does not exist: " + licFile);
return null;
}
Console.WriteLine("Reading license from file.");
try
{
var serializer = CreateLicenseInfoSerializer();
using var reader = new JsonTextReader(new StreamReader(licFile));
return serializer.Deserialize<DownloadOptions.LicenseInfo>(reader);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Failed to read license file: {@LicenseFile}", licFile);
Console.Error.WriteLine("Error: Failed to read license file. Please ensure the file is a valid license file in JSON format.");
}
return null;
}
private static DownloadOptions.LicenseInfo? ReadLicenseFromStdIn()
{
if (!Console.IsInputRedirected)
{
Console.Error.WriteLine("Ther is nothing in standard input to read.");
return null;
}
Console.WriteLine("Reading license from standard input.");
try
{
var serializer = CreateLicenseInfoSerializer();
using var reader = new JsonTextReader(new StreamReader(Console.OpenStandardInput()));
return serializer.Deserialize<DownloadOptions.LicenseInfo>(reader);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Failed to read license from standard input");
Console.Error.WriteLine("Error: Failed to read license file from standard input. Please ensure the input is a valid license file in JSON format.");
}
return null;
}
private static JsonSerializer CreateLicenseInfoSerializer()
{
var jsonSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
};
return JsonSerializer.Create(jsonSettings);
}
private Processable GetProcessable(DownloadOptions.LicenseInfo? licenseInfo = null)
=> PdfOnly ? CreateProcessable<DownloadPdf>() : CreateBackupBook(licenseInfo);

View File

@@ -5,7 +5,7 @@ namespace LibationFileManager
public static class SqliteStorage
{
// not customizable. don't move to config
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
public static string DatabasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
public static string ConnectionString => $"Data Source={DatabasePath};Foreign Keys=False;Pooling=False;";
}
}

View File

@@ -41,7 +41,14 @@ namespace LibationWinForms.Login
//Load init cookies
foreach (System.Net.Cookie cookie in choiceIn.SignInCookies ?? [])
{
webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie));
try
{
webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to set cookie {cookie.Name} for domain {cookie.Domain}");
}
}
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;

View File

@@ -1,6 +1,7 @@
using DataLayer;
using LibationUiBase;
using ApplicationServices;
using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -13,12 +14,21 @@ namespace LibationWinForms
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
{
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
BackupAllBooks(library);
}
private void BackupAllBooks(IEnumerable<LibraryBook> books)
{
try
{
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
SetQueueCollapseState(false);
var unliberated = books.UnLiberated().ToArray();
Invoke(() =>
{
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
SetQueueCollapseState(false);
});
}
catch (Exception ex)
{

View File

@@ -28,13 +28,11 @@ namespace LibationWinForms
// winforms only. this should NOT be allowed in cli
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
{
if (!Configuration.Instance.AutoDownloadEpisodes)
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
return;
var libraryStats = e.Result as LibraryCommands.LibraryStats;
if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0)
Invoke(() => beginBookBackupsToolStripMenuItem_Click(null, System.EventArgs.Empty));
BackupAllBooks(libraryStats.LibraryBooks);
};
}