mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-02-06 20:23:03 -05:00
Compare commits
36 Commits
v13.1.3
...
rmcrackan-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f00951d615 | ||
|
|
c7e844a54c | ||
|
|
178cc82d65 | ||
|
|
054f7437d1 | ||
|
|
99051b6975 | ||
|
|
eed7f1811b | ||
|
|
33cd6b8639 | ||
|
|
06014f467e | ||
|
|
b2eef18217 | ||
|
|
443d1f64ca | ||
|
|
c43e88d269 | ||
|
|
dc1919f411 | ||
|
|
fdfae2f806 | ||
|
|
5c56e1d39b | ||
|
|
b39e2c3e0b | ||
|
|
946d4779a9 | ||
|
|
e620d0be24 | ||
|
|
d58f5abe35 | ||
|
|
d957d6d5d7 | ||
|
|
cb159336a6 | ||
|
|
807bb56c49 | ||
|
|
3d56554aa5 | ||
|
|
f720127a68 | ||
|
|
c6c3b39e9c | ||
|
|
e70cc5342c | ||
|
|
bf0d380dd2 | ||
|
|
471fd1e757 | ||
|
|
a43f25db23 | ||
|
|
e1f4168599 | ||
|
|
29501bddf3 | ||
|
|
e9016ace03 | ||
|
|
d498c094bf | ||
|
|
ce92e79cd8 | ||
|
|
f54a789ae8 | ||
|
|
3ef0bce909 | ||
|
|
413da72bf0 |
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("©", "©");
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.MetadataItems.Copyright))
|
||||
AaxFile.MetadataItems.Copyright = AaxFile.MetadataItems.Copyright.Replace("(P)", "℗").Replace("©", "©");
|
||||
|
||||
//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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using FileManager;
|
||||
using Mpeg4Lib;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using AAXClean;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core;
|
||||
using Mpeg4Lib;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AAXClean;
|
||||
using Mpeg4Lib;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="If you'd like to report this error to an advinistrator:

Step 1: Go to Libation's "issues" page on github
Step 2: Find your log files
Setp 3: Click "New issue" button
Step 4: Drag/drop your log files" />
|
||||
Text="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
Setp 3: Click "New issue" button
Step 4: Drag/drop your log files" />
|
||||
|
||||
<StackPanel
|
||||
Margin="50,0,0,0"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 += (_,_) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.")]
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using LibationWinForms.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
31
docs/advanced/troubleshoot.md
Normal file
31
docs/advanced/troubleshoot.md
Normal 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;"`
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user