diff --git a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs index 4a3bcfc..53c1897 100644 --- a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs +++ b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Props.Shop.Adafruit.Api; +using Props.Shop.Adafruit.Persistence; using Props.Shop.Framework; namespace Props.Shop.Adafruit { public class AdafruitShop : IShop { + private string workspaceDir; private ILoggerFactory loggerFactory; private ILogger logger; private SearchManager searchManager; @@ -32,22 +36,80 @@ namespace Props.Shop.Adafruit ); public void Initialize(string workspaceDir, ILoggerFactory loggerFactory) { + this.workspaceDir = workspaceDir; this.loggerFactory = loggerFactory; + logger = loggerFactory.CreateLogger(); http = new HttpClient(); http.BaseAddress = new Uri("http://www.adafruit.com/api/"); - configuration = new Configuration(); - // TODO: Implement config persistence. - // TODO: Implement product listing persisted cache. - LiveProductListingManager productListingManager = new LiveProductListingManager(http, loggerFactory.CreateLogger(), configuration.MinDownloadInterval); - this.searchManager = new SearchManager(productListingManager, configuration.Similarity); - productListingManager.StartUpdateTimer(delay: 0); + try + { + configuration = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(workspaceDir, Configuration.FILE_NAME))); + } + catch (JsonException) + { + logger.LogWarning("Could not read JSON file."); + } + catch (ArgumentException) + { + logger.LogWarning("No working directory path provided."); + } + catch (DirectoryNotFoundException) + { + logger.LogWarning("Directory could not be found."); + } + catch (FileNotFoundException) + { + logger.LogWarning("File could not be found."); + } + finally + { + if (configuration == null) + { + configuration = new Configuration(); + } + } + + ProductListingCacheData listingData = null; + try + { + listingData = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(workspaceDir, ProductListingCacheData.FILE_NAME))); + if (listingData.LastUpdatedUtc - DateTime.UtcNow > TimeSpan.FromMilliseconds(configuration.CacheLifespan)) + { + listingData = null; + } + } + catch (JsonException) + { + logger.LogWarning("Could not read JSON file."); + } + catch (ArgumentException) + { + logger.LogWarning("No working directory path provided."); + } + catch (DirectoryNotFoundException) + { + logger.LogWarning("Directory could not be found."); + } + catch (FileNotFoundException) + { + logger.LogWarning("File could not be found."); + } + finally + { + if (configuration == null) + { + configuration = new Configuration(); + } + } + LiveProductListingManager productListingManager = new LiveProductListingManager(http, loggerFactory.CreateLogger(), listingData?.ProductListings, configuration.MinDownloadInterval); + this.searchManager = new SearchManager(productListingManager, configuration.Similarity); + productListingManager.StartUpdateTimer(delay: 0, configuration.CacheLifespan); - logger = loggerFactory.CreateLogger(); } - public Task GetProductListingFromUrl(string url) + public async Task GetProductFromIdentifier(string identifier) { - return searchManager.ProductListingManager.GetProductListingFromUrl(url); + return await searchManager.ProductListingManager.GetProductListingFromIdentifier(identifier); } public IAsyncEnumerable Search(string query, Filters filters) @@ -73,5 +135,17 @@ namespace Props.Shop.Adafruit Dispose(disposing: true); GC.SuppressFinalize(this); } + + public async ValueTask SaveData() + { + if (workspaceDir != null) + { + await File.WriteAllTextAsync(Path.Combine(workspaceDir, Configuration.FILE_NAME), JsonSerializer.Serialize(configuration)); + await File.WriteAllTextAsync( + Path.Combine(workspaceDir, configuration.ProductListingCacheFileName), + JsonSerializer.Serialize(new ProductListingCacheData(await searchManager.ProductListingManager.ProductListings)) + ); + } + } } } diff --git a/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs index 558e543..c2f7839 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs @@ -7,11 +7,11 @@ namespace Props.Shop.Adafruit.Api { public interface IProductListingManager : IDisposable { - public Task>> ProductListings { get; } + public Task>> ProductListings { get; } public void RefreshProductListings(); public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5); public void StopUpdateTimer(); - public Task GetProductListingFromUrl(string url); + public Task GetProductListingFromIdentifier(string url); } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs b/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs index c454584..16064ea 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/ListingsParser.cs @@ -16,6 +16,7 @@ namespace Props.Shop.Adafruit.Api { using (StreamReader streamReader = new StreamReader(stream)) { + DateTime startTime = DateTime.UtcNow; dynamic data = JArray.Load(new JsonTextReader(streamReader)); List parsed = new List(); foreach (dynamic item in data) @@ -23,6 +24,7 @@ namespace Props.Shop.Adafruit.Api if (item.products_discontinued == 0) { ProductListing res = new ProductListing(); + res.TimeFetchedUtc = startTime; res.Name = item.product_name; res.LowerPrice = item.product_price; res.UpperPrice = res.LowerPrice; @@ -40,6 +42,7 @@ namespace Props.Shop.Adafruit.Api res.URL = item.product_url; res.InStock = item.product_stock > 0; parsed.Add(res); + res.Identifier = res.URL; } } ProductListings = parsed; diff --git a/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs index 28e2fde..411c6f6 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs @@ -16,20 +16,21 @@ namespace Props.Shop.Adafruit.Api private int minDownloadInterval; private DateTime? lastDownload; private object refreshLock = new object(); - private volatile Task>> productListingsTask; + private volatile Task>> productListingsTask; - public Task>> ProductListings => productListingsTask; - private readonly ConcurrentDictionary activeProductListingUrls = new ConcurrentDictionary(); + public Task>> ProductListings => productListingsTask; + private readonly ConcurrentDictionary identifierMap = new ConcurrentDictionary(); private ProductListingsParser parser = new ProductListingsParser(); private HttpClient httpClient; private Timer updateTimer; - public LiveProductListingManager(HttpClient httpClient, ILogger logger, int minDownloadInterval = 5 * 60 * 1000) + public LiveProductListingManager(HttpClient httpClient, ILogger logger, IReadOnlyDictionary> productListings = null, int minDownloadInterval = 5 * 60 * 1000) { this.logger = logger; this.minDownloadInterval = minDownloadInterval; this.httpClient = httpClient; + productListingsTask = Task.FromResult(productListings); } public void RefreshProductListings() @@ -44,14 +45,14 @@ namespace Props.Shop.Adafruit.Api } } - public async Task GetProductListingFromUrl(string url) + public async Task GetProductListingFromIdentifier(string identifier) { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); await productListingsTask; - return activeProductListingUrls[url]; + return identifierMap[identifier]; } - private async Task>> DownloadListings() + private async Task>> DownloadListings() { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); logger.LogDebug("Beginning listing database download."); @@ -59,10 +60,10 @@ namespace Props.Shop.Adafruit.Api parser.BuildProductListings(responseMessage.Content.ReadAsStream()); logger.LogDebug("Listing database parsed."); Dictionary> listingNames = new Dictionary>(); - activeProductListingUrls.Clear(); + identifierMap.Clear(); foreach (ProductListing product in parser.ProductListings) { - activeProductListingUrls.TryAdd(product.URL, product); + identifierMap.TryAdd(product.Identifier, product); IList sameProducts = listingNames.GetValueOrDefault(product.Name); if (sameProducts == null) { diff --git a/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs index ae73b21..ede16c6 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using FuzzySharp; @@ -26,7 +27,8 @@ namespace Props.Shop.Adafruit.Api if (ProductListingManager.ProductListings == null) { ProductListingManager.RefreshProductListings(); } - IDictionary> productListings = await ProductListingManager.ProductListings; + IReadOnlyDictionary> productListings = await ProductListingManager.ProductListings; + if (productListings == null) throw new InvalidAsynchronousStateException("productListings can't be null"); foreach (ExtractedResult listingNames in Process.ExtractAll(query, productListings.Keys, cutoff: (int)(Similarity * 100))) { foreach (ProductListing same in productListings[listingNames.Value]) diff --git a/Props-Modules/Props.Shop/Adafruit/Configuration.cs b/Props-Modules/Props.Shop/Adafruit/Configuration.cs deleted file mode 100644 index 25a1832..0000000 --- a/Props-Modules/Props.Shop/Adafruit/Configuration.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Props.Shop.Adafruit -{ - public class Configuration - { - public int MinDownloadInterval { get; set; } - - public float Similarity { get; set; } - - public Configuration() - { - MinDownloadInterval = 5 * 60 * 1000; - Similarity = 0.8f; - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Persistence/Configuration.cs b/Props-Modules/Props.Shop/Adafruit/Persistence/Configuration.cs new file mode 100644 index 0000000..f9358f0 --- /dev/null +++ b/Props-Modules/Props.Shop/Adafruit/Persistence/Configuration.cs @@ -0,0 +1,19 @@ +namespace Props.Shop.Adafruit.Persistence +{ + public class Configuration + { + public const string FILE_NAME = "config.json"; + public int MinDownloadInterval { get; set; } + public int CacheLifespan { get; set; } + public float Similarity { get; set; } + public string ProductListingCacheFileName { get; set; } + + public Configuration() + { + MinDownloadInterval = 5 * 60 * 1000; + Similarity = 0.8f; + CacheLifespan = 5 * 60 * 1000; + ProductListingCacheFileName = "ProductListings.json"; + } + } +} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Persistence/ProductListingCacheData.cs b/Props-Modules/Props.Shop/Adafruit/Persistence/ProductListingCacheData.cs new file mode 100644 index 0000000..a8e0654 --- /dev/null +++ b/Props-Modules/Props.Shop/Adafruit/Persistence/ProductListingCacheData.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Props.Shop.Framework; + +namespace Props.Shop.Adafruit.Persistence +{ + public class ProductListingCacheData + { + public const string FILE_NAME = "Product-listing-cache.json"; + public DateTime LastUpdatedUtc { get; private set; } + public IReadOnlyDictionary> ProductListings { get; set; } + + public ProductListingCacheData(IReadOnlyDictionary> productListings) + { + this.ProductListings = productListings; + LastUpdatedUtc = DateTime.UtcNow; + } + + public ProductListingCacheData() + { + } + } +} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Ebay/Actions/SearchRequest.cs b/Props-Modules/Props.Shop/Ebay/Actions/SearchRequest.cs deleted file mode 100644 index 8a6b6c3..0000000 --- a/Props-Modules/Props.Shop/Ebay/Actions/SearchRequest.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Props.Shop.Framework; - -namespace Props.Shop.Ebay.Actions -{ - public class SearchRequest : IAsyncEnumerable - { - private HttpClient http; - private string[] query; - public SearchRequest(HttpClient http, string[] query) - { - this.http = http ?? throw new ArgumentNullException("http"); - this.query = query ?? throw new ArgumentNullException("query"); - } - - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - { - throw new System.NotImplementedException(); - } - - public class Enumerator : IAsyncEnumerator - { - private HttpClient http; - private string[] query; - - public Enumerator(HttpClient http, string[] query) - { - this.http = http; - this.query = query; - } - - public ProductListing Current { get; private set; } - - public ValueTask MoveNextAsync() - { - // TODO: Implement this. - throw new System.NotImplementedException(); - } - public ValueTask DisposeAsync() - { - // TODO: Implement this. - throw new System.NotImplementedException(); - } - - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchResultParser.cs b/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchResultParser.cs deleted file mode 100644 index 3ce4850..0000000 --- a/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchResultParser.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using Props.Shop.Framework; - -namespace Props.Shop.Ebay.Api.ItemSummary -{ - public class SearchResultParser - { - private dynamic data; - public int BeginIndex => data.offset; - public int EndIndex => BeginIndex + data.limit; - public IEnumerable ProductListings { get; private set; } - - public SearchResultParser(string result) - { - data = JObject.Parse(result); - - List parsed = new List(); - foreach (dynamic itemSummary in data.itemSummaries) - { - ProductListing listing = new ProductListing(); - // TODO: Finish parsing the data. - parsed.Add(listing); - } - ProductListings = parsed; - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchUriBuilder.cs b/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchUriBuilder.cs deleted file mode 100644 index 17904ee..0000000 --- a/Props-Modules/Props.Shop/Ebay/Api/ItemSummary/SearchUriBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Props.Shop.Ebay.Api.ItemSummary -{ - public class SearchUriBuilder - { - UriBuilder uriBuilder = new UriBuilder("/search"); - private HashSet queries = new HashSet(); - private bool autoCorrect = false; - private int? maxResults = 100; - private int? offset = 0; - public bool AutoCorrect - { - set - { - autoCorrect = value; - } - } - - public int? MaxResults - { - set - { - maxResults = value; - } - } - - public int? Offset - { - set - { - offset = value; - } - } - - public void AddSearchQuery(string query) - { - queries.Add(query); - } - - public Uri Build() - { - StringBuilder queryBuilder = new StringBuilder("q="); - queryBuilder.Append('('); - queryBuilder.AppendJoin(", ", queries); - queryBuilder.Append(')'); - uriBuilder.Query += queryBuilder.ToString(); - - if (autoCorrect) uriBuilder.Query += "&auto_correct=KEYWORD"; - if (maxResults.HasValue) uriBuilder.Query += "&limit=" + maxResults.Value; - if (offset.HasValue) uriBuilder.Query += "&offset=" + offset.Value; - return uriBuilder.Uri; - } - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Ebay/Configuration.cs b/Props-Modules/Props.Shop/Ebay/Configuration.cs deleted file mode 100644 index 0ac57b4..0000000 --- a/Props-Modules/Props.Shop/Ebay/Configuration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Props.Shop.Ebay -{ - public class Configuration - { - public bool Sandbox { get; set; } = true; - } -} \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Ebay/EbayShop.cs b/Props-Modules/Props.Shop/Ebay/EbayShop.cs deleted file mode 100644 index ad3a5f0..0000000 --- a/Props-Modules/Props.Shop/Ebay/EbayShop.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Props.Shop.Framework; - -namespace Props.Shop.Ebay -{ - public class EbayShop : IShop - { - private bool disposedValue; - - public string ShopName => "Ebay"; - - public string ShopDescription => "A multi-national online store host to consumer-to-consumer and business-to-consumer sales."; - - public string ShopModuleAuthor => "Reslate"; - - public SupportedFeatures SupportedFeatures => new SupportedFeatures( - true, - true, - true, - true, - true - ); - - Configuration configuration; - - private HttpClient httpClient; - - public void Initialize(string workspaceDir) - { - httpClient = new HttpClient(); - configuration = new Configuration(); // TODO: Implement config persistence. - } - - - public Task InitializeAsync(string workspaceDir) - { - throw new NotImplementedException(); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - httpClient.Dispose(); - } - disposedValue = true; - } - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public byte[] GetDataForPersistence() - { - return JsonSerializer.SerializeToUtf8Bytes(configuration); - } - - public IEnumerable Search(string query, Filters filters) - { - // TODO: Implement the search system. - throw new NotImplementedException(); - } - } -} diff --git a/Props-Modules/Props.Shop/Ebay/Props.Shop.Ebay.csproj b/Props-Modules/Props.Shop/Ebay/Props.Shop.Ebay.csproj deleted file mode 100644 index aa7e6a8..0000000 --- a/Props-Modules/Props.Shop/Ebay/Props.Shop.Ebay.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - net5.0 - - - diff --git a/Props-Modules/Props.Shop/Framework/IShop.cs b/Props-Modules/Props.Shop/Framework/IShop.cs index 341ffd0..c4b3d70 100644 --- a/Props-Modules/Props.Shop/Framework/IShop.cs +++ b/Props-Modules/Props.Shop/Framework/IShop.cs @@ -15,9 +15,10 @@ namespace Props.Shop.Framework public IAsyncEnumerable Search(string query, Filters filters); - public Task GetProductListingFromUrl(string url); + public Task GetProductFromIdentifier(string identifier); void Initialize(string workspaceDir, ILoggerFactory loggerFactory); + ValueTask SaveData(); public SupportedFeatures SupportedFeatures { get; } } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Framework/ProductListing.cs b/Props-Modules/Props.Shop/Framework/ProductListing.cs index bb9f356..a3d4754 100644 --- a/Props-Modules/Props.Shop/Framework/ProductListing.cs +++ b/Props-Modules/Props.Shop/Framework/ProductListing.cs @@ -1,3 +1,5 @@ +using System; + namespace Props.Shop.Framework { public class ProductListing @@ -13,5 +15,41 @@ namespace Props.Shop.Framework public int? ReviewCount { get; set; } public bool ConvertedPrices { get; set; } public bool? InStock { get; set; } + public string Identifier { get; set; } + public DateTime TimeFetchedUtc { get; set; } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + ProductListing other = obj as ProductListing; + return + this.LowerPrice == other.LowerPrice && + this.UpperPrice == other.UpperPrice && + this.Shipping == other.Shipping && + this.Name == other.Name && + this.URL == other.URL && + this.ImageURL == other.ImageURL && + this.Rating == other.Rating && + this.PurchaseCount == other.PurchaseCount && + this.ReviewCount == other.ReviewCount && + this.ConvertedPrices == other.ConvertedPrices && + this.InStock == other.InStock && + this.Identifier == other.Identifier && + this.TimeFetchedUtc == other.TimeFetchedUtc; + } + + public override int GetHashCode() + { + return (Name, URL, UpperPrice, LowerPrice, ImageURL).GetHashCode(); + } + + public ProductListing Copy() + { + return MemberwiseClone() as ProductListing; + } } } \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs index cf37472..a81414d 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Props.Shop.Framework; using Xunit; diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs index 61ad595..e08c814 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs @@ -11,31 +11,29 @@ namespace Props.Shop.Adafruit.Tests.Api { public class FakeProductListingManager : IProductListingManager { - private Timer refreshTimer; private bool disposedValue; - private volatile Task>> activeListings; private DateTime? lastDownload; private ProductListingsParser parser = new ProductListingsParser(); private readonly ConcurrentDictionary activeProductListingUrls = new ConcurrentDictionary(); - public Task>> ProductListings => activeListings; + public Task>> ProductListings { get; private set; } - public async Task GetProductListingFromUrl(string url) + public async Task GetProductListingFromIdentifier(string url) { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); - await activeListings; + await ProductListings; return activeProductListingUrls[url]; } public void RefreshProductListings() { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); - if ((lastDownload != null && DateTime.UtcNow - lastDownload <= TimeSpan.FromMilliseconds(5 * 60 * 1000)) || (activeListings != null && !activeListings.IsCompleted)) return; - activeListings = DownloadListings(); + if ((lastDownload != null && DateTime.UtcNow - lastDownload <= TimeSpan.FromMilliseconds(5 * 60 * 1000)) || (ProductListings != null && !ProductListings.IsCompleted)) return; + ProductListings = DownloadListings(); } - private Task>> DownloadListings() { + private Task>> DownloadListings() { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); lastDownload = DateTime.UtcNow; parser.BuildProductListings(File.OpenRead("./Assets/products.json")); @@ -52,24 +50,16 @@ namespace Props.Shop.Adafruit.Tests.Api sameProducts.Add(product); } - return Task.FromResult>>(listingNames); + return Task.FromResult>>(listingNames); } public void StartUpdateTimer(int delay = 300000, int period = 300000) { - if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); - if (refreshTimer != null) throw new InvalidOperationException("Refresh timer already running."); - refreshTimer = new Timer((state) => { - RefreshProductListings(); - }, null, delay, period); + RefreshProductListings(); } public void StopUpdateTimer() { - if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); - if (refreshTimer == null) throw new InvalidOperationException("Refresh timer not running."); - refreshTimer.Dispose(); - refreshTimer = null; } protected virtual void Dispose(bool disposing) @@ -78,8 +68,6 @@ namespace Props.Shop.Adafruit.Tests.Api { if (disposing) { - refreshTimer?.Dispose(); - refreshTimer = null; } disposedValue = true; diff --git a/Props/Data/ApplicationDbContext.cs b/Props/Data/ApplicationDbContext.cs index 339597c..d9febdc 100644 --- a/Props/Data/ApplicationDbContext.cs +++ b/Props/Data/ApplicationDbContext.cs @@ -31,40 +31,52 @@ namespace Props.Data base.OnModelCreating(modelBuilder); modelBuilder.Entity() - .Property(e => e.Order) - .HasConversion( - v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize>(v, null), - new ValueComparer>( - (a, b) => a.SequenceEqual(b), - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), - c => (IList)c.ToList() - ) - ); + .Property(e => e.Order) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize>(v, null), + new ValueComparer>( + (a, b) => a.SequenceEqual(b), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => (IList)c.ToList() + ) + ); modelBuilder.Entity() - .Property(e => e.Enabled) - .HasConversion( - v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize(v, null), - new ValueComparer( - (a, b) => a.Equals(b), - c => c.GetHashCode(), - c => c.Copy() - ) - ); + .Property(e => e.Enabled) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize(v, null), + new ValueComparer( + (a, b) => a.Equals(b), + c => c.GetHashCode(), + c => c.Copy() + ) + ); modelBuilder.Entity() - .Property(e => e.Filters) - .HasConversion( - v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize(v, null), - new ValueComparer( - (a, b) => a.Equals(b), - c => c.GetHashCode(), - c => c.Copy() - ) - ); + .Property(e => e.Filters) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize(v, null), + new ValueComparer( + (a, b) => a.Equals(b), + c => c.GetHashCode(), + c => c.Copy() + ) + ); + + modelBuilder.Entity() + .Property(e => e.ProductListing) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize(v, null), + new ValueComparer( + (a, b) => a.Equals(b), + c => c.GetHashCode(), + c => c.Copy() + ) + ); } } } diff --git a/Props/Data/Migrations/20210805055109_InitialCreate.Designer.cs b/Props/Data/Migrations/20210809194646_InitialCreate.Designer.cs similarity index 98% rename from Props/Data/Migrations/20210805055109_InitialCreate.Designer.cs rename to Props/Data/Migrations/20210809194646_InitialCreate.Designer.cs index 989043f..e0c0b92 100644 --- a/Props/Data/Migrations/20210805055109_InitialCreate.Designer.cs +++ b/Props/Data/Migrations/20210809194646_InitialCreate.Designer.cs @@ -9,7 +9,7 @@ using Props.Data; namespace Props.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20210805055109_InitialCreate")] + [Migration("20210809194646_InitialCreate")] partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -185,13 +185,10 @@ namespace Props.Data.Migrations b.Property("Hits") .HasColumnType("INTEGER"); - b.Property("LastUpdated") + b.Property("ProductListing") .HasColumnType("TEXT"); - b.Property("ProductName") - .HasColumnType("TEXT"); - - b.Property("ProductUrl") + b.Property("ProductListingIdentifier") .HasColumnType("TEXT"); b.Property("ShopName") diff --git a/Props/Data/Migrations/20210805055109_InitialCreate.cs b/Props/Data/Migrations/20210809194646_InitialCreate.cs similarity index 98% rename from Props/Data/Migrations/20210805055109_InitialCreate.cs rename to Props/Data/Migrations/20210809194646_InitialCreate.cs index ad3a704..a9b4715 100644 --- a/Props/Data/Migrations/20210805055109_InitialCreate.cs +++ b/Props/Data/Migrations/20210809194646_InitialCreate.cs @@ -68,9 +68,8 @@ namespace Props.Data.Migrations .Annotation("Sqlite:Autoincrement", true), ShopName = table.Column(type: "TEXT", nullable: true), Hits = table.Column(type: "INTEGER", nullable: false), - LastUpdated = table.Column(type: "TEXT", nullable: false), - ProductUrl = table.Column(type: "TEXT", nullable: true), - ProductName = table.Column(type: "TEXT", nullable: true) + ProductListing = table.Column(type: "TEXT", nullable: true), + ProductListingIdentifier = table.Column(type: "TEXT", nullable: true) }, constraints: table => { diff --git a/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 71e8ac7..af01399 100644 --- a/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -183,13 +183,10 @@ namespace Props.Data.Migrations b.Property("Hits") .HasColumnType("INTEGER"); - b.Property("LastUpdated") + b.Property("ProductListing") .HasColumnType("TEXT"); - b.Property("ProductName") - .HasColumnType("TEXT"); - - b.Property("ProductUrl") + b.Property("ProductListingIdentifier") .HasColumnType("TEXT"); b.Property("ShopName") diff --git a/Props/Extensions/ProductListingExtensions.cs b/Props/Extensions/ProductListingExtensions.cs index da780a5..8462849 100644 --- a/Props/Extensions/ProductListingExtensions.cs +++ b/Props/Extensions/ProductListingExtensions.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Builder; using Props.Shop.Framework; namespace Props.Extensions @@ -10,5 +11,7 @@ namespace Props.Extensions int purchaseFactor = productListing.PurchaseCount.HasValue ? productListing.PurchaseCount.Value : 1; return (productListing.Rating * (reviewFactor > purchaseFactor ? reviewFactor : purchaseFactor)) / (productListing.LowerPrice * productListing.UpperPrice); } + + } } \ No newline at end of file diff --git a/Props/Models/Search/ProductListingInfo.cs b/Props/Models/Search/ProductListingInfo.cs index aad4dea..e49f0a3 100644 --- a/Props/Models/Search/ProductListingInfo.cs +++ b/Props/Models/Search/ProductListingInfo.cs @@ -11,10 +11,8 @@ namespace Props.Models.Search public uint Hits { get; set; } - public DateTime LastUpdated { get; set; } + public ProductListing ProductListing { get; set; } - public string ProductUrl { get; set; } - - public string ProductName { get; set; } + public string ProductListingIdentifier { get; set; } } } \ No newline at end of file diff --git a/Props/Services/Modules/LiveMetricsManager.cs b/Props/Services/Modules/LiveMetricsManager.cs index 19f6ded..ea2201c 100644 --- a/Props/Services/Modules/LiveMetricsManager.cs +++ b/Props/Services/Modules/LiveMetricsManager.cs @@ -68,16 +68,15 @@ namespace Props.Services.Modules { ProductListingInfo productListingInfo = (from info in dbContext.ProductListingInfos - where info.ProductUrl.Equals(productListing.URL) + where info.ProductListingIdentifier.Equals(productListing.Identifier) select info).SingleOrDefault() ?? new ProductListingInfo(); if (productListingInfo.Hits == 0) { dbContext.Add(productListingInfo); } productListingInfo.ShopName = shopName; - productListingInfo.ProductName = productListing.Name; - productListingInfo.ProductUrl = productListing.URL; - productListingInfo.LastUpdated = DateTime.UtcNow; + productListingInfo.ProductListing = productListing; + productListingInfo.ProductListingIdentifier = productListing.Identifier; productListingInfo.Hits += 1; dbContext.SaveChanges(); } diff --git a/Props/Services/Modules/ModularShopManager.cs b/Props/Services/Modules/ModularShopManager.cs index c9fbc5b..3242f9a 100644 --- a/Props/Services/Modules/ModularShopManager.cs +++ b/Props/Services/Modules/ModularShopManager.cs @@ -55,7 +55,6 @@ namespace Props.Services.Modules public void LoadShops() { - // TODO: Figure out how to best call this. string shopsDir = options.ModulesDir; string shopRegex = options.ShopRegex; bool recursiveLoad = options.RecursiveLoad; @@ -87,7 +86,8 @@ namespace Props.Services.Modules IShop shop = Activator.CreateInstance(type) as IShop; if (shop != null) { - DirectoryInfo dataDir = Directory.CreateDirectory(Path.Combine(options.ModuleDataDir, file)); + DirectoryInfo dataDir = Directory.CreateDirectory(Path.Combine(options.ModuleDataDir, file.Substring(file.IndexOf(Path.DirectorySeparatorChar) + 1))); + logger.LogDebug("Checking data directory for \"{0}\" at \"{1}\"", Path.GetFileName(file), dataDir.FullName); shop.Initialize(dataDir.FullName, loggerFactory); success += 1; if (!shops.TryAdd(shop.ShopName, shop)) @@ -105,8 +105,6 @@ namespace Props.Services.Modules } } } - logger.LogDebug("Waiting for all shops to finish asynchronous initialization."); - logger.LogDebug("All shops finished asynchronous initialization."); } protected virtual void Dispose(bool disposing) @@ -117,7 +115,7 @@ namespace Props.Services.Modules { foreach (string shopName in shops.Keys) { - // TODO: Get shop data to persist. + shops[shopName].SaveData().AsTask().Wait(); shops[shopName].Dispose(); } } diff --git a/Props/shops/Props.Shop.Adafruit.dll b/Props/shops/Props.Shop.Adafruit.dll index be966c0..04ed1cd 100644 Binary files a/Props/shops/Props.Shop.Adafruit.dll and b/Props/shops/Props.Shop.Adafruit.dll differ diff --git a/Props/shops/Props.Shop.Framework.dll b/Props/shops/Props.Shop.Framework.dll deleted file mode 100644 index cadba8b..0000000 Binary files a/Props/shops/Props.Shop.Framework.dll and /dev/null differ