Changes Summary
appsettings.json — Added PoolEnabled to each API section: - TumblrApi → true - TumblrApi3 → false - TumblrApi4 → true DataAccess.cs — Added: - ApiKeyConfig class — holds credentials + metadata per key - ApiKeyPool class — manages pool with round-robin rotation, SQLite-backed state (ApiKeyPoolState, ApiKeyPoolMeta tables) - GetCurrentKey() — returns next key, skipping rate-limited ones, falls back to earliest-recovery if all are throttled - MarkRateLimited(key, retryUntil) / MarkAvailable(key) — persists state - Initialize() — discovers pool-enabled keys, detects single-key override mode - Refactored APIAccess.GrabNotes(), GrabPostWithReplies(), GrabLikes() to accept ApiKeyConfig param and log [Key#N] Program.cs — Updated: - Tracks apiExplicitlySet flag from -api/-api3/-api4 - Initializes ApiKeyPool at startup (pool mode or single-key override) - All 3 API callers updated to use pool rotation + 429 handling Startup Output - Pool mode: [Pool] Active keys: Key#1=TumblrApi, Key#2=TumblrApi4 - Single-key: [Pool] Single-key mode: TumblrApi3
This commit is contained in:
@@ -23,6 +23,7 @@ namespace URLNotesGrabberCORE
|
||||
var settings = config.GetSection("appSettings");
|
||||
|
||||
string apiSectionName = "TumblrApi";
|
||||
bool apiExplicitlySet = false;
|
||||
string startFromBlogName = string.Empty;
|
||||
List<string> filteredArgs = new List<string>();
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
@@ -31,6 +32,7 @@ namespace URLNotesGrabberCORE
|
||||
string.Equals(args[i], "--api3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
apiSectionName = "TumblrApi3";
|
||||
apiExplicitlySet = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -38,6 +40,7 @@ namespace URLNotesGrabberCORE
|
||||
string.Equals(args[i], "--api4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
apiSectionName = "TumblrApi4";
|
||||
apiExplicitlySet = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@ namespace URLNotesGrabberCORE
|
||||
if (i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
|
||||
{
|
||||
apiSectionName = args[i + 1].Trim();
|
||||
apiExplicitlySet = true;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
@@ -75,7 +79,12 @@ namespace URLNotesGrabberCORE
|
||||
}
|
||||
|
||||
args = filteredArgs.ToArray();
|
||||
APIAccess.SetApiConfigSection(apiSectionName);
|
||||
|
||||
string? dbPath = config["appSettings:PathDB"];
|
||||
if (string.IsNullOrWhiteSpace(dbPath))
|
||||
dbPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "tl.db");
|
||||
|
||||
ApiKeyPool.Initialize(config, dbPath, apiExplicitlySet ? apiSectionName : null);
|
||||
|
||||
// Setup Dual Logging
|
||||
bool enableFileLogging = settings.GetValue("EnableFileLogging", true);
|
||||
@@ -439,9 +448,17 @@ namespace URLNotesGrabberCORE
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[Reply Text] Fetching reply text for {blogName}/{postID}/{timestamp}");
|
||||
// Add 2-second delay before API call to avoid server-side rate limiting
|
||||
await Task.Delay(2000);
|
||||
var postsResponse = await APIAccess.GrabPostWithReplies(blogName, postID, timestamp);
|
||||
var key = ApiKeyPool.GetCurrentKey();
|
||||
var postsResponse = await APIAccess.GrabPostWithReplies(key, blogName, postID, timestamp);
|
||||
|
||||
if (postsResponse?.statusCode == "TooManyRequests")
|
||||
{
|
||||
int retry = postsResponse.retryInSeconds > 0 ? postsResponse.retryInSeconds : 60;
|
||||
ApiKeyPool.MarkRateLimited(key, retry);
|
||||
Console.WriteLine($"[Reply Text] Rate limited, will retry with next key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (postsResponse?.response?.posts == null || postsResponse.response.posts.Count == 0)
|
||||
{
|
||||
@@ -677,24 +694,16 @@ namespace URLNotesGrabberCORE
|
||||
if (!lease.IsAcquired)
|
||||
{
|
||||
Console.WriteLine("!@@@@@@@ - Rate Limited Exceeded: No Lease Available");
|
||||
return; // Might want to sleep instead, but this matches other functions
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await APIAccess.GrabLikes(blogName, cursor);
|
||||
var key = ApiKeyPool.GetCurrentKey();
|
||||
var response = await APIAccess.GrabLikes(key, blogName, cursor);
|
||||
|
||||
if (response?.statusCode == "NotFound" || (response?.meta != null && response.meta.status == 404))
|
||||
if (response?.statusCode == "TooManyRequests")
|
||||
{
|
||||
Console.WriteLine($"API returned 404 Not Found for {blogName} Likes");
|
||||
// Set pulled = 1 to skip in future
|
||||
DataAccess.UpdateBlogLikesStatus(blogName, 1, cursor);
|
||||
break;
|
||||
}
|
||||
|
||||
if (response == null || response.statusCode == "TooManyRequests")
|
||||
{
|
||||
int retry = response?.retryInSeconds ?? 60;
|
||||
if (retry <= 0)
|
||||
retry = 60;
|
||||
int retry = response.retryInSeconds > 0 ? response.retryInSeconds : 60;
|
||||
ApiKeyPool.MarkRateLimited(key, retry);
|
||||
|
||||
int remaining = retry;
|
||||
DateTime retryUntil = DateTime.Now.AddSeconds(retry);
|
||||
@@ -705,7 +714,17 @@ namespace URLNotesGrabberCORE
|
||||
Thread.Sleep(sleepSeconds * 1000);
|
||||
remaining -= sleepSeconds;
|
||||
}
|
||||
continue; // Retry same cursor
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.meta?.status != 429)
|
||||
ApiKeyPool.MarkAvailable(key);
|
||||
|
||||
if (response?.statusCode == "NotFound" || (response?.meta != null && response.meta.status == 404))
|
||||
{
|
||||
Console.WriteLine($"API returned 404 Not Found for {blogName} Likes");
|
||||
DataAccess.UpdateBlogLikesStatus(blogName, 1, cursor);
|
||||
break;
|
||||
}
|
||||
|
||||
if (response?.response?.liked_posts == null || response.response.liked_posts.Count == 0)
|
||||
@@ -879,16 +898,25 @@ namespace URLNotesGrabberCORE
|
||||
|
||||
int APICount = DataAccess.GetAPICount();
|
||||
Console.WriteLine($"{post.Item1}\t{post.Item2}\t{DateTime.Now}\t{APICount}");
|
||||
//Thread.Sleep(3000);
|
||||
|
||||
var allNotes = new List<dynamic>();
|
||||
int page = 1;
|
||||
string beforeTimestamp = post.Item3.ToString();
|
||||
bool hasReplies = false;
|
||||
const int maxPages = 500;
|
||||
var response = APIAccess.GrabNotes(post.Item1, post.Item2, beforeTimestamp).GetAwaiter().GetResult();
|
||||
var key = ApiKeyPool.GetCurrentKey();
|
||||
var response = await APIAccess.GrabNotes(key, post.Item1, post.Item2, beforeTimestamp);
|
||||
|
||||
if (response.statusCode == "TooManyRequests")
|
||||
{
|
||||
int retry = response.retryInSeconds > 0 ? response.retryInSeconds : 60;
|
||||
ApiKeyPool.MarkRateLimited(key, retry);
|
||||
return "TooManyRequests";
|
||||
}
|
||||
|
||||
if (response.meta?.status != 429)
|
||||
ApiKeyPool.MarkAvailable(key);
|
||||
|
||||
// Handle 404 and error codes
|
||||
if (IsNotFound(response))
|
||||
{
|
||||
Console.WriteLine($"API returned 404 Not Found for {post.Item1}/{post.Item2}");
|
||||
@@ -903,6 +931,8 @@ namespace URLNotesGrabberCORE
|
||||
}
|
||||
if (response.statusCode == "TooManyRequests")
|
||||
{
|
||||
int retry = response.retryInSeconds > 0 ? response.retryInSeconds : 60;
|
||||
ApiKeyPool.MarkRateLimited(key, retry);
|
||||
for (int s = 0; s <= response.retryInSeconds; s += 60)
|
||||
{
|
||||
Console.WriteLine("Sleeping for {0} more seconds, until {1}", response.retryInSeconds - s, DateTime.Now.AddSeconds(response.retryInSeconds - s).ToShortTimeString());
|
||||
@@ -959,8 +989,17 @@ namespace URLNotesGrabberCORE
|
||||
break;
|
||||
}
|
||||
|
||||
// Fetch next page
|
||||
response = APIAccess.GrabNotes(post.Item1, post.Item2, beforeTimestamp).GetAwaiter().GetResult();
|
||||
key = ApiKeyPool.GetCurrentKey();
|
||||
response = await APIAccess.GrabNotes(key, post.Item1, post.Item2, beforeTimestamp);
|
||||
if (response.statusCode == "TooManyRequests")
|
||||
{
|
||||
int retry = response.retryInSeconds > 0 ? response.retryInSeconds : 60;
|
||||
ApiKeyPool.MarkRateLimited(key, retry);
|
||||
return "TooManyRequests";
|
||||
}
|
||||
if (response.meta?.status != 429)
|
||||
ApiKeyPool.MarkAvailable(key);
|
||||
|
||||
if (IsNotFound(response))
|
||||
{
|
||||
Console.WriteLine($"API returned 404 Not Found during pagination for {post.Item1}/{post.Item2}");
|
||||
|
||||
Reference in New Issue
Block a user