Compare commits

...

22 Commits

Author SHA1 Message Date
Robert
946d4779a9 incr ver 2026-01-28 07:47:10 -05:00
Robert
e620d0be24 remove unneeded coalesce 2026-01-28 07:44:32 -05:00
rmcrackan
f720127a68 Merge pull request #1579 from rmcrackan/rmcrackan/bug-1578
Bug #1578 Add null checks
2026-01-27 13:22:20 -05:00
Robert
c6c3b39e9c Bug #1578 Adding null checks to the line that's logged with NRE. This bit of code has been untouched for years and is run hundreds of times per day. I'm documenting this suspiciously 2026-01-27 09:06:46 -05:00
rmcrackan
e70cc5342c Merge pull request #1572 from shuvashish76/patch-1
Add AppImage installation instructions
2026-01-23 09:45:12 -05:00
shuvashish76
bf0d380dd2 Add AppImage installation instructions
Added installation instructions for AppImage using AppMan and AM.
2026-01-23 14:41:46 +05:30
Robert
a43f25db23 incr ver 2026-01-22 17:50:31 -05:00
rmcrackan
e1f4168599 Merge pull request #1567 from Mbucari/master
Minor big fixes and performance improvements
2026-01-22 17:48:31 -05:00
Michael Bucari-Tovo
29501bddf3 Clean up queries
Use .NET 10 extensions
Refactor some extensions for clarity
2026-01-22 13:36:25 -07:00
Michael Bucari-Tovo
e9016ace03 Make IncludedUntil and LastDownload nulls always sort to bottom 2026-01-21 13:41:47 -07:00
Michael Bucari-Tovo
d498c094bf Update AAXClean
Some minor performance improvements, especially when opening a file/stream.
2026-01-21 13:40:59 -07:00
Michael Bucari-Tovo
ce92e79cd8 Fix SaferEnumerateFiles creating too manu open file handles (#1565)
Using the EnumerationOptions overload of EnumerateFiles() serves the same function as the previous manual implementation.
2026-01-21 10:34:59 -07:00
Michael Bucari-Tovo
f54a789ae8 Improve querying UnLiberated books
Add GetUnliberated_Flat_NoTracking() which queries only unliberated books/episodes and does not load the entire library.

Fix UnLiberated() query to only return products or episodes (not parents) (#1564)
2026-01-19 16:57:32 -07:00
Michael Bucari-Tovo
3ef0bce909 No reason to restrict CDM type to Android or Level to L3 2026-01-19 14:16:47 -07:00
Robert
413da72bf0 logging typo 2026-01-19 10:31:34 -05:00
Robert
db9c810c2d incr ver 2026-01-13 20:05:28 -05:00
rmcrackan
ae18ae1b8d Merge pull request #1558 from Mbucari/master
Fix export, remove spatial audio option, and add file move progress.
2026-01-13 19:56:24 -05:00
Michael Bucari-Tovo
c1298e9ff6 Update dependencies 2026-01-13 12:02:36 -07:00
Michael Bucari-Tovo
647eb8b9d9 Use MemoryPool 2026-01-13 11:35:59 -07:00
Michael Bucari-Tovo
fd64d394c2 Remove spatial audio options #1553 2026-01-13 09:43:51 -07:00
Michael Bucari-Tovo
f026d415bd Fix library export #1552 2026-01-13 09:35:08 -07:00
Michael Bucari-Tovo
3948e25c99 Add file move progress 2026-01-12 14:34:52 -07:00
29 changed files with 394 additions and 231 deletions

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="2.1.3.1" />
<PackageReference Include="AAXClean.Codecs" Version="2.1.3.2" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -5,8 +5,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" PrivateAssets="all" />
<PackageReference Include="ClosedXML" Version="0.105.0" PrivateAssets="all" />
<PackageReference Include="CsvHelper" Version="33.1.0">
<PrivateAssets>compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers;native</PrivateAssets>
</PackageReference>
<PackageReference Include="ClosedXML" Version="0.105.0">
<PrivateAssets>compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers;native</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -25,6 +25,12 @@ namespace ApplicationServices
return context.GetLibrary_Flat_NoTracking(includeParents);
}
public static List<LibraryBook> GetUnliberated_Flat_NoTracking()
{
using var context = GetContext();
return context.GetUnLiberated_Flat_NoTracking();
}
public static List<LibraryBook> GetDeletedLibraryBooks()
{
using var context = GetContext();

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="AudibleApi" Version="10.1.2.1" />
<PackageReference Include="Google.Protobuf" Version="3.33.3" />
<PackageReference Include="Google.Protobuf" Version="3.33.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -35,10 +35,6 @@ internal class Device
if (FileVersion != 2)
throw new InvalidDataException($"Unknown CDM File Version: '{FileVersion}'");
if (Type != DeviceTypes.Android)
throw new InvalidDataException($"Unknown CDM Type: '{Type}'");
if (SecurityLevel != 3)
throw new InvalidDataException($"Unknown CDM Security Level: '{SecurityLevel}'");
var privateKeyLength = (fileData[7] << 8) | fileData[8];

View File

@@ -9,11 +9,11 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -9,11 +9,11 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -13,12 +13,12 @@
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="10.0.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,116 +1,131 @@
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
#nullable enable
namespace DataLayer
namespace DataLayer;
// only library importing should use tracking. All else should be NoTracking.
// only library importing should directly query Book. All else should use LibraryBook
public static class LibraryBookQueries
{
// only library importing should use tracking. All else should be NoTracking.
// only library importing should directly query Book. All else should use LibraryBook
public static class LibraryBookQueries
private static System.Linq.Expressions.Expression<System.Func<LibraryBook, bool>> IsUnLiberatedExpression { get; }
= lb =>
!lb.AbsentFromLastScan &&
(lb.Book.ContentType == ContentType.Product || lb.Book.ContentType == ContentType.Episode) &&
(lb.Book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.PartialDownload);
extension(LibationContext context)
{
//// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid
//// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid
//public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
// => context
// .Library
// .GetLibrary()
// .ToList();
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context, bool includeParents = false)
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.AsEnumerable()
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList();
public List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.Where(lb => lb.Book.ContentType != ContentType.Parent || includeParents)
.AsEnumerable()
.ToList();
public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId, bool caseSensative = true)
{
var libraryQuery
= context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary();
public LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true)
{
var libraryQuery
= context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary();
return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId)
: libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId);
return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId)
: libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId);
}
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)
=> library
.Where(lb => !lb.IsDeleted)
.getLibrary();
public List<LibraryBook> GetUnLiberated_Flat_NoTracking()
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.Where(IsUnLiberatedExpression)
.AsEnumerable()
.ToList();
public static List<LibraryBook> GetDeletedLibraryBooks(this LibationContext context)
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
//Return all parents so the trash bin grid can show podcasts beneath their parents
.Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent)
.getLibrary()
.ToList();
public List<LibraryBook> GetDeletedLibraryBooks()
=> context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
//Return all parents so the trash bin grid can show podcasts beneath their parents
.Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent)
.getLibrary()
.ToList();
}
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
private static IQueryable<LibraryBook> getLibrary(this IQueryable<LibraryBook> library)
=> library
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
extension(IQueryable<LibraryBook> library)
{
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public IQueryable<LibraryBook> GetLibrary()
=> library.Where(lb => !lb.IsDeleted).getLibrary();
public static IEnumerable<LibraryBook> ParentedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren);
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
private IQueryable<LibraryBook> getLibrary()
=> library
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
}
public static IEnumerable<LibraryBook> FindOrphanedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks
.Where(lb => lb.Book.IsEpisodeChild())
.ExceptBy(
libraryBooks
.ParentedEpisodes()
.Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId);
extension(IEnumerable<LibraryBook> libraryBooks)
{
public IEnumerable<LibraryBook> UnLiberated()
=> libraryBooks.Where(lb => lb.NeedsPdfDownload || lb.NeedsBookDownload);
#nullable enable
public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode)
{
if (seriesEpisode.Book.SeriesLink is null) return null;
public IEnumerable<LibraryBook> ParentedEpisodes()
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren);
try
{
//Parent books will always have exactly 1 SeriesBook due to how
//they are imported in ApiExtended.getChildEpisodesAsync()
return libraryBooks.FirstOrDefault(
lb =>
lb.Book.IsEpisodeParent() &&
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (System.Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
}
#nullable disable
public IEnumerable<LibraryBook> FindOrphanedEpisodes()
=> libraryBooks
.Where(lb => lb.Book.IsEpisodeChild())
.ExceptBy(
libraryBooks
.ParentedEpisodes()
.Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId);
public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
=> bookList
.Where(
lb =>
lb.Book.IsEpisodeChild() &&
lb.Book.SeriesLink?
.Any(
s =>
s.Series.AudibleSeriesId == parent.Book.AudibleProductId
) == true
).ToList();
public IEnumerable<LibraryBook> FindChildren(LibraryBook parent)
=> libraryBooks.Where(lb => lb.Book.IsEpisodeChild() && lb.HasSeriesId(parent.Book.AudibleProductId));
public static bool NeedsPdfDownload(this LibraryBook libraryBook)
=> !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated;
public static bool NeedsBookDownload(this LibraryBook libraryBook)
=> !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload;
public static IEnumerable<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> bookList)
=> bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload());
}
public LibraryBook? FindSeriesParent(LibraryBook seriesEpisode)
{
if (seriesEpisode.Book.SeriesLink is null) return null;
try
{
//Parent books will always have exactly 1 SeriesBook due to how
//they are imported in ApiExtended.getChildEpisodesAsync()
return libraryBooks.FirstOrDefault(
lb =>
lb.Book.IsEpisodeParent() &&
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (System.Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
}
}
extension(LibraryBook libraryBook)
{
public bool HasSeriesId(string audibleSeriesId) => libraryBook.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId.EqualsInsensitive(audibleSeriesId)) is true;
public bool Downloadable => !libraryBook.AbsentFromLastScan && libraryBook.Book.ContentType is ContentType.Product or ContentType.Episode;
public bool NeedsPdfDownload => libraryBook.Downloadable && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated;
public bool NeedsBookDownload => libraryBook.Downloadable && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload;
}
}

