refactor: unify TryCreate* into one helper, parallel photo upload, static JsonSettings, remove dead ExtractEbayError
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using EbayListingTool.Models;
|
using EbayListingTool.Models;
|
||||||
@@ -16,6 +16,13 @@ public class EbayListingService
|
|||||||
private static readonly HttpClient _http = new(); // REST / Inventory / Account APIs
|
private static readonly HttpClient _http = new(); // REST / Inventory / Account APIs
|
||||||
private static readonly HttpClient _photoHttp = new(); // Trading API (photo upload)
|
private static readonly HttpClient _photoHttp = new(); // Trading API (photo upload)
|
||||||
|
|
||||||
|
private static readonly JsonSerializerSettings _jsonSettings = new()
|
||||||
|
{ NullValueHandling = NullValueHandling.Ignore };
|
||||||
|
|
||||||
|
private static readonly string _logPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"EbayListingTool", "crash_log.txt");
|
||||||
|
|
||||||
// Per-session cache of eBay account IDs — fetched once, reused for every listing
|
// Per-session cache of eBay account IDs — fetched once, reused for every listing
|
||||||
private string? _fulfillmentPolicyId;
|
private string? _fulfillmentPolicyId;
|
||||||
private string? _paymentPolicyId;
|
private string? _paymentPolicyId;
|
||||||
@@ -43,13 +50,10 @@ public class EbayListingService
|
|||||||
{
|
{
|
||||||
var token = await _auth.GetValidAccessTokenAsync();
|
var token = await _auth.GetValidAccessTokenAsync();
|
||||||
|
|
||||||
// Resolve business policies and merchant location before touching inventory/offers
|
|
||||||
await EnsurePoliciesAndLocationAsync(token, draft.Postcode);
|
await EnsurePoliciesAndLocationAsync(token, draft.Postcode);
|
||||||
|
|
||||||
// 1. Upload photos and get eBay-hosted URLs
|
|
||||||
var imageUrls = await UploadPhotosAsync(draft.PhotoPaths, token);
|
var imageUrls = await UploadPhotosAsync(draft.PhotoPaths, token);
|
||||||
|
|
||||||
// 2. Resolve category if not set
|
|
||||||
if (string.IsNullOrEmpty(draft.CategoryId) && !string.IsNullOrEmpty(draft.CategoryName))
|
if (string.IsNullOrEmpty(draft.CategoryId) && !string.IsNullOrEmpty(draft.CategoryName))
|
||||||
{
|
{
|
||||||
draft.CategoryId = await _categoryService.GetCategoryIdByKeywordAsync(draft.CategoryName)
|
draft.CategoryId = await _categoryService.GetCategoryIdByKeywordAsync(draft.CategoryName)
|
||||||
@@ -59,14 +63,9 @@ public class EbayListingService
|
|||||||
if (string.IsNullOrEmpty(draft.CategoryId))
|
if (string.IsNullOrEmpty(draft.CategoryId))
|
||||||
throw new InvalidOperationException("Please select a category before posting.");
|
throw new InvalidOperationException("Please select a category before posting.");
|
||||||
|
|
||||||
// 3. Create inventory item
|
|
||||||
await CreateInventoryItemAsync(draft, imageUrls, token);
|
await CreateInventoryItemAsync(draft, imageUrls, token);
|
||||||
|
|
||||||
// 4. Create offer
|
|
||||||
var offerId = await CreateOfferAsync(draft, token);
|
var offerId = await CreateOfferAsync(draft, token);
|
||||||
|
var itemId = await PublishOfferAsync(offerId, token);
|
||||||
// 5. Publish offer → get item ID
|
|
||||||
var itemId = await PublishOfferAsync(offerId, token);
|
|
||||||
|
|
||||||
draft.EbayItemId = itemId;
|
draft.EbayItemId = itemId;
|
||||||
var domain = _auth.BaseUrl.Contains("sandbox") ? "sandbox.ebay.co.uk" : "ebay.co.uk";
|
var domain = _auth.BaseUrl.Contains("sandbox") ? "sandbox.ebay.co.uk" : "ebay.co.uk";
|
||||||
@@ -77,11 +76,6 @@ public class EbayListingService
|
|||||||
|
|
||||||
// ---- Setup: policies + location ----
|
// ---- Setup: policies + location ----
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches fulfillment, payment and return policy IDs from the seller's eBay account,
|
|
||||||
/// and ensures at least one merchant location exists (creating "home" from the seller's
|
|
||||||
/// postcode if needed). Results are cached for the session.
|
|
||||||
/// </summary>
|
|
||||||
private async Task EnsurePoliciesAndLocationAsync(string token, string postcode)
|
private async Task EnsurePoliciesAndLocationAsync(string token, string postcode)
|
||||||
{
|
{
|
||||||
var baseUrl = _auth.BaseUrl;
|
var baseUrl = _auth.BaseUrl;
|
||||||
@@ -145,7 +139,7 @@ public class EbayListingService
|
|||||||
if (_paymentPolicyId == null)
|
if (_paymentPolicyId == null)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"No payment policy found on your eBay account.\n\n" +
|
"No payment policy found on your eBay account.\n\n" +
|
||||||
"Please set one up in My eBay → Account → Business policies, then try again.");
|
"Please set one up in My eBay \u2192 Account \u2192 Business policies, then try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_returnPolicyId == null)
|
if (_returnPolicyId == null)
|
||||||
@@ -167,7 +161,7 @@ public class EbayListingService
|
|||||||
if (_returnPolicyId == null)
|
if (_returnPolicyId == null)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"No return policy found on your eBay account.\n\n" +
|
"No return policy found on your eBay account.\n\n" +
|
||||||
"Please set one up in My eBay → Account → Business policies, then try again.");
|
"Please set one up in My eBay \u2192 Account \u2192 Business policies, then try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_merchantLocationKey == null)
|
if (_merchantLocationKey == null)
|
||||||
@@ -179,13 +173,12 @@ public class EbayListingService
|
|||||||
|
|
||||||
if (res.IsSuccessStatusCode)
|
if (res.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var arr = JObject.Parse(json)["locations"] as JArray;
|
var arr = JObject.Parse(json)["locations"] as JArray;
|
||||||
_merchantLocationKey = arr?.Count > 0
|
_merchantLocationKey = arr?.Count > 0
|
||||||
? arr[0]["merchantLocationKey"]?.ToString()
|
? arr[0]["merchantLocationKey"]?.ToString()
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No existing locations — create one from the seller's postcode
|
|
||||||
if (_merchantLocationKey == null)
|
if (_merchantLocationKey == null)
|
||||||
{
|
{
|
||||||
await CreateMerchantLocationAsync(token, postcode);
|
await CreateMerchantLocationAsync(token, postcode);
|
||||||
@@ -194,73 +187,28 @@ public class EbayListingService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Parses an eBay error JSON body into a user-friendly message.</summary>
|
|
||||||
private static string ExtractEbayError(string json, string policyType)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var errors = JObject.Parse(json)["errors"] as JArray;
|
|
||||||
var first = errors?.FirstOrDefault() as JObject;
|
|
||||||
if (first != null)
|
|
||||||
{
|
|
||||||
var errorId = first["errorId"]?.Value<int>() ?? 0;
|
|
||||||
var longMsg = first["longMessage"]?.ToString() ?? first["message"]?.ToString() ?? "";
|
|
||||||
|
|
||||||
// 20403 = account not opted in to Business Policies
|
|
||||||
if (errorId == 20403 || longMsg.Contains("not eligible for Business Policy"))
|
|
||||||
return $"Your eBay account is not set up for Business Policies, which are required to post listings via the API.\n\n" +
|
|
||||||
$"To fix this:\n" +
|
|
||||||
$"1. Log in to the eBay Seller Hub (or sandbox Seller Hub)\n" +
|
|
||||||
$"2. Go to Account \u2192 Business policies\n" +
|
|
||||||
$"3. Create at least one Postage, Payment and Returns policy\n\n" +
|
|
||||||
$"Once done, click Post to eBay again.";
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(longMsg))
|
|
||||||
return $"eBay {policyType} error: {longMsg}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return $"Could not fetch {policyType} from eBay. Please check your account settings.";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called automatically on first 20403 error. Opts the account in to SELLING_POLICY_MANAGEMENT
|
|
||||||
/// then creates minimal default policies so posting can proceed without manual eBay setup.
|
|
||||||
/// </summary>
|
|
||||||
private static void LogSetup(string msg)
|
private static void LogSetup(string msg)
|
||||||
{
|
{
|
||||||
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EbayListingTool");
|
Directory.CreateDirectory(Path.GetDirectoryName(_logPath)!);
|
||||||
Directory.CreateDirectory(dir);
|
File.AppendAllText(_logPath, $"{DateTime.Now:HH:mm:ss} [Setup] {msg}{Environment.NewLine}");
|
||||||
File.AppendAllText(Path.Combine(dir, "crash_log.txt"),
|
|
||||||
$"{DateTime.Now:HH:mm:ss} [Setup] {msg}{Environment.NewLine}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SetupDefaultBusinessPoliciesAsync(string token)
|
private async Task SetupDefaultBusinessPoliciesAsync(string token)
|
||||||
{
|
{
|
||||||
// Step 1: opt in to the Business Policies programme
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var optInBody = JsonConvert.SerializeObject(new { programType = "SELLING_POLICY_MANAGEMENT" });
|
var optInBody = JsonConvert.SerializeObject(new { programType = "SELLING_POLICY_MANAGEMENT" });
|
||||||
using var req = MakeRequest(HttpMethod.Post,
|
using var req = MakeRequest(HttpMethod.Post,
|
||||||
$"{_auth.BaseUrl}/sell/account/v1/program/opt_in", token);
|
$"{_auth.BaseUrl}/sell/account/v1/program/opt_in", token);
|
||||||
req.Content = new StringContent(optInBody, Encoding.UTF8, "application/json");
|
req.Content = new StringContent(optInBody, Encoding.UTF8, "application/json");
|
||||||
var optInRes = await _http.SendAsync(req);
|
var res = await _http.SendAsync(req);
|
||||||
var optInBody2 = await optInRes.Content.ReadAsStringAsync();
|
var body = await res.Content.ReadAsStringAsync();
|
||||||
LogSetup($"opt_in → {(int)optInRes.StatusCode} {optInBody2}");
|
LogSetup($"opt_in \u2192 {(int)res.StatusCode} {body}");
|
||||||
}
|
}
|
||||||
catch (Exception ex) { LogSetup($"opt_in exception: {ex.Message}"); }
|
catch (Exception ex) { LogSetup($"opt_in exception: {ex.Message}"); }
|
||||||
|
|
||||||
// Step 2: create one policy of each required type (ignore errors — they may already exist)
|
await Task.WhenAll(
|
||||||
await TryCreateFulfillmentPolicyAsync(token);
|
TryCreatePolicyAsync(token, "fulfillment_policy", new
|
||||||
await TryCreatePaymentPolicyAsync(token);
|
|
||||||
await TryCreateReturnPolicyAsync(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryCreateFulfillmentPolicyAsync(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = new
|
|
||||||
{
|
{
|
||||||
name = "Standard UK Shipping",
|
name = "Standard UK Shipping",
|
||||||
marketplaceId = "EBAY_GB",
|
marketplaceId = "EBAY_GB",
|
||||||
@@ -278,52 +226,22 @@ public class EbayListingService
|
|||||||
{
|
{
|
||||||
sortOrder = 1,
|
sortOrder = 1,
|
||||||
shippingCarrierCode = "ROYALMAIL",
|
shippingCarrierCode = "ROYALMAIL",
|
||||||
shippingServiceCode = "UK_OtherCourier",
|
shippingServiceCode = "UK_OtherCourier",
|
||||||
shippingCost = new { value = "2.85", currency = "GBP" },
|
shippingCost = new { value = "2.85", currency = "GBP" },
|
||||||
additionalShippingCost = new { value = "0.00", currency = "GBP" }
|
additionalShippingCost = new { value = "0.00", currency = "GBP" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}),
|
||||||
using var req = MakeRequest(HttpMethod.Post,
|
TryCreatePolicyAsync(token, "payment_policy", new
|
||||||
$"{_auth.BaseUrl}/sell/account/v1/fulfillment_policy", token);
|
|
||||||
req.Content = new StringContent(
|
|
||||||
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
|
||||||
var res2 = await _http.SendAsync(req);
|
|
||||||
var body2 = await res2.Content.ReadAsStringAsync();
|
|
||||||
LogSetup($"fulfillment_policy create → {(int)res2.StatusCode} {body2}");
|
|
||||||
}
|
|
||||||
catch (Exception ex) { LogSetup($"fulfillment_policy exception: {ex.Message}"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryCreatePaymentPolicyAsync(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = new
|
|
||||||
{
|
{
|
||||||
name = "Managed Payments",
|
name = "Managed Payments",
|
||||||
marketplaceId = "EBAY_GB",
|
marketplaceId = "EBAY_GB",
|
||||||
categoryTypes = new[] { new { name = "ALL_EXCLUDING_MOTORS_VEHICLES" } },
|
categoryTypes = new[] { new { name = "ALL_EXCLUDING_MOTORS_VEHICLES" } },
|
||||||
paymentMethods = Array.Empty<object>()
|
paymentMethods = Array.Empty<object>()
|
||||||
};
|
}),
|
||||||
using var req = MakeRequest(HttpMethod.Post,
|
TryCreatePolicyAsync(token, "return_policy", new
|
||||||
$"{_auth.BaseUrl}/sell/account/v1/payment_policy", token);
|
|
||||||
req.Content = new StringContent(
|
|
||||||
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
|
||||||
var res2 = await _http.SendAsync(req);
|
|
||||||
var body2 = await res2.Content.ReadAsStringAsync();
|
|
||||||
LogSetup($"payment_policy create → {(int)res2.StatusCode} {body2}");
|
|
||||||
}
|
|
||||||
catch (Exception ex) { LogSetup($"payment_policy exception: {ex.Message}"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryCreateReturnPolicyAsync(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = new
|
|
||||||
{
|
{
|
||||||
name = "Standard Returns",
|
name = "Standard Returns",
|
||||||
marketplaceId = "EBAY_GB",
|
marketplaceId = "EBAY_GB",
|
||||||
@@ -332,22 +250,29 @@ public class EbayListingService
|
|||||||
returnPeriod = new { value = 30, unit = "DAY" },
|
returnPeriod = new { value = 30, unit = "DAY" },
|
||||||
refundMethod = "MONEY_BACK",
|
refundMethod = "MONEY_BACK",
|
||||||
returnShippingCostPayer = "BUYER"
|
returnShippingCostPayer = "BUYER"
|
||||||
};
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TryCreatePolicyAsync(string token, string policyType, object body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
using var req = MakeRequest(HttpMethod.Post,
|
using var req = MakeRequest(HttpMethod.Post,
|
||||||
$"{_auth.BaseUrl}/sell/account/v1/return_policy", token);
|
$"{_auth.BaseUrl}/sell/account/v1/{policyType}", token);
|
||||||
req.Content = new StringContent(
|
req.Content = new StringContent(
|
||||||
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||||
var res2 = await _http.SendAsync(req);
|
var res = await _http.SendAsync(req);
|
||||||
var body2 = await res2.Content.ReadAsStringAsync();
|
var respBody = await res.Content.ReadAsStringAsync();
|
||||||
LogSetup($"return_policy create → {(int)res2.StatusCode} {body2}");
|
LogSetup($"{policyType} create \u2192 {(int)res.StatusCode} {respBody}");
|
||||||
}
|
}
|
||||||
catch (Exception ex) { LogSetup($"return_policy exception: {ex.Message}"); }
|
catch (Exception ex) { LogSetup($"{policyType} exception: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateMerchantLocationAsync(string token, string postcode)
|
private async Task CreateMerchantLocationAsync(string token, string postcode)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(postcode))
|
if (string.IsNullOrWhiteSpace(postcode))
|
||||||
postcode = "N/A"; // eBay allows this when postcode is genuinely unknown
|
postcode = "N/A";
|
||||||
|
|
||||||
var body = new
|
var body = new
|
||||||
{
|
{
|
||||||
@@ -355,8 +280,8 @@ public class EbayListingService
|
|||||||
{
|
{
|
||||||
address = new { postalCode = postcode, country = "GB" }
|
address = new { postalCode = postcode, country = "GB" }
|
||||||
},
|
},
|
||||||
locationTypes = new[] { "WAREHOUSE" },
|
locationTypes = new[] { "WAREHOUSE" },
|
||||||
name = "Home",
|
name = "Home",
|
||||||
merchantLocationStatus = "ENABLED"
|
merchantLocationStatus = "ENABLED"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -383,10 +308,8 @@ public class EbayListingService
|
|||||||
{
|
{
|
||||||
shipToLocationAvailability = new { quantity = draft.Quantity }
|
shipToLocationAvailability = new { quantity = draft.Quantity }
|
||||||
},
|
},
|
||||||
condition = draft.ConditionId,
|
condition = draft.ConditionId,
|
||||||
conditionDescription = draft.Condition == ItemCondition.Used ? "Used — see photos" : null,
|
conditionDescription = draft.Condition == ItemCondition.Used ? "Used \u2014 see photos" : null,
|
||||||
description = draft.Description,
|
|
||||||
title = draft.Title,
|
|
||||||
product = new
|
product = new
|
||||||
{
|
{
|
||||||
title = draft.Title,
|
title = draft.Title,
|
||||||
@@ -396,14 +319,9 @@ public class EbayListingService
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(inventoryItem, new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
NullValueHandling = NullValueHandling.Ignore
|
|
||||||
});
|
|
||||||
|
|
||||||
var url = $"{_auth.BaseUrl}/sell/inventory/v1/inventory_item/{Uri.EscapeDataString(draft.Sku)}";
|
var url = $"{_auth.BaseUrl}/sell/inventory/v1/inventory_item/{Uri.EscapeDataString(draft.Sku)}";
|
||||||
using var req = MakeRequest(HttpMethod.Put, url, token);
|
using var req = MakeRequest(HttpMethod.Put, url, token);
|
||||||
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
req.Content = new StringContent(JsonConvert.SerializeObject(inventoryItem, _jsonSettings), Encoding.UTF8, "application/json");
|
||||||
req.Content.Headers.Add("Content-Language", "en-GB");
|
req.Content.Headers.Add("Content-Language", "en-GB");
|
||||||
|
|
||||||
var res = await _http.SendAsync(req);
|
var res = await _http.SendAsync(req);
|
||||||
@@ -432,24 +350,16 @@ public class EbayListingService
|
|||||||
paymentPolicyId = _paymentPolicyId,
|
paymentPolicyId = _paymentPolicyId,
|
||||||
returnPolicyId = _returnPolicyId
|
returnPolicyId = _returnPolicyId
|
||||||
},
|
},
|
||||||
pricingSummary = new
|
pricingSummary = new { price = new { value = draft.Price.ToString("F2"), currency = "GBP" } },
|
||||||
{
|
|
||||||
price = new { value = draft.Price.ToString("F2"), currency = "GBP" }
|
|
||||||
},
|
|
||||||
merchantLocationKey = _merchantLocationKey,
|
merchantLocationKey = _merchantLocationKey,
|
||||||
tax = new { vatPercentage = 0, applyTax = false }
|
tax = new { vatPercentage = 0, applyTax = false }
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(offer, new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
NullValueHandling = NullValueHandling.Ignore
|
|
||||||
});
|
|
||||||
|
|
||||||
using var req = MakeRequest(HttpMethod.Post,
|
using var req = MakeRequest(HttpMethod.Post,
|
||||||
$"{_auth.BaseUrl}/sell/inventory/v1/offer", token);
|
$"{_auth.BaseUrl}/sell/inventory/v1/offer", token);
|
||||||
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
req.Content = new StringContent(JsonConvert.SerializeObject(offer, _jsonSettings), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var res = await _http.SendAsync(req);
|
var res = await _http.SendAsync(req);
|
||||||
var responseJson = await res.Content.ReadAsStringAsync();
|
var responseJson = await res.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
if (!res.IsSuccessStatusCode)
|
if (!res.IsSuccessStatusCode)
|
||||||
@@ -481,29 +391,22 @@ public class EbayListingService
|
|||||||
|
|
||||||
private async Task<List<string>> UploadPhotosAsync(List<string> photoPaths, string token)
|
private async Task<List<string>> UploadPhotosAsync(List<string> photoPaths, string token)
|
||||||
{
|
{
|
||||||
var urls = new List<string>();
|
if (photoPaths.Count == 0) return [];
|
||||||
if (photoPaths.Count == 0) return urls;
|
|
||||||
|
|
||||||
var tradingBase = _auth.BaseUrl.Contains("sandbox")
|
var tradingBase = _auth.BaseUrl.Contains("sandbox")
|
||||||
? "https://api.sandbox.ebay.com/ws/api.dll"
|
? "https://api.sandbox.ebay.com/ws/api.dll"
|
||||||
: "https://api.ebay.com/ws/api.dll";
|
: "https://api.ebay.com/ws/api.dll";
|
||||||
|
|
||||||
foreach (var path in photoPaths.Take(12))
|
var semaphore = new SemaphoreSlim(4);
|
||||||
|
var tasks = photoPaths.Take(12).Select(async path =>
|
||||||
{
|
{
|
||||||
if (!File.Exists(path)) continue;
|
await semaphore.WaitAsync();
|
||||||
try
|
try { return await UploadSinglePhotoAsync(path, tradingBase, token); }
|
||||||
{
|
catch { return null; }
|
||||||
var url = await UploadSinglePhotoAsync(path, tradingBase, token);
|
finally { semaphore.Release(); }
|
||||||
if (!string.IsNullOrEmpty(url))
|
});
|
||||||
urls.Add(url);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Skip failed photos; don't abort the whole listing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
return [.. (await Task.WhenAll(tasks)).Where(u => !string.IsNullOrEmpty(u))];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> UploadSinglePhotoAsync(string filePath, string tradingUrl, string token)
|
private async Task<string?> UploadSinglePhotoAsync(string filePath, string tradingUrl, string token)
|
||||||
@@ -522,7 +425,6 @@ public class EbayListingService
|
|||||||
</UploadSiteHostedPicturesRequest>
|
</UploadSiteHostedPicturesRequest>
|
||||||
""";
|
""";
|
||||||
|
|
||||||
// Use HttpRequestMessage with _photoHttp so we don't create a new socket per photo
|
|
||||||
using var content = new MultipartFormDataContent();
|
using var content = new MultipartFormDataContent();
|
||||||
content.Add(new StringContent(soapBody, Encoding.UTF8, "text/xml"), "XML Payload");
|
content.Add(new StringContent(soapBody, Encoding.UTF8, "text/xml"), "XML Payload");
|
||||||
var imageContent = new ByteArrayContent(fileBytes);
|
var imageContent = new ByteArrayContent(fileBytes);
|
||||||
@@ -546,7 +448,6 @@ public class EbayListingService
|
|||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
/// <summary>Creates a pre-authorised request targeting the eBay REST APIs.</summary>
|
|
||||||
private HttpRequestMessage MakeRequest(HttpMethod method, string url, string token)
|
private HttpRequestMessage MakeRequest(HttpMethod method, string url, string token)
|
||||||
{
|
{
|
||||||
var req = new HttpRequestMessage(method, url);
|
var req = new HttpRequestMessage(method, url);
|
||||||
@@ -555,8 +456,3 @@ public class EbayListingService
|
|||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user