Compare commits

..

36 Commits

Author SHA1 Message Date
rmcrackan
f00951d615 Update config.js
"Edit this page" went to 'main' instead of 'master'
2026-02-03 10:07:17 -05:00
Robert
c7e844a54c incr ver 2026-02-03 09:38:30 -05:00
rmcrackan
178cc82d65 Merge pull request #1587 from Mbucari/master
Documentation and bug fixes
2026-02-03 09:05:06 -05:00
Michael Bucari-Tovo
054f7437d1 Fix importer NRE 2026-02-02 16:02:44 -07:00
Mbucari
99051b6975 Merge branch 'rmcrackan:master' into master 2026-01-31 23:52:05 -07:00
Robert
eed7f1811b incr ver 2026-01-31 23:33:29 -05:00
rmcrackan
33cd6b8639 Merge pull request #1585 from thoughtbox/patch-2
Fix typos
2026-01-31 23:31:16 -05:00
rmcrackan
06014f467e Merge pull request #1581 from scagood/patch-1
Remove progress bar in non interactive mode (#1481)
2026-01-31 23:29:04 -05:00
Tor Houghton
b2eef18217 Fix typos
buit-in -> built-in
broswer -> browser
2026-01-31 22:19:53 +01:00
Robert
443d1f64ca incr ver 2026-01-31 12:36:46 -05:00
Robert
c43e88d269 verbose null checking to debug #1578 2026-01-31 12:36:29 -05:00
Michael Bucari-Tovo
dc1919f411 Update AAXClean to 3.0.2 and fix deprecation warnings 2026-01-30 14:46:11 -07:00
Mbucari
fdfae2f806 Merge branch 'rmcrackan:master' into master 2026-01-30 14:42:13 -07:00
Sebastian Good
5c56e1d39b Correct small spelling mistake 2026-01-30 16:12:48 +00:00
Sebastian Good
b39e2c3e0b Remove progress bar in non interactive mode (#1481) 2026-01-30 16:12:44 +00:00
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
Mbucari
d58f5abe35 Add Troubleshooting link to Advanced topics
Added 'Troubleshooting' link to the Advanced section.
2026-01-27 14:58:37 -07:00
Mbucari
d957d6d5d7 Update troubleshooting guide with Hangover app details
Added troubleshooting information for the Hangover app and SQLite Error 10.
2026-01-27 14:53:08 -07:00
Michael Bucari-Tovo
cb159336a6 Enable NTRs on main form and fix resulting warnings. 2026-01-27 12:42:21 -07:00
Michael Bucari-Tovo
807bb56c49 Refactor Chardonnay startup to prevent loading library twice. 2026-01-27 12:31:17 -07:00
Mbucari
3d56554aa5 Merge branch 'rmcrackan:master' into master 2026-01-27 12:29:03 -07: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
Michael Bucari-Tovo
471fd1e757 Remove unwanted meta box from dash files 2026-01-22 16:33:12 -07:00
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
47 changed files with 346 additions and 250 deletions

View File

@@ -19,7 +19,7 @@ export default defineConfig({
},
editLink: {
pattern: "https://github.com/rmcrackan/Libation/edit/main/:path",
pattern: "https://github.com/rmcrackan/Libation/edit/master/:path",
},
lastUpdated: true,
@@ -77,7 +77,10 @@ export default defineConfig({
{
text: "Advanced",
collapsed: false,
items: [{ text: "Advanced Topics", link: "/docs/advanced/advanced" }],
items: [
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
{ text: "Troubleshooting", link: "/docs/advanced/troubleshoot" }
],
},
{
text: "Development",

View File

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

View File

@@ -1,4 +1,5 @@
using AAXClean;
using Mpeg4Lib;
using System;
using System.IO;
using System.Linq;
@@ -9,7 +10,7 @@ namespace AaxDecrypter
{
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
{
public event EventHandler<AppleTags>? RetrievedMetadata;
public event EventHandler<MetadataItems>? RetrievedMetadata;
public Mp4File? AaxFile { get; private set; }
protected Mp4Operation? AaxConversion { get; set; }
@@ -21,8 +22,8 @@ namespace AaxDecrypter
public override void SetCoverArt(byte[] coverArt)
{
base.SetCoverArt(coverArt);
if (coverArt is not null && AaxFile?.AppleTags is not null)
AaxFile.AppleTags.Cover = coverArt;
if (coverArt is not null && AaxFile?.MetadataItems is not null)
AaxFile.MetadataItems.Cover = coverArt;
}
public override async Task CancelAsync()
@@ -42,8 +43,10 @@ namespace AaxDecrypter
var keyIds = keys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
var dash = new DashFile(InputFileStream);
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
if (dash.Tenc is null)
throw new InvalidOperationException("The DASH file does not contain 'tenc' box, indicating that it is unencrypted.");
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
if (kidIndex == -1)
throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}");
@@ -52,6 +55,11 @@ namespace AaxDecrypter
var key = keys[kidIndex].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null decryption key (KeyPart2).");
dash.SetDecryptionKey(keyId, key);
WriteKeyFile($"KeyId={Convert.ToHexString(keyId)}{Environment.NewLine}Key={Convert.ToHexString(key)}");
//Remove meta box containing DRM info
if (DownloadOptions.FixupFile && dash.Moov.GetChild<Mpeg4Lib.Boxes.MetaBox>() is { } meta)
dash.Moov.Children.Remove(meta);
return dash;
}
else if (DownloadOptions.InputType is FileType.Aax)
@@ -85,55 +93,55 @@ namespace AaxDecrypter
{
AaxFile = Open();
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
RetrievedMetadata?.Invoke(this, AaxFile.MetadataItems);
if (DownloadOptions.StripUnabridged)
{
AaxFile.AppleTags.Title = AaxFile.AppleTags.TitleSansUnabridged;
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
AaxFile.MetadataItems.Title = AaxFile.MetadataItems.TitleSansUnabridged;
AaxFile.MetadataItems.Album = AaxFile.MetadataItems.Album?.Replace(" (Unabridged)", "");
}
if (DownloadOptions.FixupFile)
{
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
AaxFile.AppleTags.AppleListBox.EditOrAddTag("©wrt", AaxFile.AppleTags.Narrator);
if (!string.IsNullOrWhiteSpace(AaxFile.MetadataItems.Narrator))
AaxFile.MetadataItems.AppleListBox.EditOrAddTag("©wrt", AaxFile.MetadataItems.Narrator);
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");
if (!string.IsNullOrWhiteSpace(AaxFile.MetadataItems.Copyright))
AaxFile.MetadataItems.Copyright = AaxFile.MetadataItems.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");
//Add audiobook shelf tags
//https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
const string tagDomain = "com.pilabor.tone";
AaxFile.AppleTags.Title = DownloadOptions.Title;
AaxFile.MetadataItems.Title = DownloadOptions.Title;
if (DownloadOptions.Subtitle is string subtitle)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
if (DownloadOptions.Publisher is string publisher)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
if (DownloadOptions.Language is string language)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
if (DownloadOptions.AudibleProductId is string asin)
{
AaxFile.AppleTags.Asin = asin;
AaxFile.AppleTags.AppleListBox.EditOrAddTag("asin", asin);
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
AaxFile.MetadataItems.Asin = asin;
AaxFile.MetadataItems.AppleListBox.EditOrAddTag("asin", asin);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
}
if (DownloadOptions.SeriesName is string series)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
if (DownloadOptions.SeriesNumber is string part)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part);
AaxFile.MetadataItems.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part);
}
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor);
OnRetrievedNarrators(AaxFile.AppleTags.Narrator);
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
OnRetrievedTitle(AaxFile.MetadataItems.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.MetadataItems.FirstAuthor);
OnRetrievedNarrators(AaxFile.MetadataItems.Narrator);
OnRetrievedCoverArt(AaxFile.MetadataItems.Cover);
OnInitialized();
return !IsCanceled;

View File

@@ -1,6 +1,7 @@
using AAXClean;
using AAXClean.Codecs;
using FileManager;
using Mpeg4Lib;
using System;
using System.IO;
using System.Threading.Tasks;

View File

@@ -1,5 +1,5 @@
using AAXClean;
using Dinah.Core;
using Dinah.Core;
using Mpeg4Lib;
using System.IO;
using System.Text;

View File

@@ -1,4 +1,5 @@
using AAXClean;
using Mpeg4Lib;
using System;
#nullable enable

View File

@@ -1,5 +1,5 @@
using AAXClean;
using AAXClean.Codecs;
using AAXClean.Codecs;
using Mpeg4Lib;
using NAudio.Lame;
using System;
using System.Linq;
@@ -11,7 +11,7 @@ namespace AaxDecrypter
{
private const string TagDomain = "com.pilabor.tone";
public static void ConfigureLameOptions(
Mp4File mp4File,
Mpeg4File mp4File,
LameConfig lameConfig,
bool downsample,
bool matchSourceBitrate,
@@ -47,9 +47,9 @@ namespace AaxDecrypter
}
//Setup metadata tags
lameConfig.ID3 = mp4File.AppleTags.ToIDTags();
lameConfig.ID3 = mp4File.MetadataItems.ToIDTags();
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
if (mp4File.MetadataItems.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
lameConfig.ID3.Subtitle = subtitle;
if (chapters?.Count > 0)
@@ -59,12 +59,12 @@ namespace AaxDecrypter
}
//Copy over all other freeform tags
foreach (var t in mp4File.AppleTags.AppleListBox.Tags.OfType<Mpeg4Lib.Boxes.FreeformTagBox>())
foreach (var t in mp4File.MetadataItems.AppleListBox.Tags.OfType<Mpeg4Lib.Boxes.FreeformTagBox>())
{
if (t.Name?.Name is string name &&
t.Mean?.ReverseDnsDomain is string domain &&
!lameConfig.ID3.UserDefinedText.ContainsKey(name) &&
mp4File.AppleTags.AppleListBox.GetFreeformTagString(domain, name) is string tagStr &&
mp4File.MetadataItems.AppleListBox.GetFreeformTagString(domain, name) is string tagStr &&
!string.IsNullOrWhiteSpace(tagStr))
lameConfig.ID3.UserDefinedText.Add(name, tagStr);
}

View File

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

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

@@ -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

@@ -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

@@ -95,7 +95,7 @@ public class LibraryBookImporter : ItemsImporterBase
{
//If the entire library was loaded, we can be sure that all existing LibraryBooks have their Book property populated.
//Find LibraryBooks which have a Book but weren't found in the import, and mark them as absent.
foreach (var absentBook in allInScannedAccounts.Where(lb => !uniqueImportItems.ContainsKey(lb.Book.AudibleProductId)))
foreach (var absentBook in allInScannedAccounts.Where(lb => lb.Book?.AudibleProductId is not string asin || !uniqueImportItems.ContainsKey(asin)))
absentBook.AbsentFromLastScan = true;
}
else

View File

@@ -2,8 +2,8 @@
using DataLayer;
using FileManager;
using Mpeg4Lib.Boxes;
using Mpeg4Lib.ID3;
using Mpeg4Lib.Util;
using NAudio.Lame.ID3;
using System;
using System.Collections.Generic;
using System.IO;

View File

@@ -206,12 +206,12 @@ namespace FileLiberator
}
#region Decryptor event handlers
private void Converter_RetrievedMetadata(object? sender, AAXClean.AppleTags tags)
private void Converter_RetrievedMetadata(object? sender, Mpeg4Lib.MetadataItems tags)
{
if (sender is not AaxcDownloadConvertBase converter ||
converter.AaxFile is not AAXClean.Mp4File aaxFile ||
converter.AaxFile is not Mpeg4Lib.Mpeg4File aaxFile ||
converter.DownloadOptions is not DownloadOptions options ||
options.ChapterInfo.Chapters is not List<AAXClean.Chapter> chapters)
options.ChapterInfo.Chapters is not List<Mpeg4Lib.Chapter> chapters)
return;
#region Prevent erroneous truncation due to incorrect chapter info
@@ -240,7 +240,7 @@ namespace FileLiberator
tags.Album ??= tags.Title;
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
tags.AlbumArtists ??= tags.Artist;
tags.Generes = string.Join(", ", options.LibraryBook.Book.LowestCategoryNames());
tags.Genres = string.Join(", ", options.LibraryBook.Book.LowestCategoryNames());
tags.ProductID ??= options.ContentMetadata.ContentReference.Sku;
tags.Comment ??= options.LibraryBook.Book.Description;
tags.LongDescription ??= tags.Comment;
@@ -256,9 +256,9 @@ namespace FileLiberator
}
const string tagDomain = "org.libation";
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ACR", tags.Acr);
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_DRM_TYPE", options.DrmType.ToString());
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_LOCALE", options.LibraryBook.Book.Locale);
tags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ACR", tags.Acr);
tags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_DRM_TYPE", options.DrmType.ToString());
tags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_LOCALE", options.LibraryBook.Book.Locale);
}
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)

View File

@@ -187,7 +187,7 @@ public partial class DownloadOptions
var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
{
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapters[0].StartOffsetMs)),
ChapterInfo = new Mpeg4Lib.ChapterInfo(TimeSpan.FromMilliseconds(chapters[0].StartOffsetMs)),
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
};