View File

@@ -334,13 +334,15 @@ namespace FileLiberator
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
private void MoveFilesToBooksDir(LibraryBook libraryBook, LongPath destinationDir, List<TempFile> entries, CancellationToken cancellationToken)
private async Task MoveFilesToBooksDir(LibraryBook libraryBook, LongPath destinationDir, List<TempFile> entries, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
AverageSpeed averageSpeed = new();
MoveWithProgress moveWithProgress = new();
var totalSizeToMove = entries.Sum(f => new FileInfo(f.FilePath).Length);
long totalBytesMoved = 0;
moveWithProgress.MoveProgress += onMovefileProgress;
for (var i = 0; i < entries.Count; i++)
{
@@ -355,28 +357,13 @@ namespace FileLiberator
Configuration.OverwriteExisting);
var realDest
= FileUtility.SaferMoveToValidPath(
entry.FilePath,
= FileUtility.GetValidFilename(
destFileName,
Configuration.ReplacementCharacters,
entry.Extension,
Configuration.OverwriteExisting);
#region File Move Progress
totalBytesMoved += new FileInfo(realDest).Length;
averageSpeed.AddPosition(totalBytesMoved);
var estSecsRemaining = (totalSizeToMove - totalBytesMoved) / averageSpeed.Average;
if (double.IsNormal(estSecsRemaining))
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estSecsRemaining));
OnStreamingProgressChanged(new DownloadProgress
{
ProgressPercentage = 100d * totalBytesMoved / totalSizeToMove,
BytesReceived = totalBytesMoved,
TotalBytesToReceive = totalSizeToMove
});
#endregion
await moveWithProgress.MoveAsync(entry.FilePath, realDest, Configuration.OverwriteExisting, cancellationToken);
// propagate corrected path for cue file (after this for-loop)
entries[i] = entry with { FilePath = realDest };
@@ -395,6 +382,23 @@ namespace FileLiberator
cancellationToken.ThrowIfCancellationRequested();
AudibleFileStorage.Audio.Refresh();
void onMovefileProgress(object? sender, MoveFileProgressEventArgs e)
{
totalBytesMoved += e.BytesMoved;
averageSpeed.AddPosition(totalBytesMoved);
var estSecsRemaining = (totalSizeToMove - totalBytesMoved) / averageSpeed.Average;
if (double.IsNormal(estSecsRemaining))
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estSecsRemaining));
OnStreamingProgressChanged(new DownloadProgress
{
ProgressPercentage = 100d * totalBytesMoved / totalSizeToMove,
BytesReceived = totalBytesMoved,
TotalBytesToReceive = totalSizeToMove
});
};
}
private void DownloadCoverArt(LongPath destinationDir, DownloadOptions options, CancellationToken cancellationToken)

