Merge pull request #2078 from rmcrackan/rmcrackan/2071-serialization

bug fix #2071 -- better metadata serialization
This commit is contained in:
rmcrackan authored and GitHub committed 2026-09-17 08:31:49 -04:00
commit 276e97037d
3 files changed
+46 -7

No files matched your search

+12 -5
View File
@@ -597,14 +597,13 @@ public class DownloadDecryptBook : AudioDecodable, IProcessable<DownloadDecryptB
extension: ".metadata.json",
returnFirstExisting: Configuration.OverwriteExisting);
if (File.Exists(metadataPath))
FileUtility.SaferDelete(metadataPath);
sourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(options.ContentMetadata.ChapterInfo));
sourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(options.ContentMetadata.ContentReference));
sourceJson.Add(nameof(ContentMetadata.ContentReference), SerializeContentReference(options.ContentMetadata.ContentReference));
cancellationToken.ThrowIfCancellationRequested();
File.WriteAllText(metadataPath, sourceJson.ToString());
// Serialize before touching the destination, then replace it atomically so a failed export
// cannot delete or truncate metadata saved by an earlier download.
Dinah.Core.IO.AtomicFileWriter.WriteAllText(metadataPath, sourceJson.ToString());
SetFileTime(options.LibraryBook, metadataPath);
OnFileCreated(options.LibraryBook, metadataPath);
}
@@ -617,6 +616,14 @@ public class DownloadDecryptBook : AudioDecodable, IProcessable<DownloadDecryptB
}
}
internal static Newtonsoft.Json.Linq.JObject SerializeContentReference(ContentReference reference)
// Unencrypted podcast licenses can omit codec and other fields required by the API model.
// Preserve the fields we have without inventing values for this optional metadata export.
=> Newtonsoft.Json.Linq.JObject.FromObject(reference, new Newtonsoft.Json.JsonSerializer
{
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore
});
/// <summary>
/// Whether a catalog product carries no details at all.
/// <para>
@@ -9,8 +9,10 @@ public readonly record struct CloudSyncStatus(bool IsSynced, string? ProviderNam
{
public static readonly CloudSyncStatus NotSynced = new(false, null);
/// <summary>The provider's own name where Windows reported one, otherwise a generic description.</summary>
public string Description => ProviderName is { Length: > 0 } name ? name : "a cloud sync folder";
/// <summary>Whether the folder is synced, naming the provider where Windows reported one.</summary>
public string Description => !IsSynced
? "not a cloud sync folder"
: ProviderName is { Length: > 0 } name ? name : "a cloud sync folder";
}
/// <summary>
@@ -11,6 +11,36 @@ namespace FileLiberator.Tests;
[TestClass]
public class CatalogMetadataTests
{
[TestMethod]
public void Podcast_content_reference_with_missing_required_fields_can_be_exported()
{
var reference = new AudibleApi.Common.ContentReference
{
Asin = "B0981FC27J", Version = "1", Acr = "CR!ACR", Codec = null!,
ContentFormat = null!, Marketplace = "AF2M0KC94RCEA", Sku = "BK_TEST_000001", Tempo = null!
};
// Reproduce the exception from the reported podcast download before exercising the export.
Assert.ThrowsExactly<Newtonsoft.Json.JsonSerializationException>(() => JObject.FromObject(reference));
var exported = DownloadDecryptBook.SerializeContentReference(reference);
Assert.AreEqual("B0981FC27J", exported.Value<string>("asin"));
Assert.AreEqual("1", exported.Value<string>("version"));
Assert.IsNull(exported.Property("codec"));
}
[TestMethod]
public void Complete_content_reference_keeps_its_existing_json_representation()
{
var reference = new AudibleApi.Common.ContentReference
{
Acr = "CR!ACR", Asin = "B0981FC27J", Codec = "mp4a", ContentFormat = "MPEG4_44_128",
Marketplace = "AF2M0KC94RCEA", Sku = "BK_TEST_000001", Tempo = "1.0", Version = "1"
};
Assert.IsTrue(JToken.DeepEquals(JObject.FromObject(reference), DownloadDecryptBook.SerializeContentReference(reference)));
}
/// <summary>
/// Verbatim from <c>api.audible.com/1.0/catalog/products/?asins=B089T8FSK6&amp;response_groups=&lt;all&gt;</c>,
/// the response behind the empty metadata file in the report. The title itself is real and downloadable;