- Added `beforeDate` parameter to filter posts by `NotesGatheredDateTime`. - Improved SQL queries with additional joins and conditions. - Refactored exception handling for better resource cleanup. - Enhanced string comparisons to support case-insensitivity. - Added `GetReplies` method for fetching replies. - Improved handling of `ReblogRecord` data and filtering logic. - Updated `-collect` command to support optional date filtering. - Adjusted `launchSettings.json` for testing with specific parameters. - Improved logging and error reporting in API and database operations.
973 lines
44 KiB
C#
973 lines
44 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Data.SQLite;
|
|
using RestSharp.Authenticators.OAuth;
|
|
using RestSharp.Authenticators;
|
|
using RestSharp;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Converters;
|
|
using Microsoft.Extensions.Diagnostics.Latency;
|
|
using System.Collections;
|
|
using Microsoft.Extensions.Configuration;
|
|
using System.IO;
|
|
|
|
namespace URLNotesGrabberCORE
|
|
{
|
|
|
|
class ReblogRecord
|
|
{
|
|
public string answer;
|
|
public string audioCaption;
|
|
public string blogName;
|
|
public string body;
|
|
public string date;
|
|
public string downloadedFiles;
|
|
public string link;
|
|
public string photoCaption;
|
|
public string photoURL;
|
|
public string postID;
|
|
public string postURL;
|
|
public string question;
|
|
public string quote;
|
|
public string reblogKey;
|
|
public string reblogName;
|
|
public string reblogURL;
|
|
public string slug;
|
|
public string summary;
|
|
public string tags;
|
|
public string title;
|
|
|
|
public ReblogRecord()
|
|
{
|
|
answer = ".";
|
|
audioCaption = ".";
|
|
blogName = ".";
|
|
body = ".";
|
|
date = ".";
|
|
downloadedFiles = ".";
|
|
link = ".";
|
|
photoCaption = ".";
|
|
photoURL = ".";
|
|
postID = ".";
|
|
postURL = ".";
|
|
question = ".";
|
|
quote = ".";
|
|
reblogKey = ".";
|
|
reblogName = ".";
|
|
reblogURL = ".";
|
|
slug = ".";
|
|
summary = ".";
|
|
tags = ".";
|
|
title = ".";
|
|
}
|
|
|
|
}
|
|
|
|
internal class DataAccess
|
|
{
|
|
public static string Q(string input)
|
|
{
|
|
return "'" + input.Replace("'", "''") + "'";
|
|
}
|
|
|
|
public static DateTime UnixTimeStampToDateTime(double unixTimeStamp)
|
|
{
|
|
// Unix timestamp is seconds past epoch
|
|
DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
|
|
dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
|
|
return dateTime;
|
|
}
|
|
|
|
#region Adds
|
|
public static void AddBlog(string blogName, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
if (blogName.Contains("deact"))
|
|
return;
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "INSERT OR IGNORE INTO Blogs (BlogName) values('" + blogName + "')";
|
|
SQLiteCommand command = new SQLiteCommand(sql, connection);
|
|
|
|
command.ExecuteNonQuery();
|
|
// Console.WriteLine("+ " + blogName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Blogs.BlogName")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static void AddPost(string blogName, long postID, string reblogURL, string postDate, string postURL, string slug, string reblogKey, string reblogName, string summary, string quote, string body, string tags, string link, string photoURL, string photoCaption, string downloadedFiles, string audioCaption, string question, string answer, string title, bool hasImage, string DBPath = @"TL.db")
|
|
{
|
|
try { AddBlog(blogName, DBPath); } catch { }
|
|
try { UpdatePostSetDate(blogName, postID, postDate, DBPath); } catch { }
|
|
try { UpdatePost(blogName, postID, reblogURL, postDate, postURL, slug, reblogKey, reblogName, summary, quote, body, tags, link, photoURL, photoCaption, downloadedFiles, audioCaption, question, answer, title, hasImage, DBPath); } catch { }
|
|
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = @"INSERT INTO Posts (
|
|
BlogName,
|
|
PostID,
|
|
reblogURL,
|
|
PostDate,
|
|
PostURL,
|
|
Slug,
|
|
ReblogKey,
|
|
ReblogName,
|
|
Summary,
|
|
Quote,
|
|
Body,
|
|
Tags,
|
|
Link,
|
|
PhotoURL,
|
|
PhotoCaption,
|
|
DownloadedFiles,
|
|
AudioCaption,
|
|
Question,
|
|
Answer,
|
|
Title,
|
|
HasImage
|
|
) VALUES (" +
|
|
Q(blogName) + ", " + postID + ", " + Q(reblogURL) + ", " + Q(postDate) + ", " + Q(postURL) + ", " + Q(slug) + ", " + Q(reblogKey) + ", " + Q(reblogName) + ", " + Q(summary) + ", " + Q(quote) + ", " + Q(body) + ", " + Q(tags) + ", " + Q(link) + ", " + Q(photoURL) + ", " + Q(photoCaption) + ", " + Q(downloadedFiles) + ", " + Q(audioCaption) + ", " + Q(question) + ", " + Q(answer) + ", " + Q(title) + ", " + hasImage + ")";
|
|
SQLiteCommand command = new SQLiteCommand(sql, connection);
|
|
|
|
command.ExecuteNonQuery();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
///
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Posts SET hasImage = " + hasImage + "WHERE blogName = '" + blogName + "' AND postID = '" + postID + "'";
|
|
SQLiteCommand command = new SQLiteCommand(sql, connection);
|
|
|
|
command.ExecuteNonQuery();
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
if (ex2.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
{
|
|
Console.WriteLine(ex2.Message);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
///
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
|
|
public static void AddAPICount(string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
// Use INSERT OR IGNORE to avoid UNIQUE constraint errors when the date row already exists.
|
|
// Also explicitly initialize APICount to 0 in case the table has no default.
|
|
string sql = "INSERT OR IGNORE INTO DailyAPICount (Date, APICount) values(@date, 0)";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@date", DateTime.Today.ToShortDateString());
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static bool AddNote(string rootBlogName, string noteBlogName, long postID, long timestamp, string type, string DBPath = @"TL.db")
|
|
{
|
|
//try { AddPost(rootBlogName, postID, DBPath); } catch { }
|
|
try { AddBlog(noteBlogName, DBPath); } catch { }
|
|
|
|
Console.WriteLine("{2}\t{0}\t{1}", UnixTimeStampToDateTime(timestamp), noteBlogName, type);
|
|
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "INSERT OR IGNORE INTO Notes (rootBlogName, noteBlogName, PostID, TimeStamp, Type) values(@rootBlogName, @noteBlogName, @PostID, @TimeStamp, @Type)";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@rootBlogName", rootBlogName);
|
|
command.Parameters.AddWithValue("@noteBlogName", noteBlogName);
|
|
command.Parameters.AddWithValue("@PostID", postID);
|
|
command.Parameters.AddWithValue("@TimeStamp", timestamp);
|
|
command.Parameters.AddWithValue("@Type", type ?? string.Empty);
|
|
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Notes.RootBlogName, Notes.PostID, Notes.TimeStamp, Notes.Type, Notes.NoteBlogName")
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
Console.WriteLine("^^^^^ - SHORTCUT");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return false;
|
|
}
|
|
#endregion Adds
|
|
|
|
#region Gets
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="withoutNotesOnly"></param>
|
|
/// <param name="DBPath"></param>
|
|
/// <returns>blogName, postID, lastNoteTimestamp, notesGatheredTimestamp</returns>
|
|
public static List<Tuple<string, long, long, long>> GetPosts(bool withoutNotesOnly = false, DateTime? beforeDate = null, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
List<Tuple<string, long, long, long>> posts = new List<Tuple<string, long, long, long>>();
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "SELECT " +
|
|
" Posts.BlogName, " + Environment.NewLine +
|
|
" Posts.PostID, " + Environment.NewLine +
|
|
" Max(IFNULL(Notes.timestamp, 1925013599)) as LatestNoteTimestamp, " + Environment.NewLine +
|
|
" Posts.NotesGatheredDatetime, " + Environment.NewLine +
|
|
" CNT.CNT " + Environment.NewLine +
|
|
"FROM " + Environment.NewLine +
|
|
" Posts " + Environment.NewLine +
|
|
" LEFT OUTER JOIN " + Environment.NewLine +
|
|
" Notes ON Notes.RootBlogName = Posts.BlogName AND Notes.PostID = Posts.PostID " + Environment.NewLine +
|
|
" LEFT OUTER JOIN " + Environment.NewLine +
|
|
" ( select BlogName, count(PostID) as CNT from Posts group by BlogName) CNT on CNT.blogName = Posts.BlogName " +
|
|
"WHERE NotFound = 0 " + Environment.NewLine;
|
|
|
|
if (withoutNotesOnly)
|
|
sql += " and HasNotesGathered = 0 " + Environment.NewLine;
|
|
|
|
// Filter by NotesGatheredDateTime if beforeDate is provided
|
|
if (beforeDate.HasValue)
|
|
{
|
|
long unixTimestamp = new DateTimeOffset(beforeDate.Value).ToUnixTimeSeconds();
|
|
sql += $" AND (NotesGatheredDateTime < {unixTimestamp} OR NotesGatheredDateTime IS NULL) " + Environment.NewLine;
|
|
}
|
|
|
|
sql += "GROUP BY " + Environment.NewLine +
|
|
" Posts.BlogName, Posts.PostID " + Environment.NewLine +
|
|
"ORDER BY " + Environment.NewLine +
|
|
" notesgathereddatetime, Posts.PostDate DESC, Posts.BlogName, Posts.PostID" + Environment.NewLine;
|
|
|
|
Console.WriteLine(withoutNotesOnly);
|
|
Console.WriteLine(sql);
|
|
Console.Write(">"); //Console.ReadKey();
|
|
Thread.Sleep(250);
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
Tuple<string, long, long, long> post = default;
|
|
string blog;
|
|
long postID = 0;
|
|
long lastNoteTimestamp;
|
|
long notesGatheredTimestamp;
|
|
|
|
blog = reader.GetString(0); // Assuming Title is the second column
|
|
postID = reader.GetInt64(1); // Assuming Id is the first column
|
|
lastNoteTimestamp = reader.GetInt64(2); // Assuming Id is the first column
|
|
notesGatheredTimestamp = reader.GetInt64(3); // Assuming Id is the first column
|
|
|
|
post = new Tuple<string, long, long, long>(blog, postID, lastNoteTimestamp, notesGatheredTimestamp);
|
|
posts.Add(post);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return posts;
|
|
}
|
|
|
|
public static List<Tuple<string, long>> GetReplies(string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
List<Tuple<string, long>> posts = new List<Tuple<string, long>>();
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "SELECT distinct RootBlogName as blogName, postID FROM Notes WHERE Notes.type = 'reply' order by RootBlogName, PostID";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
Tuple<string, long> post = default;
|
|
string blog = null;
|
|
long id = 0;
|
|
|
|
id = reader.GetInt64(reader.GetOrdinal("postID")); // Assuming Id is the first column
|
|
blog = reader.GetString(reader.GetOrdinal("blogName")); // Assuming Title is the second column
|
|
|
|
post = new Tuple<string, long>(blog, id);
|
|
posts.Add(post);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return posts;
|
|
}
|
|
|
|
public static int GetAPICount(string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
int count = 0;
|
|
|
|
try { AddAPICount(); } catch { }
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "SELECT APICount FROM DailyAPICount WHERE [Date] = @date";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@date", DateTime.Today.ToShortDateString());
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
count = reader.GetInt32(0); // Assuming Id is the first column
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public static List<string> GetBlogs(bool reblogsOnly, int from, int to, int top, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
List<string> blogs = new List<string>();
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
string sql = "";
|
|
if (reblogsOnly)
|
|
sql = "SELECT NoteBlogName as blogName, count(*) FROM notes INNER JOIN blogs ON blogs.BlogName = notes.NoteBlogName WHERE blogs.IsActive = @isActive AND type IN ('reblog', 'reply', 'posted') AND HasBeenOutput = 0 GROUP BY NoteBlogName ORDER BY count(*) DESC, BlogName LIMIT @top";
|
|
else
|
|
sql = "SELECT NoteBlogName as blogName, count(*) FROM notes INNER JOIN blogs ON blogs.BlogName = notes.NoteBlogName WHERE blogs.IsActive = @isActive AND type NOT IN ('reblog', 'reply', 'posted') AND HasBeenOutput = 0 GROUP BY NoteBlogName ORDER BY count(*) DESC, BlogName LIMIT @top";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@isActive", 1);
|
|
command.Parameters.AddWithValue("@top", top);
|
|
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string blog = null;
|
|
blog = reader.GetString(0); // Assuming Title is the second column
|
|
|
|
blogs.Add(blog);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return blogs;
|
|
}
|
|
|
|
public static List<string> GetBlogsAll(bool reblogsOnly, int from, int to, int top, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
List<string> blogs = new List<string>();
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
string sql = "";
|
|
if (reblogsOnly)
|
|
sql = "SELECT NoteBlogName as blogName, count(*) FROM notes INNER JOIN blogs ON blogs.BlogName = notes.NoteBlogName WHERE blogs.IsActive = @isActive AND type IN ('reblog', 'reply', 'posted') AND HasBeenOutput = 0 GROUP BY NoteBlogName ORDER BY count(*) DESC, BlogName LIMIT @top";
|
|
else
|
|
sql = "SELECT NoteBlogName as blogName, count(*) FROM notes INNER JOIN blogs ON blogs.BlogName = notes.NoteBlogName WHERE blogs.IsActive = @isActive AND HasBeenOutput = 0 GROUP BY NoteBlogName ORDER BY count(*) DESC, BlogName LIMIT @top";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@isActive", 1);
|
|
command.Parameters.AddWithValue("@top", top);
|
|
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string blog = null;
|
|
blog = reader.GetString(0); // Assuming Title is the second column
|
|
|
|
blogs.Add(blog);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return blogs;
|
|
}
|
|
#endregion Gets
|
|
|
|
#region Updates
|
|
|
|
|
|
public static void UpdatePostMarkNotesCollected(string blogName, long postID, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Posts SET HasNotesGathered = 1, NotesGatheredDateTime = @notesGathered WHERE BlogName = @BlogName AND PostID = @PostID";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@notesGathered", DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
command.Parameters.AddWithValue("@BlogName", blogName);
|
|
command.Parameters.AddWithValue("@PostID", postID);
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static void UpdatePostMarkNotFound(string blogName, long postID, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
// Informative output when marking a post as NotFound
|
|
Console.WriteLine($"Marking post NotFound: {blogName}/{postID}");
|
|
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Posts SET NotFound = 1 WHERE BlogName = @BlogName AND PostID = @PostID";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@BlogName", blogName);
|
|
command.Parameters.AddWithValue("@PostID", postID);
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static void UpdatePostSetDate(string blogName, long postID, string postDate, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Posts SET postDate = @postDate WHERE BlogName = @BlogName AND PostID = @PostID";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@postDate", postDate ?? string.Empty);
|
|
command.Parameters.AddWithValue("@BlogName", blogName);
|
|
command.Parameters.AddWithValue("@PostID", postID);
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static bool UpdateNote(string rootBlogName, string noteBlogName, long postID, long timestamp, string type, string DBPath = @"TL.db")
|
|
{
|
|
//try { AddPost(rootBlogName, postID, DBPath); } catch { }
|
|
try { AddBlog(noteBlogName, DBPath); } catch { }
|
|
|
|
Console.WriteLine("{2}\t{0}\t{1}", UnixTimeStampToDateTime(timestamp), noteBlogName, type);
|
|
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Notes SET timestamp = @timestamp WHERE rootBlogName = @rootBlogName AND noteBlogName = @noteBlogName AND PostID = @postID";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@timestamp", timestamp);
|
|
command.Parameters.AddWithValue("@rootBlogName", rootBlogName);
|
|
command.Parameters.AddWithValue("@noteBlogName", noteBlogName);
|
|
command.Parameters.AddWithValue("@postID", postID);
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Notes.RootBlogName, Notes.PostID, Notes.TimeStamp, Notes.Type, Notes.NoteBlogName")
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
Console.WriteLine("^^^^^ - SHORTCUT");
|
|
return true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static void UpdatePost(string blogName, long postID, string reblogURL, string postDate, string postURL, string slug, string reblogKey, string reblogName, string summary, string quote, string body, string tags, string link, string photoURL, string photoCaption, string downloadedFiles, string audioCaption, string question, string answer, string title, bool hasImage, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Posts SET ";
|
|
sql += "postDate = @postDate, ";
|
|
sql += "reblogURL = @reblogURL, ";
|
|
sql += "postURL = @postURL, ";
|
|
sql += "slug = @slug, ";
|
|
sql += "reblogKey = @reblogKey, ";
|
|
sql += "reblogName = @reblogName, ";
|
|
sql += "summary = @summary, ";
|
|
sql += "quote = @quote, ";
|
|
sql += "body = @body, ";
|
|
sql += "tags = @tags, ";
|
|
sql += "link = @link, ";
|
|
sql += "photoURL = @photoURL, ";
|
|
sql += "photoCaption = @photoCaption, ";
|
|
sql += "downloadedFiles = @downloadedFiles, ";
|
|
sql += "audioCaption = @audioCaption, ";
|
|
sql += "question = @question, ";
|
|
sql += "answer = @answer, ";
|
|
sql += "title = @title, ";
|
|
sql += "hasImage = @hasImage ";
|
|
sql += " WHERE BlogName = @BlogName AND PostID = @PostID";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@postDate", postDate ?? string.Empty);
|
|
command.Parameters.AddWithValue("@reblogURL", reblogURL ?? string.Empty);
|
|
command.Parameters.AddWithValue("@postURL", postURL ?? string.Empty);
|
|
command.Parameters.AddWithValue("@slug", slug ?? string.Empty);
|
|
command.Parameters.AddWithValue("@reblogKey", reblogKey ?? string.Empty);
|
|
command.Parameters.AddWithValue("@reblogName", reblogName ?? string.Empty);
|
|
command.Parameters.AddWithValue("@summary", summary ?? string.Empty);
|
|
command.Parameters.AddWithValue("@quote", quote ?? string.Empty);
|
|
command.Parameters.AddWithValue("@body", body ?? string.Empty);
|
|
command.Parameters.AddWithValue("@tags", tags ?? string.Empty);
|
|
command.Parameters.AddWithValue("@link", link ?? string.Empty);
|
|
command.Parameters.AddWithValue("@photoURL", photoURL ?? string.Empty);
|
|
command.Parameters.AddWithValue("@photoCaption", photoCaption ?? string.Empty);
|
|
command.Parameters.AddWithValue("@downloadedFiles", downloadedFiles ?? string.Empty);
|
|
command.Parameters.AddWithValue("@audioCaption", audioCaption ?? string.Empty);
|
|
command.Parameters.AddWithValue("@question", question ?? string.Empty);
|
|
command.Parameters.AddWithValue("@answer", answer ?? string.Empty);
|
|
command.Parameters.AddWithValue("@title", title ?? string.Empty);
|
|
command.Parameters.AddWithValue("@hasImage", hasImage ? 1 : 0);
|
|
command.Parameters.AddWithValue("@BlogName", blogName);
|
|
command.Parameters.AddWithValue("@PostID", postID);
|
|
|
|
command.ExecuteNonQuery();
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static void UpdateBlogOutput(string blogName, string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE Blogs SET HasBeenOutput = 1 WHERE BlogName = @BlogName";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@BlogName", blogName);
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
}
|
|
|
|
public static int UpdateAPICount(string DBPath = @"TL.db")
|
|
{
|
|
SQLiteConnection connection = new SQLiteConnection("Data Source=" + DBPath);
|
|
|
|
int APICount = DataAccess.GetAPICount();
|
|
APICount++;
|
|
|
|
try
|
|
{
|
|
connection.Open();
|
|
|
|
string sql = "UPDATE DailyAPICount SET APICount = @APICount WHERE [Date] = @date";
|
|
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
|
|
{
|
|
command.Parameters.AddWithValue("@APICount", APICount);
|
|
command.Parameters.AddWithValue("@date", DateTime.Today.ToShortDateString());
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.Message != "constraint failed\r\nUNIQUE constraint failed: Posts.BlogName, Posts.PostID")
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
connection.Close();
|
|
}
|
|
|
|
return APICount;
|
|
}
|
|
#endregion Updates
|
|
}
|
|
|
|
internal class APIAccess
|
|
{
|
|
public static string Blog { get; set; } = string.Empty;
|
|
private static IConfiguration Configuration { get; set; } = null!;
|
|
|
|
static APIAccess()
|
|
{
|
|
Configuration = new ConfigurationBuilder()
|
|
.SetBasePath(Directory.GetCurrentDirectory())
|
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
|
.Build();
|
|
|
|
if (Configuration["TumblrApi:ConsumerKey"] == null)
|
|
throw new InvalidOperationException("TumblrApi configuration is missing in appsettings.json");
|
|
}
|
|
|
|
private static string ConsumerKey => Configuration["TumblrApi:ConsumerKey"] ??
|
|
throw new InvalidOperationException("ConsumerKey is not configured");
|
|
private static string ConsumerSecret => Configuration["TumblrApi:ConsumerSecret"] ??
|
|
throw new InvalidOperationException("ConsumerSecret is not configured");
|
|
private static string OAuthToken => Configuration["TumblrApi:OAuthToken"] ??
|
|
throw new InvalidOperationException("OAuthToken is not configured");
|
|
private static string OAuthTokenSecret => Configuration["TumblrApi:OAuthTokenSecret"] ??
|
|
throw new InvalidOperationException("OAuthTokenSecret is not configured");
|
|
|
|
public static async Task<Root> GrabNotes(string blog, long ID, string? timestamp = null)
|
|
{
|
|
var URL = "https://api.tumblr.com/v2/blog/[0].tumblr.com/notes?id=[1]";
|
|
URL = URL.Replace("[0]", blog).Replace("[1]", ID.ToString());
|
|
if (!string.IsNullOrEmpty(timestamp))
|
|
{
|
|
URL += "&before_timestamp=" + timestamp;
|
|
await Task.Delay(100);
|
|
}
|
|
var client = new RestClient(URL);
|
|
var oAuth1 = OAuth1Authenticator.ForAccessToken(consumerKey: ConsumerKey,
|
|
consumerSecret: ConsumerSecret,
|
|
token: OAuthToken,
|
|
tokenSecret: OAuthTokenSecret,
|
|
OAuthSignatureMethod.HmacSha1
|
|
);
|
|
|
|
client.Authenticator = oAuth1;
|
|
var request = new RestRequest(URL, Method.Get);
|
|
var response = await client.ExecuteAsync(request);
|
|
|
|
var myJsonResponse = response.Content ?? string.Empty;
|
|
Console.WriteLine($"{timestamp}\t{DateTime.Now}\t{DataAccess.UpdateAPICount()}");
|
|
var myDeserializedClass = new Root();
|
|
|
|
try
|
|
{
|
|
var deserializedResult = JsonConvert.DeserializeObject<Root>(myJsonResponse);
|
|
if (deserializedResult != null)
|
|
{
|
|
myDeserializedClass = deserializedResult;
|
|
myDeserializedClass.rawJson = myJsonResponse;
|
|
|
|
// If the response JSON indicates a 429 (Too Many Requests) via meta.status or message,
|
|
// treat it like a rate-limited response and attempt to read Retry headers.
|
|
bool metaIndicatesRateLimit = myDeserializedClass.meta != null && myDeserializedClass.meta.status == 429;
|
|
bool metaMsgIndicatesRateLimit = myDeserializedClass.meta != null && !string.IsNullOrEmpty(myDeserializedClass.meta.msg) && myDeserializedClass.meta.msg.IndexOf("Too Many", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
|
|
if (metaIndicatesRateLimit || metaMsgIndicatesRateLimit || (response != null && (response.StatusDescription?.IndexOf("Too Many", StringComparison.OrdinalIgnoreCase) >= 0 || response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)))
|
|
{
|
|
// populate retryInSeconds from headers if possible
|
|
if (response?.Headers != null)
|
|
{
|
|
bool checkResetLocal = false;
|
|
foreach (var header in response.Headers)
|
|
{
|
|
string? headerName = header?.Name;
|
|
string? headerValue = header?.Value?.ToString();
|
|
if (string.IsNullOrEmpty(headerName) || string.IsNullOrEmpty(headerValue))
|
|
continue;
|
|
|
|
if (string.Equals(headerName, "Retry-After", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (int.TryParse(headerValue, out int retrySecs))
|
|
myDeserializedClass.retryInSeconds = Math.Max(myDeserializedClass.retryInSeconds, retrySecs);
|
|
else if (DateTimeOffset.TryParse(headerValue, out var dto))
|
|
myDeserializedClass.retryInSeconds = Math.Max(myDeserializedClass.retryInSeconds, (int)Math.Max(0, (dto - DateTimeOffset.UtcNow).TotalSeconds));
|
|
}
|
|
|
|
if (headerName.IndexOf("X-RateLimit-Reset", StringComparison.OrdinalIgnoreCase) >= 0 && long.TryParse(headerValue, out long epoch))
|
|
{
|
|
var secs = (int)Math.Max(0, epoch - DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
myDeserializedClass.retryInSeconds = Math.Max(myDeserializedClass.retryInSeconds, secs);
|
|
}
|
|
|
|
if (headerName.Contains("Remaining", StringComparison.OrdinalIgnoreCase) && headerValue == "0")
|
|
checkResetLocal = true;
|
|
|
|
if (checkResetLocal && headerName.IndexOf("Reset", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
if (int.TryParse(headerValue, out int resetValue))
|
|
myDeserializedClass.retryInSeconds = Math.Max(myDeserializedClass.retryInSeconds, resetValue);
|
|
else if (long.TryParse(headerValue, out long epochVal))
|
|
myDeserializedClass.retryInSeconds = Math.Max(myDeserializedClass.retryInSeconds, (int)Math.Max(0, epochVal - DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// surface rate-limit status back to caller
|
|
myDeserializedClass.statusCode = "TooManyRequests";
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Failed JSON: {myJsonResponse}");
|
|
Console.WriteLine(ex.ToString());
|
|
|
|
if (!response.IsSuccessful)
|
|
{
|
|
// Response.StatusCode may be null with some RestSharp responses.
|
|
// Guard it and still attempt to extract retry time from headers (Retry-After, Remaining/Reset, X-RateLimit-Reset)
|
|
string? statusStr = null;
|
|
try { statusStr = response != null ? response.StatusCode.ToString() : null; } catch { statusStr = null; }
|
|
Console.WriteLine($"{statusStr}\t{response?.StatusDescription}");
|
|
// Only set the status if we actually have one; otherwise leave existing value alone (may be null)
|
|
if (!string.IsNullOrEmpty(statusStr))
|
|
myDeserializedClass.statusCode = statusStr;
|
|
|
|
bool checkReset = false;
|
|
|
|
if ((myDeserializedClass.statusCode != "NotFound" || myDeserializedClass.retryInSeconds > 0) && response.Headers != null)
|
|
{
|
|
bool foundRateLimitHeader = false;
|
|
foreach (var header in response.Headers)
|
|
{
|
|
if (header.Name != null && header.Value != null)
|
|
{
|
|
Console.WriteLine($"{header.Name} - {header.Value}");
|
|
// Common header patterns used by APIs to indicate retry times:
|
|
// - Retry-After: either seconds or HTTP date
|
|
// - X-RateLimit-Reset: often seconds since epoch
|
|
// - <something>Reset (after Remaining==0): seconds
|
|
var headerValue = header.Value?.ToString();
|
|
if (!string.IsNullOrEmpty(headerValue))
|
|
{
|
|
// 1) Retry-After header (seconds or date)
|
|
if (string.Equals(header.Name, "Retry-After", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (int.TryParse(headerValue, out int retrySecs))
|
|
{
|
|
if (myDeserializedClass.retryInSeconds < retrySecs)
|
|
myDeserializedClass.retryInSeconds = retrySecs;
|
|
}
|
|
else if (DateTimeOffset.TryParse(headerValue, out DateTimeOffset dto))
|
|
{
|
|
var secs = (int)Math.Max(0, (dto - DateTimeOffset.UtcNow).TotalSeconds);
|
|
if (myDeserializedClass.retryInSeconds < secs)
|
|
myDeserializedClass.retryInSeconds = secs;
|
|
}
|
|
}
|
|
|
|
// 2) Common 'Reset' header: numeric seconds or epoch seconds
|
|
if (checkReset && header.Name.Contains("Reset", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (int.TryParse(headerValue, out int resetValue))
|
|
{
|
|
if (myDeserializedClass.retryInSeconds < resetValue)
|
|
myDeserializedClass.retryInSeconds = resetValue;
|
|
}
|
|
else if (long.TryParse(headerValue, out long epochVal))
|
|
{
|
|
// treat as epoch seconds -> compute secs until that epoch
|
|
var secs = (int)Math.Max(0, epochVal - DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
if (myDeserializedClass.retryInSeconds < secs)
|
|
myDeserializedClass.retryInSeconds = secs;
|
|
}
|
|
}
|
|
|
|
// 3) X-RateLimit-Reset header: often epoch seconds
|
|
if (header.Name.IndexOf("X-RateLimit-Reset", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
if (long.TryParse(headerValue, out long epoch))
|
|
{
|
|
var secs = (int)Math.Max(0, epoch - DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
if (myDeserializedClass.retryInSeconds < secs)
|
|
myDeserializedClass.retryInSeconds = secs;
|
|
}
|
|
foundRateLimitHeader = true;
|
|
}
|
|
}
|
|
if (header.Name.Contains("Remaining", StringComparison.OrdinalIgnoreCase) && header.Value.ToString() == "0")
|
|
checkReset = true;
|
|
else
|
|
checkReset = false;
|
|
}
|
|
|
|
// If we detected rate-limit related headers (Retry-After / X-RateLimit-Reset / Remaining/Reset)
|
|
// or the HTTP status indicates 429, surface it.
|
|
if (foundRateLimitHeader || (response != null && response.StatusCode == System.Net.HttpStatusCode.TooManyRequests))
|
|
{
|
|
myDeserializedClass.statusCode = "TooManyRequests";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return myDeserializedClass;
|
|
}
|
|
}
|
|
}
|