Added identifier and fetch time product listings; Added persistence to AdafruitShop.

Implemented in AdafruitShop.

AdafruitShop Product listing data now persisted.

AdafruitShop configuration now persisted.
This commit is contained in:
Harrison Deng 2021-08-09 13:32:16 -05:00
parent 38ffb3c7e1
commit 0b507b90a1
28 changed files with 251 additions and 349 deletions

View File

@ -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<AdafruitShop> 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<AdafruitShop>();
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<LiveProductListingManager>(), configuration.MinDownloadInterval);
this.searchManager = new SearchManager(productListingManager, configuration.Similarity);
productListingManager.StartUpdateTimer(delay: 0);
try
{
configuration = JsonSerializer.Deserialize<Configuration>(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<ProductListingCacheData>(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<LiveProductListingManager>(), listingData?.ProductListings, configuration.MinDownloadInterval);
this.searchManager = new SearchManager(productListingManager, configuration.Similarity);
productListingManager.StartUpdateTimer(delay: 0, configuration.CacheLifespan);
logger = loggerFactory.CreateLogger<AdafruitShop>();
}
public Task<ProductListing> GetProductListingFromUrl(string url)
public async Task<ProductListing> GetProductFromIdentifier(string identifier)
{
return searchManager.ProductListingManager.GetProductListingFromUrl(url);
return await searchManager.ProductListingManager.GetProductListingFromIdentifier(identifier);
}
public IAsyncEnumerable<ProductListing> 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))
);
}
}
}
}

View File

@ -7,11 +7,11 @@ namespace Props.Shop.Adafruit.Api
{
public interface IProductListingManager : IDisposable
{
public Task<IDictionary<string, IList<ProductListing>>> ProductListings { get; }
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings { get; }
public void RefreshProductListings();
public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5);
public void StopUpdateTimer();
public Task<ProductListing> GetProductListingFromUrl(string url);
public Task<ProductListing> GetProductListingFromIdentifier(string url);
}
}

View File

@ -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<ProductListing> parsed = new List<ProductListing>();
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;

View File

@ -16,20 +16,21 @@ namespace Props.Shop.Adafruit.Api
private int minDownloadInterval;
private DateTime? lastDownload;
private object refreshLock = new object();
private volatile Task<IDictionary<string, IList<ProductListing>>> productListingsTask;
private volatile Task<IReadOnlyDictionary<string, IList<ProductListing>>> productListingsTask;
public Task<IDictionary<string, IList<ProductListing>>> ProductListings => productListingsTask;
private readonly ConcurrentDictionary<string, ProductListing> activeProductListingUrls = new ConcurrentDictionary<string, ProductListing>();
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings => productListingsTask;
private readonly ConcurrentDictionary<string, ProductListing> identifierMap = new ConcurrentDictionary<string, ProductListing>();
private ProductListingsParser parser = new ProductListingsParser();
private HttpClient httpClient;
private Timer updateTimer;
public LiveProductListingManager(HttpClient httpClient, ILogger<LiveProductListingManager> logger, int minDownloadInterval = 5 * 60 * 1000)
public LiveProductListingManager(HttpClient httpClient, ILogger<LiveProductListingManager> logger, IReadOnlyDictionary<string, IList<ProductListing>> 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<ProductListing> GetProductListingFromUrl(string url)
public async Task<ProductListing> GetProductListingFromIdentifier(string identifier)
{
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
await productListingsTask;
return activeProductListingUrls[url];
return identifierMap[identifier];
}
private async Task<IDictionary<string, IList<ProductListing>>> DownloadListings()
private async Task<IReadOnlyDictionary<string, IList<ProductListing>>> 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<string, IList<ProductListing>> listingNames = new Dictionary<string, IList<ProductListing>>();
activeProductListingUrls.Clear();
identifierMap.Clear();
foreach (ProductListing product in parser.ProductListings)
{
activeProductListingUrls.TryAdd(product.URL, product);
identifierMap.TryAdd(product.Identifier, product);
IList<ProductListing> sameProducts = listingNames.GetValueOrDefault(product.Name);
if (sameProducts == null)
{

View File

@ -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<string, IList<ProductListing>> productListings = await ProductListingManager.ProductListings;
IReadOnlyDictionary<string, IList<ProductListing>> productListings = await ProductListingManager.ProductListings;
if (productListings == null) throw new InvalidAsynchronousStateException("productListings can't be null");
foreach (ExtractedResult<string> listingNames in Process.ExtractAll(query, productListings.Keys, cutoff: (int)(Similarity * 100)))
{
foreach (ProductListing same in productListings[listingNames.Value])

View File

@ -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;
}
}
}

View File

@ -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";
}
}
}

View File

@ -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<string, IList<ProductListing>> ProductListings { get; set; }
public ProductListingCacheData(IReadOnlyDictionary<string, IList<ProductListing>> productListings)
{
this.ProductListings = productListings;
LastUpdatedUtc = DateTime.UtcNow;
}
public ProductListingCacheData()
{
}
}
}

View File

