- Added methods to `IMediaStore` for loading, saving, and removing media rules, allowing users to manage rules for media items effectively. - Updated `MediaFetcher` to utilize the new rule management system, integrating rule checks into the fetching process to handle geo-blocks and previously ruled items. - Enhanced `ProxyPool` to support exclusion of proxies from specific countries during acquisition, improving the handling of geo-blocked content. - Adjusted `FetchOptions` to include rules instead of tombstones, streamlining the decision-making process during media fetching. - Updated UI components to support rule editing, providing users with a more interactive experience when managing media rules. These changes improve the overall media collection process by allowing users to define rules for handling media items and enhancing the proxy management system for better content accessibility.
93 lines
3.5 KiB
C#
93 lines
3.5 KiB
C#
namespace AvParser.Core.Collecting;
|
|
|
|
/// <summary>What to do when a collected item turns out to be a particular known picture.</summary>
|
|
public enum MediaRuleAction
|
|
{
|
|
/// <summary>Never keep it: drop the bytes and settle the address without storing anything.</summary>
|
|
Skip = 0,
|
|
|
|
/// <summary>Fetch the same address again through a proxy in another country.</summary>
|
|
RetryElsewhere = 1,
|
|
}
|
|
|
|
/// <summary>
|
|
/// A standing instruction about one exact picture, recognised by its content hash.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Born from what a scan actually produces: services answer a missing id with a placeholder and a
|
|
/// geo-block with a banner, and both arrive as a perfectly valid image with a 200. Nothing about the
|
|
/// address distinguishes them — only the bytes do. So the user points at one in the gallery and says
|
|
/// what it means, and every future copy is treated the same way.
|
|
/// </para>
|
|
/// <para>
|
|
/// Matching is by SHA-256, which is exact: a re-encoded or differently sized variant of the same
|
|
/// banner is a different rule. That is the same deliberate line the store draws for de-duplication —
|
|
/// there is no perceptual hashing here, and pretending otherwise would silently drop real finds.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="Sha256">Content hash the rule recognises.</param>
|
|
/// <param name="Action">What to do with a match.</param>
|
|
public sealed record MediaRule(string Sha256, MediaRuleAction Action)
|
|
{
|
|
/// <summary>Separates ids inside <see cref="SourceIds"/>.</summary>
|
|
public const char SourceSeparator = ';';
|
|
|
|
/// <summary>
|
|
/// Sources the rule covers, separated by <c>;</c>; <see langword="null"/> or empty means all of them.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A joined string rather than a list so the record keeps value equality and the row stays one
|
|
/// column. Scoped because the same bytes mean different things to different services: one host's
|
|
/// "removed" placeholder is another host's perfectly ordinary picture.
|
|
/// </remarks>
|
|
public string? SourceIds { get; init; }
|
|
|
|
/// <summary>Why the user made this rule, for the list they will read months later.</summary>
|
|
public string? Reason { get; init; }
|
|
|
|
/// <summary>When it was created.</summary>
|
|
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
|
|
|
/// <summary>Whether the rule covers <paramref name="sourceId"/>.</summary>
|
|
public bool AppliesTo(string? sourceId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(SourceIds))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(sourceId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var id in Split(SourceIds))
|
|
{
|
|
if (string.Equals(id, sourceId, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>The ids this rule is scoped to; empty means every source.</summary>
|
|
public IReadOnlyList<string> Sources => Split(SourceIds);
|
|
|
|
/// <summary>Joins ids for storage in <see cref="SourceIds"/>.</summary>
|
|
public static string? JoinSources(IEnumerable<string> sourceIds)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(sourceIds);
|
|
|
|
var joined = string.Join(SourceSeparator, sourceIds);
|
|
return joined.Length == 0 ? null : joined;
|
|
}
|
|
|
|
private static string[] Split(string? ids) =>
|
|
string.IsNullOrWhiteSpace(ids)
|
|
? []
|
|
: ids.Split(SourceSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
}
|