View File

@@ -244,27 +244,14 @@ namespace FileManager
/// <returns>List of files</returns>
public static IEnumerable<LongPath> SaferEnumerateFiles(LongPath path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
var foundFiles = Enumerable.Empty<LongPath>();
try
{
if (searchOption == SearchOption.AllDirectories)
{
IEnumerable<LongPath> subDirs = Directory.EnumerateDirectories(path).Select(p => (LongPath)p);
// Add files in subdirectories recursively to the list
foreach (string dir in subDirs)
foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
}
// Add files from the current directory
foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern).Select(f => (LongPath)f));
}
catch (UnauthorizedAccessException) { }
catch (PathTooLongException) { }
// Symbolic links will result in DirectoryNotFoundException. Ohter logical directories might also. Just skip them. Don't want to risk (or have to handle) infinite recursion
catch (DirectoryNotFoundException) { }
return foundFiles;
var enumOptions = new EnumerationOptions
{
RecurseSubdirectories = searchOption == SearchOption.AllDirectories,
IgnoreInaccessible = true,
ReturnSpecialDirectories = false,
MatchType = MatchType.Simple
};
return Directory.EnumerateFiles(path.Path, searchPattern, enumOptions).Select(p => (LongPath) p);
}
/// <summary>

View File

@@ -0,0 +1,184 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Serilog;
#nullable enable
namespace FileManager;
public class MoveFileProgressEventArgs : EventArgs
{
public long TotalFileSize { get; }
public long TotalBytesTransferred { get; }
public long BytesMoved { get; }
public bool Continue { get; set; } = true;
internal MoveFileProgressEventArgs(long bytesMoved, long totalBytesTransferred, long totalFileSize)
{
BytesMoved = bytesMoved;
TotalBytesTransferred = totalBytesTransferred;
TotalFileSize = totalFileSize;
}
}
public class MoveWithProgress
{
public event EventHandler<MoveFileProgressEventArgs>? MoveProgress;
public async Task<bool> MoveAsync(LongPath source, LongPath destination, bool overwrite = false, CancellationToken cancellation = default)
{
ArgumentException.ThrowIfNullOrEmpty(source, nameof(source));
ArgumentException.ThrowIfNullOrEmpty(destination, nameof(destination));
var sourceFileInfo = new FileInfo(source);
if (!sourceFileInfo.Exists)
throw new FileNotFoundException($"Source file '{source}' does not exist.", source);
var destinationFile = new FileInfo(destination);
var sourceDevice = GetDeviceId(sourceFileInfo);
var destinationDevice = GetDeviceId(destinationFile.Directory);
if (sourceDevice == destinationDevice)
{
File.Move(sourceFileInfo.FullName, destinationFile.FullName, overwrite);
MoveProgress?.Invoke(this, new MoveFileProgressEventArgs(destinationFile.Length, destinationFile.Length, sourceFileInfo.Length));
return true;
}
if (destinationFile.Exists && !overwrite)
throw new IOException("The file exists.");
bool success = false;
try
{
success = await CopyWithProgressAsync(sourceFileInfo, destinationFile, cancellation);
}
finally
{
if (success)
FileUtility.SaferDelete(sourceFileInfo.FullName);
else
FileUtility.SaferDelete(destinationFile.FullName);
}
return success;
}
private static string? GetDeviceId(FileSystemInfo? fsEntry)
=> fsEntry?.FullName is not string path ? null
: LongPath.IsWindows ? GetDriveSerialNumber(path)
: LongPath.IsOSX ? RunShellCommand("stat -L -f %d \"" + path + "\"")
: RunShellCommand("stat -L -f -c %d \"" + path + "\"");
private async Task<bool> CopyWithProgressAsync(FileInfo sourceFileInfo, FileInfo destinationFile, CancellationToken cancellation)
{
const int BlockSizeMb = 8;
const int BlockSizeBytes = BlockSizeMb * (1 << 20);
using FileStream sourceStream = sourceFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using FileStream destinationStream = destinationFile.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
using IMemoryOwner<byte> pool = MemoryPool<byte>.Shared.Rent(2 * BlockSizeBytes);
Memory<byte> readBuff = pool.Memory.Slice(0, BlockSizeBytes);
Memory<byte> writeBuff = pool.Memory.Slice(BlockSizeBytes, BlockSizeBytes);
long totalCopied = 0, bytesMovedSinceLastReport = 0;
DateTime nextReport = default;
int bytesRead = await sourceStream.ReadAsync(writeBuff, cancellation);
while (bytesRead > 0)
{
totalCopied += bytesRead;
bytesMovedSinceLastReport += bytesRead;
var readTask = sourceStream.ReadAsync(readBuff, cancellation);
await destinationStream.WriteAsync(writeBuff[..bytesRead], cancellation);
if (DateTime.UtcNow >= nextReport)
{
var args = new MoveFileProgressEventArgs(bytesMovedSinceLastReport, totalCopied, sourceFileInfo.Length);
bytesMovedSinceLastReport = 0;
MoveProgress?.Invoke(this, args);
if (!args.Continue)
break;
nextReport = DateTime.UtcNow.AddMilliseconds(200.0);
}
bytesRead = await readTask;
(readBuff, writeBuff) = (writeBuff, readBuff);
}
destinationStream.SetLength(totalCopied);
MoveProgress?.Invoke(this, new MoveFileProgressEventArgs(bytesMovedSinceLastReport, totalCopied, sourceFileInfo.Length));
return totalCopied == sourceFileInfo.Length;
}
private static string? RunShellCommand(string command)
{
var psi = new ProcessStartInfo
{
FileName = "/bin/sh",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
ArgumentList = { "-c", command }
};
try
{
var proc = Process.Start(psi);
proc?.WaitForExit();
return proc?.StandardOutput?.ReadToEnd()?.Trim();
}
catch (Exception e)
{
Log.Logger.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
return null;
}
}
private static string? GetDriveSerialNumber(string path)
{
const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
const uint OPEN_EXISTING = 3;
var handle = CreateFile(path, FileAccess.Read, FileShare.Read, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
if (handle.IsInvalid)
return null;
try
{
BY_HANDLE_FILE_INFORMATION info = default;
if (!GetFileInformationByHandle(handle, ref info))
{
return null;
}
return info.dwVolumeSerialNumber.ToString("x8");
}
finally
{
handle.Close();
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetFileInformationByHandle(SafeFileHandle hFile, ref BY_HANDLE_FILE_INFORMATION lpFileInformation);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string fileName, FileAccess fileAccess, FileShare fileShare, nint lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, nint hTemplateFile);
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct BY_HANDLE_FILE_INFORMATION
{
private uint dwFileAttributes;
private long ftCreationTime;
private long ftLastAccessTime;
private long ftLastWriteTime;
public uint dwVolumeSerialNumber;
private uint nFileSizeHigh;
private uint nFileSizeLow;
private uint nNumberOfLinks;
private uint nFileIndexHigh;
private uint nFileIndexLow;
}
}

View File

@@ -70,11 +70,11 @@
<TrimmableAssembly Include="Avalonia.Themes.Default" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.10" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.11" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@@ -63,7 +63,7 @@
<TextBlock Text="{Binding Request_xHE_AACText}" />
</CheckBox>
</Grid>
<!--
<Grid ColumnDefinitions="*,Auto">
<CheckBox
ToolTip.Tip="{Binding RequestSpatialTip}"
@@ -95,9 +95,9 @@
VerticalAlignment="Center"
ItemsSource="{Binding SpatialAudioCodecs}"
SelectedItem="{Binding SpatialAudioCodec}"/>
</Grid>
</Grid>
</Grid>
-->
<CheckBox IsChecked="{Binding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{Binding CreateCueSheetText}" />

View File

@@ -73,12 +73,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.11" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.11" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
<PackageReference Include="WebViewControlAvaloniaFree" Version="11.3.15" />
</ItemGroup>

View File

@@ -17,7 +17,7 @@ namespace LibationAvalonia.ViewModels
/// <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());
var books = await Task.Run(DbContexts.GetUnliberated_Flat_NoTracking);
BackupAllBooks(books);
}

View File

@@ -98,8 +98,8 @@ namespace LibationFileManager
Some audiobooks are only delivered in the highest
available quality with special, third-party content
protection. Enabling this option will allows you to
request audiobooks in the xHE-AAC codec and in
spatial (Dolby Atmos) audio formats.
request audiobooks in the xHE-AAC codec, which is
often higher quality than the standard AAC-LC codec.
""" },
{nameof(Request_xHE_AAC), """
If selected, Libation will request audiobooks in the

View File

@@ -306,10 +306,10 @@ namespace LibationFileManager
[Description("Request xHE-AAC codec")]
public bool Request_xHE_AAC { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Request Spatial Audio")]
public bool RequestSpatial { get => GetNonString(defaultValue: true); set => SetNonString(value); }
//[Description("Request Spatial Audio")]
public bool RequestSpatial { get => false; set { } } // { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Spatial audio codec:")]
//[Description("Spatial audio codec:")]
public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); }
[Description("Audio quality to request from Audible:")]

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
<PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
</ItemGroup>

