using System; namespace DataLayer; /// /// Why a download attempt failed, coarse enough to choose how long to wait before trying again. /// Persisted as an int; do not renumber. /// public enum DownloadFailureKind { /// /// Audible refused a content license and named an eligibility reason: the title is not owned, is not in /// the Plus catalog, or the account is not entitled to it. Changes only when the account or the catalog /// changes, so this is worth waiting a long time on. /// LicenseDenied = 0, /// /// Audible accepted the request but has no downloadable asset, as for a preorder that has not been /// released. Expected to start working by itself once the title is published. /// AssetUnavailable = 1, /// /// Looks like a service interruption or throttling rather than a decision about this title. Retried soon. /// ServiceInterruption = 2, } /// /// The most recent failed attempt to download one title, so that a title Audible has just refused is not /// requested again on every run. One row per (account, title): the same ASIN can be refused on one account /// and downloadable on another. /// /// Nothing here is permanent. always names a time, so a title held back /// because of an outage, throttling or an unreleased preorder starts being attempted again on its own. /// /// /// The database is deliberately the home for this instead of a file under LibationFiles: in Docker, /// LibationFiles is a throwaway directory inside the container and only the database is on a volume, so a /// file-based record would forget every failure on each container start - exactly the case this fixes. /// /// public class DownloadAttemptFailure { internal int DownloadAttemptFailureId { get; private set; } public string AudibleProductId { get; private set; } /// The the attempt was made with. public string Account { get; private set; } public DownloadFailureKind Kind { get; private set; } /// Failures in a row without an intervening success. Drives how long the next wait is. public int ConsecutiveFailures { get; private set; } /// /// UTC ticks rather than a DateTime so range queries mean the same thing on SQLite and PostgreSQL. /// Local time is for display only. /// public long LastFailedAtUtcTicks { get; private set; } /// When this title becomes eligible for another automatic attempt, in UTC ticks. public long RetryAfterUtcTicks { get; private set; } /// One line from Audible, kept so the user can be told why without re-requesting a license. public string? Reason { get; private set; } public DateTimeOffset LastFailedAt => new(LastFailedAtUtcTicks, TimeSpan.Zero); public DateTimeOffset RetryAfter => new(RetryAfterUtcTicks, TimeSpan.Zero); private DownloadAttemptFailure() { // for EF AudibleProductId = null!; Account = null!; } public DownloadAttemptFailure(string audibleProductId, string account, DownloadFailureKind kind, int consecutiveFailures, DateTimeOffset lastFailedAt, DateTimeOffset retryAfter, string? reason) { AudibleProductId = audibleProductId; Account = account; Record(kind, consecutiveFailures, lastFailedAt, retryAfter, reason); } public void Record(DownloadFailureKind kind, int consecutiveFailures, DateTimeOffset lastFailedAt, DateTimeOffset retryAfter, string? reason) { Kind = kind; ConsecutiveFailures = consecutiveFailures; LastFailedAtUtcTicks = lastFailedAt.UtcTicks; RetryAfterUtcTicks = retryAfter.UtcTicks; Reason = reason; } public override string ToString() => $"{AudibleProductId} {Kind} x{ConsecutiveFailures}, retry after {RetryAfter.ToLocalTime()}"; }