@ -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<ProductListing>
{
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<ProductListing> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
public class Enumerator : IAsyncEnumerator<ProductListing>
{
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<bool> MoveNextAsync()
{
// TODO: Implement this.
throw new System.NotImplementedException();
}
public ValueTask DisposeAsync()
{
// TODO: Implement this.
throw new System.NotImplementedException();
}
}
}
}

View File

@ -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<ProductListing> ProductListings { get; private set; }
public SearchResultParser(string result)
{
data = JObject.Parse(result);
List<ProductListing> parsed = new List<ProductListing>();
foreach (dynamic itemSummary in data.itemSummaries)
{
ProductListing listing = new ProductListing();
// TODO: Finish parsing the data.
parsed.Add(listing);
}
ProductListings = parsed;
}
}
}

View File

@ -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<string> queries = new HashSet<string>();
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;
}
}
}

View File

@ -1,7 +0,0 @@
namespace Props.Shop.Ebay
{
public class Configuration
{
public bool Sandbox { get; set; } = true;
}
}

View File

@ -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<ProductListing> Search(string query, Filters filters)
{
// TODO: Implement the search system.
throw new NotImplementedException();
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Framework\Props.Shop.Framework.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -15,9 +15,10 @@ namespace Props.Shop.Framework
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters);
public Task<ProductListing> GetProductListingFromUrl(string url);
public Task<ProductListing> GetProductFromIdentifier(string identifier);
void Initialize(string workspaceDir, ILoggerFactory loggerFactory);
ValueTask SaveData();
public SupportedFeatures SupportedFeatures { get; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -11,31 +11,29 @@ namespace Props.Shop.Adafruit.Tests.Api
{
public class FakeProductListingManager : IProductListingManager
{
private Timer refreshTimer;
private bool disposedValue;
private volatile Task<IDictionary<string, IList<ProductListing>>> activeListings;
private DateTime? lastDownload;
private ProductListingsParser parser = new ProductListingsParser();
private readonly ConcurrentDictionary<string, ProductListing> activeProductListingUrls = new ConcurrentDictionary<string, ProductListing>();
public Task<IDictionary<string, IList<ProductListing>>> ProductListings => activeListings;
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings { get; private set; }
public async Task<ProductListing> GetProductListingFromUrl(string url)
public async Task<ProductListing> 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<IDictionary<string, IList<ProductListing>>> DownloadListings() {
private Task<IReadOnlyDictionary<string, IList<ProductListing>>> 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<IDictionary<string, IList<ProductListing>>>(listingNames);
return Task.FromResult<IReadOnlyDictionary<string, IList<ProductListing>>>(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;

View File

@ -31,40 +31,52 @@ namespace Props.Data
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ResultsPreferences>()
.Property(e => e.Order)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<ResultsPreferences.Category>>(v, null),
new ValueComparer<IList<ResultsPreferences.Category>>(
(a, b) => a.SequenceEqual(b),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<ResultsPreferences.Category>)c.ToList()
)
);
.Property(e => e.Order)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<ResultsPreferences.Category>>(v, null),
new ValueComparer<IList<ResultsPreferences.Category>>(
(a, b) => a.SequenceEqual(b),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<ResultsPreferences.Category>)c.ToList()
)
);
modelBuilder.Entity<SearchOutline>()
.Property(e => e.Enabled)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
new ValueComparer<SearchOutline.ShopsDisabled>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()
)
);
.Property(e => e.Enabled)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
new ValueComparer<SearchOutline.ShopsDisabled>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()
)
);
modelBuilder.Entity<SearchOutline>()
.Property(e => e.Filters)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<Filters>(v, null),
new ValueComparer<Filters>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()
)
);
.Property(e => e.Filters)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<Filters>(v, null),
new ValueComparer<Filters>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()
)
);
modelBuilder.Entity<ProductListingInfo>()
.Property(e => e.ProductListing)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<ProductListing>(v, null),
new ValueComparer<ProductListing>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()
)
);
}
}
}

View File

@ -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<uint>("Hits")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastUpdated")
b.Property<string>("ProductListing")
.HasColumnType("TEXT");
b.Property<string>("ProductName")
.HasColumnType("TEXT");
b.Property<string>("ProductUrl")
b.Property<string>("ProductListingIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ShopName")

View File

@ -68,9 +68,8 @@ namespace Props.Data.Migrations
.Annotation("Sqlite:Autoincrement", true),
ShopName = table.Column<string>(type: "TEXT", nullable: true),
Hits = table.Column<uint>(type: "INTEGER", nullable: false),
LastUpdated = table.Column<DateTime>(type: "TEXT", nullable: false),
ProductUrl = table.Column<string>(type: "TEXT", nullable: true),
ProductName = table.Column<string>(type: "TEXT", nullable: true)
ProductListing = table.Column<string>(type: "TEXT", nullable: true),
ProductListingIdentifier = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{

View File

@ -183,13 +183,10 @@ namespace Props.Data.Migrations
b.Property<uint>("Hits")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastUpdated")
b.Property<string>("ProductListing")
.HasColumnType("TEXT");
b.Property<string>("ProductName")
.HasColumnType("TEXT");
b.Property<string>("ProductUrl")
b.Property<string>("ProductListingIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ShopName")

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

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

View File

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

Binary file not shown.

Binary file not shown.