almost ready
This commit is contained in:
@@ -36,9 +36,12 @@ public class Listing
|
||||
/// <summary>"buy_now" or "auction".</summary>
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
/// <summary>Asking price in USD.</summary>
|
||||
/// <summary>Asking price.</summary>
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>Currency of <see cref="Price"/>. CSFloat lists in USD.</summary>
|
||||
public string Currency { get; set; } = "USD";
|
||||
|
||||
/// <summary>When CSFloat says the listing was created.</summary>
|
||||
public DateTimeOffset ListedAt { get; set; }
|
||||
|
||||
@@ -48,7 +51,13 @@ public class Listing
|
||||
public int PaintIndex { get; set; }
|
||||
public string MarketHashName { get; set; } = null!;
|
||||
public string? WearName { get; set; }
|
||||
public decimal FloatValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Exact float, or null for items with no float at all (e.g. Vanilla knives).
|
||||
/// Null is deliberately distinct from a genuine 0.0 float; a floatless item
|
||||
/// also can't be fingerprinted, so its <see cref="SkinInstanceId"/> stays null.
|
||||
/// </summary>
|
||||
public decimal? FloatValue { get; set; }
|
||||
public int PaintSeed { get; set; }
|
||||
public bool IsStatTrak { get; set; }
|
||||
public bool IsSouvenir { get; set; }
|
||||
@@ -68,6 +77,15 @@ public class Listing
|
||||
public int? SkinId { get; set; }
|
||||
public Skin? Skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The wear band this listing belongs to. Unlike <see cref="SkinId"/> this is NOT
|
||||
/// best-effort: the catalogue sweep pages one skin+wear band at a time, so the band
|
||||
/// is set directly from the sweep unit. Null for whole-skin sweeps (e.g. vanilla
|
||||
/// knives with no wear bands).
|
||||
/// </summary>
|
||||
public int? ConditionId { get; set; }
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The physical item (by fingerprint) this listing is for. Many listings over
|
||||
/// time roll up to one instance, forming its market-movement history. Nullable
|
||||
|
||||
@@ -16,12 +16,6 @@ public class Skin
|
||||
public int? DefIndex { get; set; }
|
||||
public int? PaintIndex { get; set; }
|
||||
|
||||
// When the catalogue-driven listing sweep last fully covered this skin. The
|
||||
// sweep processes least-recently-swept skins first (nulls = never swept), so
|
||||
// capped runs chain across the whole catalogue and the stalest data refreshes
|
||||
// first. Null until the first sweep reaches this skin.
|
||||
public DateTimeOffset? ListingsSweptAt { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public string Rarity { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
@@ -44,6 +38,12 @@ public class Skin
|
||||
public bool? TrueFloat { get; private set; }
|
||||
|
||||
public ICollection<SkinCondition> Conditions { get; set; } = new List<SkinCondition>();
|
||||
|
||||
// Per-site "last swept" checkpoints for the whole-skin sweep unit — only used for
|
||||
// skins with no wear bands (the per-band checkpoint lives on SkinCondition.Sweeps).
|
||||
// The sweep processes never-swept (no row) / stalest skins first. See SkinSweep.
|
||||
public ICollection<SkinSweep> Sweeps { get; set; } = new List<SkinSweep>();
|
||||
|
||||
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
|
||||
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ public class SkinCondition
|
||||
public Skin Skin { get; set; } = null!;
|
||||
|
||||
public string Condition { get; set; } = null!;
|
||||
public decimal MinFloat { get; set; }
|
||||
public decimal MaxFloat { get; set; }
|
||||
public decimal FloatMin { get; set; }
|
||||
public decimal FloatMax { get; set; }
|
||||
|
||||
// When the catalogue-driven listing sweep last fully covered this skin's wear
|
||||
// band. The sweep splits each skin by wear and pages one band at a time, so this
|
||||
// is the per-band checkpoint: an interrupted run resumes from never-swept/stalest
|
||||
// bands rather than redoing a whole skin. Null until the first sweep reaches it.
|
||||
public DateTimeOffset? ListingsSweptAt { get; set; }
|
||||
// Per-site "last swept" checkpoints for this wear band — one row per marketplace
|
||||
// (Source). The sweep splits each skin by wear and pages one band at a time, so
|
||||
// this is the per-band checkpoint: an interrupted run resumes from never-swept
|
||||
// (no row) / stalest bands rather than redoing a whole skin. Tracked per site so a
|
||||
// band swept on CSFloat is still never-swept on cs.money. See SkinConditionSweep.
|
||||
public ICollection<SkinConditionSweep> Sweeps { get; set; } = new List<SkinConditionSweep>();
|
||||
|
||||
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
|
||||
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One site's "last swept" checkpoint for a single wear band. The catalogue sweep
|
||||
/// processes least-recently-swept bands first (no row = never swept), so capped/looping
|
||||
/// runs chain across the catalogue and refresh the stalest data first. Keyed by
|
||||
/// <c>(SkinConditionId, Source)</c> so each marketplace tracks its own progress
|
||||
/// independently — a band swept on one site stays never-swept on another.
|
||||
/// </summary>
|
||||
public class SkinConditionSweep
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int SkinConditionId { get; set; }
|
||||
public SkinCondition SkinCondition { get; set; } = null!;
|
||||
|
||||
/// <summary>Which site swept it — a <see cref="SweepSource"/> value.</summary>
|
||||
public string Source { get; set; } = null!;
|
||||
|
||||
public DateTimeOffset SweptAt { get; set; }
|
||||
}
|
||||
@@ -26,9 +26,11 @@ public class SkinInstance
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
// The fingerprint. FloatValue is stored at full precision (see config) so
|
||||
// that exact-match dupe detection isn't fooled by rounding.
|
||||
// that exact-match dupe detection isn't fooled by rounding. An instance is
|
||||
// only created for items that have a float + paint seed (skins), so both are
|
||||
// non-null here even though some listings (e.g. vanilla knives) lack them.
|
||||
public decimal FloatValue { get; set; }
|
||||
public string PaintSeed { get; set; } = null!;
|
||||
public int PaintSeed { get; set; }
|
||||
public bool StatTrak { get; set; }
|
||||
public bool Souvenir { get; set; }
|
||||
public DateTimeOffset FirstSeenAt { get; set; }
|
||||
|
||||
54
BlueLaminate/BlueLaminate.EFCore/Entities/SkinLandListing.cs
Normal file
54
BlueLaminate/BlueLaminate.EFCore/Entities/SkinLandListing.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One offer observed on skin.land via its internal
|
||||
/// <c>GET /api/v2/obtained-skins?skin_id={id}&page={n}</c> endpoint (scraped through
|
||||
/// the Python worker, since skin.land has no public API and sits behind Cloudflare).
|
||||
/// <para>
|
||||
/// Kept in its own table like <see cref="CsMoneyListing"/>, but deliberately thinner:
|
||||
/// skin.land exposes a full-precision float and price but <b>no paint seed / def index</b>,
|
||||
/// so an offer can't be fingerprinted to a market-agnostic <see cref="SkinInstance"/> and
|
||||
/// there is no cross-market roll-up or dupe detection here (revisit if pattern is ever
|
||||
/// exposed). StatTrak and Souvenir live on <em>separate</em> skin.land pages (their own
|
||||
/// <c>stattrak-</c>/<c>souvenir-</c> slugs); v1 sweeps the base page per skin+wear, so
|
||||
/// <see cref="IsStatTrak"/>/<see cref="IsSouvenir"/> are normally false.
|
||||
/// </para>
|
||||
/// Soft-tracked across sweeps exactly like <see cref="CsMoneyListing"/>:
|
||||
/// <see cref="FirstSeenAt"/>/<see cref="LastSeenAt"/> bound the observation window and
|
||||
/// <see cref="Status"/> flips to <see cref="ListingStatus.Removed"/> when a once-seen
|
||||
/// offer stops appearing (sold/delisted).
|
||||
/// </summary>
|
||||
public class SkinLandListing
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>skin.land's offer id (obtained-skin <c>id</c>). Natural key for dedup.</summary>
|
||||
public long ListingId { get; set; }
|
||||
|
||||
// Catalogue links. Like cs.money (and unlike the CSFloat global sweep) these are NOT
|
||||
// best-effort: each scrape job targets one skin+wear, so we set them directly.
|
||||
public int SkinId { get; set; }
|
||||
public Skin Skin { get; set; } = null!;
|
||||
public int? ConditionId { get; set; }
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
// Item identity, from the offer's skin block.
|
||||
public string MarketHashName { get; set; } = null!;
|
||||
public decimal? FloatValue { get; set; } // item_float (string, full precision)
|
||||
public bool IsStatTrak { get; set; }
|
||||
public bool IsSouvenir { get; set; }
|
||||
public string? NameTag { get; set; } // offer.name_tag (rare; affects value)
|
||||
public int StickerCount { get; set; }
|
||||
|
||||
// Pricing. skin.land returns a single price (the amount to buy/withdraw the item).
|
||||
public decimal Price { get; set; } // final_withdrawal_price
|
||||
public string Currency { get; set; } = "USD"; // prices are read in USD
|
||||
|
||||
public string? InspectLink { get; set; } // item_link (steam:// inspect)
|
||||
|
||||
// Soft-tracking across sweeps.
|
||||
public DateTimeOffset FirstSeenAt { get; set; }
|
||||
public DateTimeOffset LastSeenAt { get; set; }
|
||||
public ListingStatus Status { get; set; }
|
||||
public DateTimeOffset? RemovedAt { get; set; }
|
||||
}
|
||||
20
BlueLaminate/BlueLaminate.EFCore/Entities/SkinSweep.cs
Normal file
20
BlueLaminate/BlueLaminate.EFCore/Entities/SkinSweep.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One site's "last swept" checkpoint for a whole skin — used only for skins with no
|
||||
/// wear bands (e.g. vanilla knives), which are swept as a single unit. The per-band
|
||||
/// equivalent is <see cref="SkinConditionSweep"/>. Keyed by <c>(SkinId, Source)</c> so
|
||||
/// each marketplace tracks its own progress independently.
|
||||
/// </summary>
|
||||
public class SkinSweep
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int SkinId { get; set; }
|
||||
public Skin Skin { get; set; } = null!;
|
||||
|
||||
/// <summary>Which site swept it — a <see cref="SweepSource"/> value.</summary>
|
||||
public string Source { get; set; } = null!;
|
||||
|
||||
public DateTimeOffset SweptAt { get; set; }
|
||||
}
|
||||
23
BlueLaminate/BlueLaminate.EFCore/Entities/SweepSource.cs
Normal file
23
BlueLaminate/BlueLaminate.EFCore/Entities/SweepSource.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical site identifiers for per-site sweep checkpoints — the <c>Source</c>
|
||||
/// discriminator on <see cref="SkinSweep"/> and <see cref="SkinConditionSweep"/>.
|
||||
/// Each marketplace sweeper stamps its own checkpoint under one of these, so a band
|
||||
/// swept on one site is still "never swept" on another.
|
||||
/// <para>
|
||||
/// To add sweeping for a new marketplace, add one constant here and have that
|
||||
/// sweeper read/stamp checkpoints with it — no schema or query changes needed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SweepSource
|
||||
{
|
||||
/// <summary>CSFloat catalogue-driven sweep (<c>ListingSweepService.SweepCatalogAsync</c>).</summary>
|
||||
public const string CsFloatCatalog = "listings-catalog";
|
||||
|
||||
/// <summary>cs.money worker sweep (<c>CsMoneyIngestService</c>).</summary>
|
||||
public const string CsMoney = "csmoney";
|
||||
|
||||
/// <summary>skin.land worker sweep (<c>SkinLandIngestService</c>).</summary>
|
||||
public const string SkinLand = "skinland";
|
||||
}
|
||||
Reference in New Issue
Block a user