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()}";
}