View File

@@ -1,5 +1,4 @@
using AaxDecrypter;
using AAXClean;
using Dinah.Core;
using DataLayer;
using LibationFileManager;
@@ -19,7 +18,7 @@ namespace FileLiberator
public KeyData[]? DecryptionKeys { get; }
public required TimeSpan RuntimeLength { get; init; }
public OutputFormat OutputFormat { get; }
public required ChapterInfo ChapterInfo { get; init; }
public required Mpeg4Lib.ChapterInfo ChapterInfo { get; init; }
public string Title => LibraryBook.Book.Title;
public string Subtitle => LibraryBook.Book.Subtitle;
public string Publisher => LibraryBook.Book.Publisher;

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

@@ -50,7 +50,16 @@ public class App : Application
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
RunSetupIfNeededAsync(desktop, Configuration.Instance);
if (LibraryTask is null)
{
RunSetupIfNeededAsync(desktop, Configuration.Instance);
}
else
{
//LibraryTask was already started early in Program.Main(),
//which means config is valid and migrations have already run.
ShowMainWindow(desktop);
}
}
base.OnFrameworkInitializationCompleted();
@@ -66,7 +75,7 @@ public class App : Application
if (await setup.RunSetupIfNeededAsync())
{
// setup succeeded or wasn't needed and LibationFiles are valid
await RunMigrationsAsync(config);
RunMigrations(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
ShowMainWindow(desktop);
}
@@ -97,11 +106,10 @@ public class App : Application
return await tcs.Task;
}
private static async Task RunMigrationsAsync(Configuration config)
public static void RunMigrations(Configuration config)
{
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
// logging is init'd here
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
}

