diff --git a/src/BanggoodShop/BanggoodShop.csproj b/src/BanggoodShop/BanggoodShop.csproj
new file mode 100644
index 0000000..38c6a2a
--- /dev/null
+++ b/src/BanggoodShop/BanggoodShop.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ net5.0
+
+
+
diff --git a/src/BanggoodShop/Shop.cs b/src/BanggoodShop/Shop.cs
new file mode 100644
index 0000000..3f82e85
--- /dev/null
+++ b/src/BanggoodShop/Shop.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using MultiShop.ShopFramework;
+
+namespace BanggoodShop
+{
+ public class Shop : IShop
+ {
+ public bool UseProxy { get; set; } = true;
+ private bool disposedValue;
+
+ public string ShopName => "Banggood";
+
+ public string ShopDescription => "A online retailer based in China.";
+
+ public string ShopModuleAuthor => "Reslate";
+
+ private HttpClient http;
+ private string query;
+ private Currency currency;
+
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ return new ShopEnumerator(query, currency, http, UseProxy, cancellationToken);
+ }
+
+ public void Initialize()
+ {
+ this.http = new HttpClient();
+ }
+
+ public void SetupSession(string query, Currency currency)
+ {
+ this.query = query;
+ this.currency = currency;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ http.Dispose();
+ }
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/BanggoodShop/ShopEnumerator.cs b/src/BanggoodShop/ShopEnumerator.cs
new file mode 100644
index 0000000..3e53fc5
--- /dev/null
+++ b/src/BanggoodShop/ShopEnumerator.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using HtmlAgilityPack;
+using MultiShop.ShopFramework;
+using SimpleLogger;
+
+namespace BanggoodShop
+{
+ class ShopEnumerator : IAsyncEnumerator
+ {
+ const string PROXY_FORMAT = "https://cors.bridged.cc/{0}";
+ private const string QUERY_FORMAT = "https://www.banggood.com/search/{0}/0-0-0-1-1-60-0-price-0-0_p-{1}.html?DCC=CA¤cy={2}";
+ HttpClient http;
+ private string query;
+ private Currency currency;
+ private bool useProxy;
+ private CancellationToken cancellationToken;
+
+ private IEnumerator pageListings;
+ private int currentPage;
+ private DateTime lastScrape;
+
+ public ProductListing Current { get; private set; }
+
+ public ShopEnumerator(string query, Currency currency, HttpClient http, bool useProxy, CancellationToken cancellationToken)
+ {
+ query = query.Replace(' ', '-');
+ this.query = query;
+ this.currency = currency;
+ this.http = http;
+ this.useProxy = useProxy;
+ this.cancellationToken = cancellationToken;
+ }
+
+ private async Task> ScrapePage(int page)
+ {
+ string requestUrl = string.Format(QUERY_FORMAT, query, page, currency.ToString());
+ if (useProxy) requestUrl = string.Format(PROXY_FORMAT, requestUrl);
+ TimeSpan difference = DateTime.Now - lastScrape;
+ if (difference.TotalMilliseconds < 200) {
+ await Task.Delay((int)Math.Ceiling(200 - difference.TotalMilliseconds));
+ }
+ HttpResponseMessage response = await http.GetAsync(requestUrl);
+ lastScrape = DateTime.Now;
+ HtmlDocument html = new HtmlDocument();
+ html.Load(await response.Content.ReadAsStreamAsync());
+ HtmlNodeCollection collection = html.DocumentNode.SelectNodes(@"//div[@class='product-list']/ul[@class='goodlist cf']/li");
+ if (collection == null) return null;
+ List results = new List();
+ foreach (HtmlNode node in collection)
+ {
+ ProductListing listing = new ProductListing();
+ HtmlNode productNode = node.SelectSingleNode(@"div/a[1]");
+ listing.Name = productNode.InnerText;
+ Logger.Log($"Found name: {listing.Name}", LogLevel.Debug);
+ listing.URL = productNode.GetAttributeValue("href", null);
+ Logger.Log($"Found URL: {listing.URL}", LogLevel.Debug);
+ listing.ImageURL = node.SelectSingleNode(@"div/span[@class='img notranslate']/a/img").GetAttributeValue("data-src", null);
+ Logger.Log($"Found image URL: {listing.ImageURL}", LogLevel.Debug);
+ listing.LowerPrice = float.Parse(Regex.Match(node.SelectSingleNode(@"div/span[@class='price-box']/span").InnerText, @"(\d*\.\d*)").Groups[1].Value);
+ Logger.Log($"Found price: {listing.LowerPrice}", LogLevel.Debug);
+ listing.UpperPrice = listing.LowerPrice;
+ listing.ReviewCount = int.Parse(Regex.Match(node.SelectSingleNode(@"div/a[2]").InnerText, @"(\d+) reviews?").Groups[1].Value);
+ Logger.Log($"Found reviews: {listing.ReviewCount}", LogLevel.Debug);
+ results.Add(listing);
+ }
+ return results;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ public async ValueTask MoveNextAsync()
+ {
+ if (pageListings == null || !pageListings.MoveNext())
+ {
+ currentPage += 1;
+ pageListings?.Dispose();
+ IEnumerable pageEnumerable = await ScrapePage(currentPage);
+ if (pageEnumerable == null) return false;
+ pageListings = pageEnumerable.GetEnumerator();
+ pageListings.MoveNext();
+ }
+ Current = pageListings.Current;
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MultiShop/DataStructures/SearchProfile.cs b/src/MultiShop/DataStructures/SearchProfile.cs
index c5130b6..e0d2ed4 100644
--- a/src/MultiShop/DataStructures/SearchProfile.cs
+++ b/src/MultiShop/DataStructures/SearchProfile.cs
@@ -70,7 +70,7 @@ namespace MultiShop.DataStructures
set
{
- if (value == false && !CanDisableShop()) return;
+ if (value == false && !(shopsEnabled.Count > 1)) return;
if (value)
{
shopsEnabled.Add(name);
@@ -81,8 +81,8 @@ namespace MultiShop.DataStructures
}
}
}
- public bool CanDisableShop() {
- return shopsEnabled.Count > 1;
+ public bool IsToggleable(string shop) {
+ return (shopsEnabled.Contains(shop) && shopsEnabled.Count > 1) || !shopsEnabled.Contains(shop);
}
}
}
diff --git a/src/MultiShop/Pages/Search.razor b/src/MultiShop/Pages/Search.razor
index a504b34..9867f74 100644
--- a/src/MultiShop/Pages/Search.razor
+++ b/src/MultiShop/Pages/Search.razor
@@ -109,7 +109,7 @@
@foreach (string shop in Shops.Keys)
{
-
+
}
@@ -330,7 +330,7 @@
if (listing.Shipping == null && !activeProfile.keepUnknownShipping || (activeProfile.enableMaxShippingFee && listing.Shipping > activeProfile.MaxShippingFee)) continue;
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
if (!(listing.LowerPrice + shippingDifference >= activeProfile.lowerPrice && (!activeProfile.enableUpperPrice || listing.UpperPrice + shippingDifference <= activeProfile.UpperPrice))) continue;
- if ((listing.Rating == null && !activeProfile.keepUnrated) || activeProfile.minRating > (listing.Rating == null ? 0 : listing.Rating)) continue;
+ if ((listing.Rating == null && !activeProfile.keepUnrated) && activeProfile.minRating > (listing.Rating == null ? 0 : listing.Rating)) continue;
if ((listing.PurchaseCount == null && !activeProfile.keepUnknownPurchaseCount) || activeProfile.minPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) continue;
if ((listing.ReviewCount == null && !activeProfile.keepUnknownRatingCount) || activeProfile.minReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) continue;
diff --git a/src/MultiShop/Shared/MainLayout.razor b/src/MultiShop/Shared/MainLayout.razor
index d91bb71..c20b2fd 100644
--- a/src/MultiShop/Shared/MainLayout.razor
+++ b/src/MultiShop/Shared/MainLayout.razor
@@ -25,6 +25,7 @@
private bool modulesLoaded = false;
private Dictionary shops = new Dictionary();
+ private Dictionary unusedDependencies = new Dictionary();
protected override async Task OnInitializedAsync()
{
@@ -48,7 +49,7 @@
foreach (Task task in assemblyDownloadTasks)
{
Assembly assembly = AppDomain.CurrentDomain.Load(await task);
-
+ bool assigned = false;
foreach (Type type in assembly.GetTypes())
{
if (typeof(IShop).IsAssignableFrom(type)) {
@@ -58,17 +59,34 @@
shops.Add(shop.ShopName, shop);
Logger.Log($"Registered and started lifetime of module for \"{shop.ShopName}\".", LogLevel.Debug);
}
+ assigned = true;
}
}
+ if (!assigned) {
+ unusedDependencies.Add(assembly.FullName, assembly);
+ Logger.Log($"Assembly \"{assembly.FullName}\" did not contain a shop module. Storing it as potential extension.", LogLevel.Debug);
+ }
}
-
+ foreach (string assembly in unusedDependencies.Keys)
+ {
+ Logger.Log($"{assembly} was unused.", LogLevel.Warning);
+ }
+ unusedDependencies.Clear();
modulesLoaded = true;
}
private Assembly OnAssemblyDependencyRequest(object sender, ResolveEventArgs args) {
- Logger.Log($"Assembly {args.RequestingAssembly} is requesting dependency assembly {args.Name}. Attempting to retrieve...", LogLevel.Debug);
- return AppDomain.CurrentDomain.Load(Http.GetByteArrayAsync(Configuration["ModulesDir"] + args.Name + ".dll").Result);
+ Logger.Log($"Assembly {args.RequestingAssembly} is requesting dependency assembly {args.Name}.", LogLevel.Debug);
+ if (unusedDependencies.ContainsKey(args.Name))
+ {
+ Logger.Log("Dependency found.", LogLevel.Debug);
+ Assembly dependency = unusedDependencies[args.Name];
+ unusedDependencies.Remove(args.Name);
+ return dependency;
+ }
+ Logger.Log($"No dependency under name {args.Name}", LogLevel.Debug);
+ return null;
}
diff --git a/src/MultiShop/wwwroot/modules/BanggoodShop.dll b/src/MultiShop/wwwroot/modules/BanggoodShop.dll
new file mode 100644
index 0000000..e6051e5
Binary files /dev/null and b/src/MultiShop/wwwroot/modules/BanggoodShop.dll differ
diff --git a/src/MultiShop/wwwroot/modules/HtmlAgilityPack.dll b/src/MultiShop/wwwroot/modules/HtmlAgilityPack.dll
new file mode 100644
index 0000000..e18de30
Binary files /dev/null and b/src/MultiShop/wwwroot/modules/HtmlAgilityPack.dll differ
diff --git a/src/MultiShop/wwwroot/modules/modules_content.json b/src/MultiShop/wwwroot/modules/modules_content.json
index 8c4a535..6c67e7b 100644
--- a/src/MultiShop/wwwroot/modules/modules_content.json
+++ b/src/MultiShop/wwwroot/modules/modules_content.json
@@ -1,3 +1,5 @@
[
- "AliExpressShop"
+ "AliExpressShop",
+ "HtmlAgilityPack",
+ "BanggoodShop"
]
\ No newline at end of file
diff --git a/test/AliExpressShop.Tests/ShopTest.cs b/test/AliExpressShop.Tests/ShopTest.cs
index f09998f..0214025 100644
--- a/test/AliExpressShop.Tests/ShopTest.cs
+++ b/test/AliExpressShop.Tests/ShopTest.cs
@@ -1,7 +1,3 @@
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using GameServiceWarden.Core.Tests;
using MultiShop.ShopFramework;
using SimpleLogger;
using Xunit;
@@ -34,10 +30,10 @@ namespace AliExpressShop.Tests
count += 1;
if (count > MAX_RESULTS) return;
}
+ shop.Dispose();
}
[Fact]
-
public async void Search_USD_ResultsFound()
{
//Given
@@ -57,6 +53,7 @@ namespace AliExpressShop.Tests
count += 1;
if (count > MAX_RESULTS) return;
}
+ shop.Dispose();
}
}
}
diff --git a/test/AliExpressShop.Tests/XUnitLogger.cs b/test/AliExpressShop.Tests/XUnitLogger.cs
index 520149c..7fe2715 100644
--- a/test/AliExpressShop.Tests/XUnitLogger.cs
+++ b/test/AliExpressShop.Tests/XUnitLogger.cs
@@ -2,7 +2,7 @@ using System;
using SimpleLogger;
using Xunit.Abstractions;
-namespace GameServiceWarden.Core.Tests
+namespace AliExpressShop
{
public class XUnitLogger : ILogReceiver
{
diff --git a/test/BanggoodShop.Tests/BanggoodShop.Tests.csproj b/test/BanggoodShop.Tests/BanggoodShop.Tests.csproj
new file mode 100644
index 0000000..a421dbc
--- /dev/null
+++ b/test/BanggoodShop.Tests/BanggoodShop.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/test/BanggoodShop.Tests/ShopTest.cs b/test/BanggoodShop.Tests/ShopTest.cs
new file mode 100644
index 0000000..9fe8f81
--- /dev/null
+++ b/test/BanggoodShop.Tests/ShopTest.cs
@@ -0,0 +1,36 @@
+using MultiShop.ShopFramework;
+using SimpleLogger;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace BanggoodShop.Tests
+{
+ public class ShopTest
+ {
+ public ShopTest(ITestOutputHelper output)
+ {
+ Logger.AddLogListener(new XUnitLogger(output));
+ }
+
+
+ [Fact]
+ public async void Search_CAD_ResultsFound()
+ {
+ //Given
+ const int MAX_RESULTS = 100;
+ Shop shop = new Shop();
+ shop.UseProxy = false;
+ //When
+ shop.Initialize();
+ shop.SetupSession("samsung galaxy 20 case", Currency.CAD);
+ //Then
+ int count = 0;
+ await foreach (ProductListing listing in shop)
+ {
+ count += 1;
+ Assert.False(string.IsNullOrWhiteSpace(listing.Name));
+ if (count >= MAX_RESULTS) return;
+ }
+ }
+ }
+}
diff --git a/test/BanggoodShop.Tests/XUnitLogger.cs b/test/BanggoodShop.Tests/XUnitLogger.cs
new file mode 100644
index 0000000..7e76ffd
--- /dev/null
+++ b/test/BanggoodShop.Tests/XUnitLogger.cs
@@ -0,0 +1,33 @@
+using System;
+using SimpleLogger;
+using Xunit.Abstractions;
+
+namespace BanggoodShop
+{
+ public class XUnitLogger : ILogReceiver
+ {
+ public LogLevel Level => LogLevel.Debug;
+
+ public string Identifier => GetType().Name;
+
+ private ITestOutputHelper outputHelper;
+
+ public XUnitLogger(ITestOutputHelper output)
+ {
+ this.outputHelper = output;
+ }
+
+ public void Flush()
+ {
+ }
+
+ public void LogMessage(string message, DateTime time, LogLevel level)
+ {
+ try
+ {
+ outputHelper.WriteLine($"[{time.ToShortTimeString()}][{level.ToString()}]: {message}");
+ }
+ catch (InvalidOperationException) { };
+ }
+ }
+}
\ No newline at end of file