View File

@@ -221,6 +221,8 @@ namespace LibationUiBase.GridView
nameof(Category) => string.IsNullOrWhiteSpace(Category),
nameof(Misc) => string.IsNullOrWhiteSpace(Misc),
nameof(BookTags) => string.IsNullOrWhiteSpace(BookTags),
nameof(IncludedUntil) => string.IsNullOrWhiteSpace(IncludedUntil),
nameof(LastDownload) => LastDownload?.IsValid is not true,
_ => false
};

View File

@@ -151,7 +151,7 @@ public class ProcessBookViewModel : ReactiveObject
}
catch (ContentLicenseDeniedException ldex)
{
Serilog.Log.Logger.Error(ldex, "Content license was denied for {#Book}", LibraryBook.LogFriendly());
Serilog.Log.Logger.Error(ldex, "Content license was denied for {Book}", LibraryBook.LogFriendly());
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
{
LogInfo($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");

View File

@@ -96,7 +96,7 @@ public class ProcessQueueViewModel : ReactiveObject
if (!IsBooksDirectoryValid(config))
return false;
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload).ToArray();
if (needsPdf.Length > 0)
{
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
@@ -137,14 +137,14 @@ public class ProcessQueueViewModel : ReactiveObject
if (item.AbsentFromLastScan)
return false;
else if (item.NeedsBookDownload())
else if (item.NeedsBookDownload)
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
AddDownloadDecrypt([item], config);
return true;
}
else if (item.NeedsPdfDownload())
else if (item.NeedsPdfDownload)
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);

View File

@@ -25,7 +25,6 @@ namespace LibationWinForms.Dialogs
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
this.useWidevineCbox.Text = desc(nameof(config.UseWidevine));
this.requestSpatialCbox.Text = desc(nameof(config.RequestSpatial));
this.request_xHE_AAC_Cbox.Text = desc(nameof(config.Request_xHE_AAC));
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
@@ -38,9 +37,7 @@ namespace LibationWinForms.Dialogs
toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile)));
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio)));
toolTip.SetToolTip(useWidevineCbox, Configuration.GetHelpText(nameof(config.UseWidevine)));
toolTip.SetToolTip(requestSpatialCbox, Configuration.GetHelpText(nameof(config.RequestSpatial)));
toolTip.SetToolTip(request_xHE_AAC_Cbox, Configuration.GetHelpText(nameof(config.Request_xHE_AAC)));
toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
toolTip.SetToolTip(minFileDurationLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
toolTip.SetToolTip(minFileDurationNud, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
@@ -50,12 +47,6 @@ namespace LibationWinForms.Dialogs
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
]);
spatialAudioCodecCb.Items.AddRange(
[
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"),
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.AC_4, "Dolby AC-4")
]);
clipsBookmarksFormatCb.Items.AddRange(
[
Configuration.ClipBookmarkFormat.CSV,
@@ -81,10 +72,8 @@ namespace LibationWinForms.Dialogs
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec;
useWidevineCbox.Checked = config.UseWidevine;
request_xHE_AAC_Cbox.Checked = config.Request_xHE_AAC;
requestSpatialCbox.Checked = config.RequestSpatial;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile;
@@ -130,8 +119,6 @@ namespace LibationWinForms.Dialogs
config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value;
config.UseWidevine = useWidevineCbox.Checked;
config.Request_xHE_AAC = request_xHE_AAC_Cbox.Checked;
config.RequestSpatial = requestSpatialCbox.Checked;
config.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked;
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
@@ -204,11 +191,6 @@ namespace LibationWinForms.Dialogs
}
}
private void requestSpatialCbox_CheckedChanged(object sender, EventArgs e)
{
spatialAudioCodecCb.Enabled = requestSpatialCbox.Checked && useWidevineCbox.Checked;
}
private void useWidevineCbox_CheckedChanged(object sender, EventArgs e)
{
if (useWidevineCbox.Checked)
@@ -246,11 +228,10 @@ namespace LibationWinForms.Dialogs
}
else
{
requestSpatialCbox.Checked = request_xHE_AAC_Cbox.Checked = false;
request_xHE_AAC_Cbox.Checked = false;
}
requestSpatialCbox.Enabled = request_xHE_AAC_Cbox.Enabled = useWidevineCbox.Checked;
requestSpatialCbox_CheckedChanged(sender, e);
request_xHE_AAC_Cbox.Enabled = useWidevineCbox.Checked;
}
}
}