View File

@@ -40,7 +40,7 @@
<TextBlock
Grid.Column="0"
Text="If you'd like to report this error to an advinistrator:&#xa;&#xa;Step 1: Go to Libation's &quot;issues&quot; page on github&#xa;Step 2: Find your log files&#xa;Setp 3: Click &quot;New issue&quot; button&#xa;Step 4: Drag/drop your log files" />
Text="If you'd like to report this error to an administrator:&#xa;&#xa;Step 1: Go to Libation's &quot;issues&quot; page on github&#xa;Step 2: Find your log files&#xa;Setp 3: Click &quot;New issue&quot; button&#xa;Step 4: Drag/drop your log files" />
<StackPanel
Margin="50,0,0,0"

View File

@@ -51,10 +51,7 @@ namespace LibationAvalonia
var config = LibationScaffolding.RunPreConfigMigrations();
if (config.LibationFiles.SettingsAreValid)
{
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
App.RunMigrations(config);
//Start loading the library before loading the main form
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}

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

@@ -119,6 +119,8 @@ namespace LibationAvalonia.Views
private async void MainWindow_Opened(object? sender, EventArgs e)
{
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
if (AudibleFileStorage.BooksDirectory is null)
{
var result = await MessageBox.Show(

View File

@@ -20,10 +20,16 @@ namespace LibationCli
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook>? completedAction = null)
where TProcessable : Processable, IProcessable<TProcessable>
{
var progressBar = new ConsoleProgressBar(Console.Out);
var strProc = TProcessable.Create(Configuration.Instance);
LibraryBook? currentLibraryBook = null;
if (Environment.UserInteractive && !Console.IsOutputRedirected && !Console.IsErrorRedirected) {
var progressBar = new ConsoleProgressBar(Console.Out);
strProc.Completed += (_, e) => progressBar.Clear();
strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
}
strProc.Begin += (o, e) =>
{
currentLibraryBook = e;
@@ -32,7 +38,6 @@ namespace LibationCli
strProc.Completed += (o, e) =>
{
progressBar.Clear();
Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
};
@@ -49,9 +54,6 @@ namespace LibationCli
}
};
strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
if (strProc is AudioDecodable audDec)
{
audDec.RequestCoverArt += (_,_) =>

View File

@@ -251,7 +251,7 @@ namespace LibationFileManager
{
if (format is OutputFormat.M4b)
{
var tags = await Task.Run(() => AAXClean.AppleTags.FromFile(path));
var tags = await Task.Run(() => Mpeg4Lib.MetadataItems.FromFile(path));
if (tags?.Asin is not null)
audioFile = new FilePathCache.CacheEntry(tags.Asin, FileType.Audio, path);
@@ -259,10 +259,10 @@ namespace LibationFileManager
else
{
using var fileStream = File.OpenRead(path);
var id3 = await Task.Run(() => NAudio.Lame.ID3.Id3Tag.Create(fileStream));
var id3 = await Task.Run(() => Mpeg4Lib.ID3.Id3Tag.Create(fileStream));
var asin = id3?.Children
.OfType<NAudio.Lame.ID3.TXXXFrame>()
.OfType<Mpeg4Lib.ID3.TXXXFrame>()
.FirstOrDefault(f => f.FieldName == "AUDIBLE_ASIN")
?.FieldValue;

View File

@@ -342,7 +342,7 @@ namespace LibationFileManager
[Description("Automatically run periodic scans in the background?")]
public bool AutoScan { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Use Libation's buit-in web broswer to log into Audible?")]
[Description("Use Libation's built-in web browser to log into Audible?")]
public bool UseWebView { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]

View File

@@ -12,7 +12,7 @@ using System.Threading.Tasks;
namespace LibationUiBase.GridView;
public delegate void LiberateClickedHandler(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
public delegate void LiberateClickedHandler(object? sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
public class GridContextMenu
{
public string CopyCellText => $"{Accelerator}Copy Cell Contents";

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

@@ -59,7 +59,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="label1.Text" xml:space="preserve">
<value>If you'd like to report this error to an advinistrator:
<value>If you'd like to report this error to an administrator:
Step 1: Go to Libation's "issues" page on github
Step 2: Find your log files

View File

@@ -4,6 +4,7 @@ using Dinah.Core;
using Dinah.Core.Threading;
using System.Collections.Generic;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -30,7 +31,7 @@ namespace LibationWinForms
private bool runBackupCountsAgain;
private void setBackupCounts(object _, List<LibraryBook> libraryBooks)
private void setBackupCounts(object? _, List<LibraryBook>? libraryBooks)
{
runBackupCountsAgain = true;
@@ -38,7 +39,7 @@ namespace LibationWinForms
updateCountsBw.RunWorkerAsync(libraryBooks);
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
private void UpdateCountsBw_DoWork(object? sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
@@ -47,47 +48,47 @@ namespace LibationWinForms
}
}
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
private void exportMenuEnable(object? _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
Invoke(() => exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults);
Invoke(() => exportLibraryToolStripMenuItem.Enabled = libraryStats?.HasBookResults is true);
}
private void updateBottomStats(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
private void updateBottomStats(object? _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = libraryStats.StatusString);
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = libraryStats?.StatusString ?? "ERROR GETTING STATUS");
}
// update 'begin book and pdf backups' menu item
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
private void update_BeginBookBackups_menuItem(object? _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.HasPendingBooks
= libraryStats?.HasPendingBooks is true
? $"{libraryStats.PendingBooks} remaining"
: "All books have been liberated";
menuStrip1.UIThreadAsync(() =>
{
beginBookBackupsToolStripMenuItem.Format(menuItemText);
beginBookBackupsToolStripMenuItem.Enabled = libraryStats.HasPendingBooks;
beginBookBackupsToolStripMenuItem.Enabled = libraryStats?.HasPendingBooks is true;
});
}
// update 'begin pdf only backups' menu item
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
private void udpate_BeginPdfOnlyBackups_menuItem(object? _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
= libraryStats?.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
menuStrip1.UIThreadAsync(() =>
{
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats?.pdfsNotDownloaded > 0;
});
}
}

View File

@@ -2,6 +2,7 @@
using System.Windows.Forms;
using ApplicationServices;
#nullable enable
namespace LibationWinForms
{
public partial class Form1

View File

@@ -2,6 +2,7 @@
using System.Windows.Forms;
using LibationWinForms.Dialogs;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -27,8 +28,8 @@ namespace LibationWinForms
private void filterBtn_Click(object sender, EventArgs e) => performFilter(this.filterSearchTb.Text);
private string lastGoodFilter = "";
private void performFilter(string filterString)
private string? lastGoodFilter = null;
private void performFilter(string? filterString)
{
this.filterSearchTb.Text = filterString;
@@ -55,12 +56,12 @@ namespace LibationWinForms
dialog.Show(this);
return dialog;
void Dialog_Closed(object sender, FormClosedEventArgs e)
void Dialog_Closed(object? sender, FormClosedEventArgs e)
{
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
filterHelpBtn.Enabled = true;
}
void Dialog_TagDoubleClicked(object sender, string tag)
void Dialog_TagDoubleClicked(object? sender, string tag)
{
if (string.IsNullOrEmpty(tag)) return;

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -13,9 +14,9 @@ namespace LibationWinForms
private void Configure_Liberate() { }
//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)
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

@@ -1,14 +1,13 @@
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.Forms;
using LibationUiBase.GridView;
using LibationWinForms.ProcessQueue;
using System;
using System.Linq;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -106,7 +105,7 @@ namespace LibationWinForms
SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
}
private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
private void ProcessBookQueue1_PopOut(object? sender, EventArgs e)
{
ProcessBookForm dockForm = new();
dockForm.WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
@@ -124,7 +123,7 @@ namespace LibationWinForms
filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X + deltax, filterSearchTb.Location.Y);
}
private void DockForm_FormClosing(object sender, FormClosingEventArgs e)
private void DockForm_FormClosing(object? sender, FormClosingEventArgs e)
{
if (sender is ProcessBookForm dockForm)
{

View File

@@ -4,6 +4,7 @@ using System.Windows.Forms;
using LibationFileManager;
using LibationWinForms.Dialogs;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -17,7 +18,7 @@ namespace LibationWinForms
}
private object quickFilterTag { get; } = new();
private void updateFiltersMenu(object _ = null, object __ = null)
private void updateFiltersMenu(object? _ = null, object? __ = null)
{
// remove old
var removeUs = quickFiltersToolStripMenuItem.DropDownItems
@@ -41,7 +42,7 @@ namespace LibationWinForms
}
}
private void updateFirstFilterIsDefaultToolStripMenuItem(object sender, EventArgs e)
private void updateFirstFilterIsDefaultToolStripMenuItem(object? sender, EventArgs e)
=> firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
@@ -57,7 +58,23 @@ namespace LibationWinForms
private void productsDisplay_InitialLoaded(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
performFilter(QuickFilters.Filters.FirstOrDefault().Filter);
{
// begin verbose null checking. shouldn't be possible, yet NRE in #1578
var f = QuickFilters.Filters;
if (f is null)
Serilog.Log.Logger.Error("Unexpected exception. QuickFilters.Filters is null");
var first = f.FirstOrDefault();
if (first is null)
Serilog.Log.Logger.Information("QuickFilters.Filters.FirstOrDefault() is null");
var filter = first?.Filter;
if (filter is null)
Serilog.Log.Logger.Information("QuickFilters.Filters.FirstOrDefault()?.Filter is null");
// end verbose null checking
performFilter(filter);
}
}
}
}

View File

@@ -1,12 +1,10 @@
using AudibleUtilities;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms
{
public partial class Form1

View File

@@ -7,6 +7,7 @@ using AudibleUtilities;
using Dinah.Core;
using LibationFileManager;
#nullable enable
namespace LibationWinForms
{
// This is for the auto-scanner. It is unrelated to manual scanning/import
@@ -58,7 +59,7 @@ namespace LibationWinForms
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
private void Configuration_PropertyChanged(object? sender, PropertyChangedEventArgsEx e)
{
// when autoscan setting is changed, update menu checkbox and run autoscan
updateAutoScanLibraryToolStripMenuItem(sender, e);
@@ -75,9 +76,9 @@ namespace LibationWinForms
.Select(a => (a.AccountId, a.Locale.Name))
.ToList();
}
private void accountsPreSave(object sender = null, EventArgs e = null)
private void accountsPreSave(object? sender = null, EventArgs? e = null)
=> preSaveDefaultAccounts = getDefaultAccounts();
private void accountsPostSave(object sender = null, EventArgs e = null)
private void accountsPostSave(object? sender = null, EventArgs? e = null)
{
var postSaveDefaultAccounts = getDefaultAccounts();
var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList();
@@ -86,7 +87,7 @@ namespace LibationWinForms
startAutoScan();
}
private void startAutoScan(object sender = null, EventArgs e = null)
private void startAutoScan(object? sender = null, EventArgs? e = null)
{
if (Configuration.Instance.AutoScan)
autoScanTimer.PerformNow();
@@ -94,8 +95,8 @@ namespace LibationWinForms
autoScanTimer.Stop();
}
private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan;
private void updateAutoScanLibraryToolStripMenuItem(object? sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan;
private void autoScanLibraryToolStripMenuItem_Click(object sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked;
private void autoScanLibraryToolStripMenuItem_Click(object? sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked;
}
}

View File

@@ -8,6 +8,7 @@ using AudibleUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;
#nullable enable
namespace LibationWinForms
{
// this is for manual scan/import. Unrelated to auto-scan
@@ -20,7 +21,7 @@ namespace LibationWinForms
locateAudiobooksToolStripMenuItem.ToolTipText = Configuration.GetHelpText("LocateAudiobooks");
}
private void refreshImportMenu(object _, EventArgs __)
private void refreshImportMenu(object? _, EventArgs? __)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
@@ -46,8 +47,8 @@ namespace LibationWinForms
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
if (persister.AccountsSettings.GetAll().FirstOrDefault() is { } firstAccount)
await scanLibrariesAsync(firstAccount);
}
private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)

View File

@@ -1,6 +1,7 @@
using System;
using ApplicationServices;
#nullable enable
namespace LibationWinForms
{
// This is for the Scanning notification in the upper right. This shown for manual scanning and auto-scan
@@ -12,7 +13,7 @@ namespace LibationWinForms
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
}
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
private void LibraryCommands_ScanBegin(object? sender, int accountsLength)
{
removeLibraryBooksToolStripMenuItem.Enabled = false;
removeAllAccountsToolStripMenuItem.Enabled = false;
@@ -29,7 +30,7 @@ namespace LibationWinForms
: $"Scanning {accountsLength} accounts...";
}
private void LibraryCommands_ScanEnd(object sender, int newCount)
private void LibraryCommands_ScanEnd(object? sender, int newCount)
{
removeLibraryBooksToolStripMenuItem.Enabled = true;
removeAllAccountsToolStripMenuItem.Enabled = true;

View File

@@ -2,6 +2,7 @@
using System.Windows.Forms;
using LibationWinForms.Dialogs;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -11,7 +12,7 @@ namespace LibationWinForms
Shown += FormShown_Settings;
}
private void FormShown_Settings(object sender, EventArgs e)
private void FormShown_Settings(object? sender, EventArgs e)
{
if (LibationFileManager.AudibleFileStorage.BooksDirectory is null)
{

View File

@@ -3,6 +3,7 @@ using LibationWinForms.Dialogs;
using System.Threading.Tasks;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms
{
public partial class Form1

View File

@@ -8,6 +8,7 @@ using Dinah.Core.Threading;
using LibationUiBase;
using LibationWinForms.Dialogs;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -24,7 +25,7 @@ namespace LibationWinForms
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
=> await Task.Run(setLiberatedVisibleMenuItem);
private static DateTime lastVisibleCountUpdated;

View File

@@ -6,6 +6,7 @@ using FileManager;
using LibationFileManager;
using LibationUiBase;
#nullable enable
namespace LibationWinForms
{
public partial class Form1
@@ -26,7 +27,7 @@ namespace LibationWinForms
// wire-up event to automatically download after scan.
// winforms only. this should NOT be allowed in cli
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
updateCountsBw.RunWorkerCompleted += (object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
{
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
return;
@@ -36,14 +37,14 @@ namespace LibationWinForms
};
}
private static object LoadResourceImage(string resourceName)
private static object? LoadResourceImage(string resourceName)
{
if (Application.IsDarkModeEnabled)
resourceName += "_dark";
return Properties.Resources.ResourceManager.GetObject(resourceName);
}
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
private void AudibleApiStorage_LoadError(object? sender, AccountSettingsLoadErrorEventArgs e)
{
try
{

View File

@@ -14,18 +14,18 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.GridView
{
public partial class ProductsDisplay : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event LiberateClickedHandler LiberateClicked;
public event EventHandler<SeriesEntry> LiberateSeriesClicked;
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
public event EventHandler InitialLoaded;
public event EventHandler<int>? VisibleCountChanged;
public event EventHandler<int>? RemovableCountChanged;
public event LiberateClickedHandler? LiberateClicked;
public event EventHandler<SeriesEntry>? LiberateSeriesClicked;
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
public event EventHandler? InitialLoaded;
private bool hasBeenDisplayed;
@@ -37,15 +37,15 @@ namespace LibationWinForms.GridView
#region Button controls
private ImageDisplay imageDisplay;
private ImageDisplay? imageDisplay;
private void productsGrid_CoverClicked(GridEntry liveGridEntry)
{
var picDef = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
void PictureCached(object sender, PictureCachedEventArgs e)
void PictureCached(object? sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == picDef.PictureId)
imageDisplay.SetCoverArt(e.Picture);
imageDisplay?.SetCoverArt(e.Picture);
PictureStorage.PictureCached -= PictureCached;
}
@@ -82,7 +82,7 @@ namespace LibationWinForms.GridView
BorderThickness = 2,
};
void CloseWindow(object o, EventArgs e)
void CloseWindow(object? o, EventArgs e)
{
displayWindow.Close();
}
@@ -92,7 +92,7 @@ namespace LibationWinForms.GridView
displayWindow.Show(this);
}
private BookDetailsDialog bookDetailsForm;
private BookDetailsDialog? bookDetailsForm;
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
{
if (bookDetailsForm is null || bookDetailsForm.IsDisposed || !bookDetailsForm.Visible)
@@ -106,7 +106,7 @@ namespace LibationWinForms.GridView
if (!bookDetailsForm.Visible)
bookDetailsForm.Show(this);
async void bookDetailsForm_FormClosed(object sender, FormClosedEventArgs e)
async void bookDetailsForm_FormClosed(object? sender, FormClosedEventArgs e)
{
bookDetailsForm.FormClosed -= bookDetailsForm_FormClosed;
bookDetailsForm.SaveSizeAndLocation(Configuration.Instance);
@@ -381,7 +381,7 @@ namespace LibationWinForms.GridView
foreach (var r in removable)
r.Remove = true;
productsGrid_RemovableCountChanged(this, null);
productsGrid_RemovableCountChanged(this, EventArgs.Empty);
}
catch (OperationCanceledException)
{
@@ -401,7 +401,7 @@ namespace LibationWinForms.GridView
#region UI display functions
public async Task DisplayAsync(List<LibraryBook> libraryBooks = null)
public async Task DisplayAsync(List<LibraryBook>? libraryBooks = null)
{
try
{
@@ -428,7 +428,7 @@ namespace LibationWinForms.GridView
#region Filter
public void Filter(string searchString)
public void Filter(string? searchString)
=> productsGrid.Filter(searchString);
#endregion
@@ -443,7 +443,7 @@ namespace LibationWinForms.GridView
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
{
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
&& !liveGridEntry.Liberate.IsUnavailable)
&& liveGridEntry.Liberate?.IsUnavailable is false)
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook], Configuration.Instance);
}

View File

@@ -0,0 +1,31 @@
# Troubleshooting Common Libation Errors
## How to run the Hangover App
When troubleshooting, you may be asked to run 'Hangover'. Hangover is a debugging app to help diagnose and solve some problems with Libation.
It is located alongside the Libation app (though not included in the docker container).
### Windows
Hangover.exe is located in the folder containing Libation.exe. Double-click it to rune it.
### macOS
Hangover is located inside the app bundle. Either:
1. From a terminal, run this command: `open -a Libation.app --args hangover`
2. Run it from within the app bundle.
1. In finder, right-click the Libation app bundle and "Show Package Contents"
2. Open folders "Contents" > "MacOS"
3. Find the file named "Hangover" and double-click it to run it.
### Linux (either the .deb or .rpm installers)
The installer creates shortcuts for `libation`, `libationcli`, and `hangover`. From a terminal, run `hangover`.
## SQLite Error 10: 'disk I/O error'.
There are two possible causes of this error.
1. Your hard disk is full. Check that you have space on the storage device containing your Libation Files (where the LibationContext.db and log files are). If that device still has available space, move on to #2 below.
2. The database's journaling mode is incompatible with your environment. Change the journaling mode to `DELETE` by one of two methods.
1. [Run hangover](#how-to-run-the-hangover-app) and execute the following command in the "Database" tab: `PRAGMA journal_mode=DELETE`
2. run this command in your terminal: `sqlite3 "path/to/libation/files/LibationContext.db" "PRAGMA journal_mode=DELETE;"`

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.
---