using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using CsvHelper.Configuration.Attributes; // ReSharper disable MemberCanBePrivate.Global namespace Buypeeb { internal class YahooAuctionsItem { public bool AutoExtension; public bool Available; public int Bids; public DateTime LastUpdated; public int Price; public bool Ready; public DateTime StartDate; public bool UpdateFailed; public int WinPrice; public YahooAuctionsItem(string id, string name) { this.id = id; this.name = name; } public YahooAuctionsItem() { // parameterless constructor for deserialisation } [JsonIgnore] public string url => $"https://page.auctions.yahoo.co.jp/jp/auction/{id}"; [JsonIgnore] public string buyeeUrl => $"https://buyee.jp/item/yahoo/auction/{id}"; // any items with a getter will be saved to userdata.json // anything that's configurable by the user, such as the custom name, should be saved // the id *must* be saved! // i'm also saving the original name to make it easier to tell what the items are in the userdata.json // there's not really a need for it i guess but it's my program and i can do what i want // anything with the attribute [Ignore] won't be put in the CSV, and things with [JsonIgnore] won't be put in // userdata.json // don't remove ID's set attribute - JSON deserialisation will silently fail to assign IDs to the auction items! // you also don't want to private any of the setters, which will have a similar effect. [Ignore] public string id { get; set; } public string name { get; set; } public string originalName { get; set; } public string notes { get; set; } public bool favourite { get; set; } public DateTime endDate { get; set; } [Ignore] [JsonIgnore] public bool updatedRecently { get { var later = LastUpdated.AddSeconds(15); return DateTime.Compare(later, LastUpdated) < 0; } } [JsonIgnore] public string priceJpy => $"¥{Price}"; [JsonIgnore] public string winPriceJpy => $"¥{WinPrice}"; [Ignore] [JsonIgnore] public string priceAud => $"${Price / 75.0:f2}"; [Ignore] [JsonIgnore] public string winPriceAud => $"${WinPrice / 75.0:f2}"; [Ignore] [JsonIgnore] public bool endingToday => endDate.DayOfYear == DateTime.UtcNow.DayOfYear; [Ignore] [JsonIgnore] public bool hasWinPrice => WinPrice != 0; [Ignore] [JsonIgnore] public bool endingSoon => DateTime.Compare(DateTime.UtcNow.AddMinutes(10), endDate) > 0; private bool success { get; set; } // TODO: custom setter that throws an exception if set to false or something idk public void AuctionEnded() { // the page 404'd. this probably means that the auction has ended, and the page has been removed. Available = false; LastUpdated = DateTime.UtcNow; UpdateFailed = true; } public void Update(string html) { // TODO: handle all the parsing errors and weird interpretation that could possibly happen here // TODO: maybe compile and match the regex in another thread var rx = new Regex(@"var pageData ?= ?(\{.+?\});", RegexOptions.Singleline); var m = rx.Match(html); Dictionary> jFull; try { // master forgive me, but i must go all out, just this once... jFull = JsonSerializer.Deserialize>>(m.Groups[1].Value); } catch { Console.WriteLine("oh jeez oh man oh jeez oh man oh jeez oh man"); Console.WriteLine(m.Groups[1].Value); throw; } var jst = TimeZoneInfo.CreateCustomTimeZone("JST", new TimeSpan(9, 0, 0), "Japan Standard Time", "Japan Standard Time"); Debug.Assert(jFull != null, nameof(jFull) + " != null"); var j = jFull["items"]; originalName = j["productName"]; StartDate = TimeZoneInfo.ConvertTimeToUtc( DateTime.ParseExact(j["starttime"], "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), jst); endDate = TimeZoneInfo.ConvertTimeToUtc( DateTime.ParseExact(j["endtime"], "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), jst); LastUpdated = DateTime.UtcNow; Available = j["isClosed"] == "0"; success = int.TryParse(j["price"], out Price); success = int.TryParse(j["winPrice"], out WinPrice); success = int.TryParse(j["bids"], out Bids); if (string.IsNullOrWhiteSpace(name)) { name = originalName; } // as far as i can tell, neither the `pageData` nor the `conf` variables in the html seem to store whether or not // the auction uses automatic extension, the `conf` variable *does*, however, store whether or not the auction // has the "early end" feature enabled, in the key `earlyed`. unfortunately, it seems like the only way to get the // auto extension info is to scrape the page for the info column that displays the auto ext status and check // whether or not it's equal to "ari" (japanese for "yes"). rx = new Regex(@"自動延長.+\n.+>(.+)<"); m = rx.Match(html); AutoExtension = m.Groups[1].Value == "あり"; UpdateFailed = false; } public override string ToString() { return $"{id}: {name}"; } } }