View File

@@ -89,9 +89,7 @@
folderTemplateLbl = new System.Windows.Forms.Label();
tab4AudioFileOptions = new System.Windows.Forms.TabPage();
request_xHE_AAC_Cbox = new System.Windows.Forms.CheckBox();
requestSpatialCbox = new System.Windows.Forms.CheckBox();
useWidevineCbox = new System.Windows.Forms.CheckBox();
spatialAudioCodecCb = new System.Windows.Forms.ComboBox();
moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
fileDownloadQualityCb = new System.Windows.Forms.ComboBox();
fileDownloadQualityLbl = new System.Windows.Forms.Label();
@@ -315,7 +313,7 @@
allowLibationFixupCbox.AutoSize = true;
allowLibationFixupCbox.Checked = true;
allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
allowLibationFixupCbox.Location = new System.Drawing.Point(19, 230);
allowLibationFixupCbox.Location = new System.Drawing.Point(19, 204);
allowLibationFixupCbox.Name = "allowLibationFixupCbox";
allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19);
allowLibationFixupCbox.TabIndex = 13;
@@ -826,9 +824,7 @@
tab4AudioFileOptions.AutoScroll = true;
tab4AudioFileOptions.BackColor = System.Drawing.SystemColors.Window;
tab4AudioFileOptions.Controls.Add(request_xHE_AAC_Cbox);
tab4AudioFileOptions.Controls.Add(requestSpatialCbox);
tab4AudioFileOptions.Controls.Add(useWidevineCbox);
tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb);
tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityCb);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityLbl);
@@ -865,19 +861,6 @@
request_xHE_AAC_Cbox.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
request_xHE_AAC_Cbox.UseVisualStyleBackColor = true;
//
// requestSpatialCbox
//
requestSpatialCbox.AutoSize = true;
requestSpatialCbox.Checked = true;
requestSpatialCbox.CheckState = System.Windows.Forms.CheckState.Checked;
requestSpatialCbox.Location = new System.Drawing.Point(19, 60);
requestSpatialCbox.Name = "requestSpatialCbox";
requestSpatialCbox.Size = new System.Drawing.Size(138, 19);
requestSpatialCbox.TabIndex = 4;
requestSpatialCbox.Text = "[RequestSpatial desc]";
requestSpatialCbox.UseVisualStyleBackColor = true;
requestSpatialCbox.CheckedChanged += requestSpatialCbox_CheckedChanged;
//
// useWidevineCbox
//
useWidevineCbox.AutoSize = true;
@@ -891,16 +874,6 @@
useWidevineCbox.UseVisualStyleBackColor = true;
useWidevineCbox.CheckedChanged += useWidevineCbox_CheckedChanged;
//
// spatialAudioCodecCb
//
spatialAudioCodecCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
spatialAudioCodecCb.FormattingEnabled = true;
spatialAudioCodecCb.Location = new System.Drawing.Point(249, 60);
spatialAudioCodecCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3);
spatialAudioCodecCb.Name = "spatialAudioCodecCb";
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
spatialAudioCodecCb.TabIndex = 5;
//
// moveMoovAtomCbox
//
moveMoovAtomCbox.AutoSize = true;
@@ -934,7 +907,7 @@
// combineNestedChapterTitlesCbox
//
combineNestedChapterTitlesCbox.AutoSize = true;
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 206);
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 180);
combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox";
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19);
combineNestedChapterTitlesCbox.TabIndex = 12;
@@ -945,7 +918,7 @@
//
clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
clipsBookmarksFormatCb.FormattingEnabled = true;
clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 132);
clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 106);
clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb";
clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23);
clipsBookmarksFormatCb.TabIndex = 9;
@@ -953,7 +926,7 @@
// downloadClipsBookmarksCbox
//
downloadClipsBookmarksCbox.AutoSize = true;
downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 134);
downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 108);
downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox";
downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19);
downloadClipsBookmarksCbox.TabIndex = 8;
@@ -968,9 +941,9 @@
audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox);
audiobookFixupsGb.Controls.Add(stripUnabridgedCbox);
audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox);
audiobookFixupsGb.Location = new System.Drawing.Point(6, 254);
audiobookFixupsGb.Location = new System.Drawing.Point(6, 229);
audiobookFixupsGb.Name = "audiobookFixupsGb";
audiobookFixupsGb.Size = new System.Drawing.Size(416, 128);
audiobookFixupsGb.Size = new System.Drawing.Size(416, 153);
audiobookFixupsGb.TabIndex = 14;
audiobookFixupsGb.TabStop = false;
audiobookFixupsGb.Text = "Audiobook Fix-ups";
@@ -1438,7 +1411,7 @@
// mergeOpeningEndCreditsCbox
//
mergeOpeningEndCreditsCbox.AutoSize = true;
mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 182);
mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 156);
mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox";
mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19);
mergeOpeningEndCreditsCbox.TabIndex = 11;
@@ -1448,7 +1421,7 @@
// retainAaxFileCbox
//
retainAaxFileCbox.AutoSize = true;
retainAaxFileCbox.Location = new System.Drawing.Point(19, 158);
retainAaxFileCbox.Location = new System.Drawing.Point(19, 132);
retainAaxFileCbox.Name = "retainAaxFileCbox";
retainAaxFileCbox.Size = new System.Drawing.Size(131, 19);
retainAaxFileCbox.TabIndex = 10;
@@ -1461,7 +1434,7 @@
downloadCoverArtCbox.AutoSize = true;
downloadCoverArtCbox.Checked = true;
downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked;
downloadCoverArtCbox.Location = new System.Drawing.Point(19, 110);
downloadCoverArtCbox.Location = new System.Drawing.Point(19, 84);
downloadCoverArtCbox.Name = "downloadCoverArtCbox";
downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19);
downloadCoverArtCbox.TabIndex = 7;
@@ -1474,7 +1447,7 @@
createCueSheetCbox.AutoSize = true;
createCueSheetCbox.Checked = true;
createCueSheetCbox.CheckState = System.Windows.Forms.CheckState.Checked;
createCueSheetCbox.Location = new System.Drawing.Point(19, 86);
createCueSheetCbox.Location = new System.Drawing.Point(19, 60);
createCueSheetCbox.Name = "createCueSheetCbox";
createCueSheetCbox.Size = new System.Drawing.Size(145, 19);
createCueSheetCbox.TabIndex = 6;
@@ -1642,9 +1615,7 @@
private System.Windows.Forms.Label gridFontScaleFactorLbl;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.Button applyDisplaySettingsBtn;
private System.Windows.Forms.ComboBox spatialAudioCodecCb;
private System.Windows.Forms.CheckBox useWidevineCbox;
private System.Windows.Forms.CheckBox requestSpatialCbox;
private System.Windows.Forms.CheckBox request_xHE_AAC_Cbox;
private DirectoryOrCustomSelectControl inProgressSelectControl;
private DirectoryOrCustomSelectControl booksSelectControl;

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Microsoft ResX Schema
Version 2.0
@@ -48,7 +48,7 @@
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter

View File

@@ -15,7 +15,7 @@ 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());
var library = await Task.Run(DbContexts.GetUnliberated_Flat_NoTracking);
BackupAllBooks(library);
}

View File

@@ -57,7 +57,7 @@ namespace LibationWinForms
private void productsDisplay_InitialLoaded(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
performFilter(QuickFilters.Filters.FirstOrDefault().Filter);
performFilter(QuickFilters.Filters.FirstOrDefault()?.Filter);
}
}
}

View File

@@ -31,6 +31,18 @@ sudo dnf5 install ./libation.rpm
---
### AppImage
- Install via [AppMan](https://github.com/ivan-hc/AppMan) (rootless)
```bash
appman -i libation
```
- Install via [AM](https://github.com/ivan-hc/AM)
```bash
am -i libation
```
Thanks to Package Forge dev [Samuel](https://github.com/Samueru-sama) for [AppImage](https://github.com/pkgforge-dev/Libation-AppImage) maintenence.
### Arch Linux
```bash
@@ -76,6 +88,7 @@ Pacstall is the AUR Ubuntu wishes it had. It takes the concept of the AUR and pu
```bash
pacstall -I libation-deb
```
Thanks to [Tobias Heinlein](https://github.com/niontrix) for Pacstall package maintenance.
---