From 161faac8a53361ac0c18653d7fe740a8ca1f4d35 Mon Sep 17 00:00:00 2001 From: Casper Sparre Date: Fri, 20 Feb 2026 18:56:00 +0100 Subject: [PATCH] Enabled updating of User Info during scrape --- Cajetan.OF-DL/ProgramCajetan.cs | 9 +- Cajetan.OF-DL/Properties/GlobalUsings.cs | 13 ++- Cajetan.OF-DL/Services/CajetanApiService.cs | 94 ++++++++++++++++ Cajetan.OF-DL/Services/CajetanDbService.cs | 107 +++++++++++++++++++ Cajetan.OF-DL/Services/ICajetanApiService.cs | 9 ++ Cajetan.OF-DL/Services/ICajetanDbService.cs | 9 ++ Cajetan.OF-DL/Worker.cs | 9 +- 7 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 Cajetan.OF-DL/Services/CajetanApiService.cs create mode 100644 Cajetan.OF-DL/Services/CajetanDbService.cs create mode 100644 Cajetan.OF-DL/Services/ICajetanApiService.cs create mode 100644 Cajetan.OF-DL/Services/ICajetanDbService.cs diff --git a/Cajetan.OF-DL/ProgramCajetan.cs b/Cajetan.OF-DL/ProgramCajetan.cs index 4602ce5..eb5296e 100644 --- a/Cajetan.OF-DL/ProgramCajetan.cs +++ b/Cajetan.OF-DL/ProgramCajetan.cs @@ -62,8 +62,13 @@ static async Task ConfigureServices(string[] args) services.AddSingleton(cajetanConfig); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Cajetan.OF-DL/Properties/GlobalUsings.cs b/Cajetan.OF-DL/Properties/GlobalUsings.cs index 32d2d56..458741a 100644 --- a/Cajetan.OF-DL/Properties/GlobalUsings.cs +++ b/Cajetan.OF-DL/Properties/GlobalUsings.cs @@ -1,5 +1,4 @@ -global using Serilog; -global using Spectre.Console; +global using Newtonsoft.Json; global using OF_DL; global using OF_DL.CLI; global using OF_DL.Crypto; @@ -8,5 +7,13 @@ global using OF_DL.Exceptions; global using OF_DL.Helpers; global using OF_DL.Models; global using OF_DL.Models.Config; +global using OF_DL.Models.Downloads; global using OF_DL.Services; -global using OF_DL.Utils; +global using Serilog; +global using Serilog.Context; +global using Spectre.Console; +global using MessageDtos = OF_DL.Models.Dtos.Messages; +global using MessageEntities = OF_DL.Models.Entities.Messages; +global using SubscriptionDtos = OF_DL.Models.Dtos.Subscriptions; +global using UserDtos = OF_DL.Models.Dtos.Users; +global using UserEntities = OF_DL.Models.Entities.Users; diff --git a/Cajetan.OF-DL/Services/CajetanApiService.cs b/Cajetan.OF-DL/Services/CajetanApiService.cs new file mode 100644 index 0000000..44b8f57 --- /dev/null +++ b/Cajetan.OF-DL/Services/CajetanApiService.cs @@ -0,0 +1,94 @@ +namespace OF_DL.Services; + +public interface ICajetanApiService : IApiService +{ + Task GetDetailedUserInfo(string endpoint); +} + +public class CajetanApiService(IAuthService authService, IConfigService configService, ICajetanDbService dbService) + : ApiService(authService, configService, dbService), ICajetanApiService +{ + public new async Task GetUserInfo(string endpoint) + { + UserEntities.UserInfo? userInfo = await GetDetailedUserInfoAsync(endpoint); + + if (userInfo is not null && !endpoint.EndsWith("/me")) + await dbService.UpdateUserInfoAsync(userInfo); + + return userInfo; + } + + public async Task GetDetailedUserInfoAsync(string endpoint) + { + Log.Debug($"Calling GetDetailedUserInfo: {endpoint}"); + + if (!HasSignedRequestAuth()) + return null; + + try + { + UserEntities.UserInfo userInfo = new(); + Dictionary getParams = new() + { + { "limit", Constants.ApiPageSize.ToString() }, { "order", "publish_date_asc" } + }; + + HttpClient client = new(); + HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint); + + using HttpResponseMessage response = await client.SendAsync(request); + + if (!response.IsSuccessStatusCode) + return userInfo; + + response.EnsureSuccessStatusCode(); + string body = await response.Content.ReadAsStringAsync(); + UserDtos.UserDto? userDto = JsonConvert.DeserializeObject(body, s_mJsonSerializerSettings); + userInfo = FromDto(userDto); + return userInfo; + } + catch (Exception ex) + { + ExceptionLoggerHelper.LogException(ex); + } + + return null; + } + + private static UserEntities.UserInfo FromDto(UserDtos.UserDto? userDto) + { + if (userDto is null) + return new(); + + return new() + { + Id = userDto.Id, + + Avatar = userDto.Avatar, + Header = userDto.Header, + Name = userDto.Name, + Username = userDto.Username, + + SubscribePrice = userDto.SubscribePrice, + CurrentSubscribePrice = userDto.CurrentSubscribePrice, + IsPaywallRequired = userDto.IsPaywallRequired, + IsRestricted = userDto.IsRestricted, + SubscribedBy = userDto.SubscribedBy, + SubscribedByExpire = userDto.SubscribedByExpire, + SubscribedByExpireDate = userDto.SubscribedByExpireDate, + SubscribedByAutoprolong = userDto.SubscribedByAutoprolong, + SubscribedIsExpiredNow = userDto.SubscribedIsExpiredNow, + SubscribedOn = userDto.SubscribedOn, + SubscribedOnExpiredNow = userDto.SubscribedOnExpiredNow, + SubscribedOnDuration = userDto.SubscribedOnDuration, + About = userDto.About, + PostsCount = userDto.PostsCount, + ArchivedPostsCount = userDto.ArchivedPostsCount, + PrivateArchivedPostsCount = userDto.PrivateArchivedPostsCount, + PhotosCount = userDto.PhotosCount, + VideosCount = userDto.VideosCount, + AudiosCount = userDto.AudiosCount, + MediasCount = userDto.MediasCount, + }; + } +} diff --git a/Cajetan.OF-DL/Services/CajetanDbService.cs b/Cajetan.OF-DL/Services/CajetanDbService.cs new file mode 100644 index 0000000..8800c85 --- /dev/null +++ b/Cajetan.OF-DL/Services/CajetanDbService.cs @@ -0,0 +1,107 @@ +using Microsoft.Data.Sqlite; + +namespace OF_DL.Services; + +public class CajetanDbService(IConfigService configService) + : DbService(configService), ICajetanDbService +{ + public async Task InitializeUserInfoTablesAsync() + { + await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + + connection.Open(); + + using (SqliteCommand cmdInfo = new("CREATE TABLE IF NOT EXISTS user_info (user_id INTEGER NOT NULL, name VARCHAR NOT NULL, about VARCHAR NULL, expires_on TIMESTAMP NULL, photo_count INT NOT NULL, video_count INT NOT NULL, PRIMARY KEY(user_id));", connection)) + { + await cmdInfo.ExecuteNonQueryAsync(); + } + + using (SqliteCommand cmdInfo = new("CREATE TABLE IF NOT EXISTS user_info_blob (user_id INTEGER NOT NULL, name VARCHAR NOT NULL, blob TEXT NULL, PRIMARY KEY(user_id));", connection)) + { + await cmdInfo.ExecuteNonQueryAsync(); + } + + } + + public async Task> GetUsersAsync() + { + await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + + using SqliteCommand cmd = new("SELECT user_id, username FROM users", connection); + using SqliteDataReader reader = cmd.ExecuteReader(); + + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + + while (reader.Read()) + { + long userId = reader.GetInt64(0); + string username = reader.GetString(1); + + result[username] = userId; + } + + return result; + } + + public async Task UpdateUserInfoAsync(UserEntities.UserInfo? userInfo) + { + if (userInfo?.Id is null || userInfo?.Username is null) + return; + + await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db"); + + Log.Debug("Database data source: " + connection.DataSource); + + await UpdateAsync(); + await UpdateBlobAsync(); + + async Task UpdateAsync() + { + using SqliteCommand cmdInfo = new( + "INSERT OR REPLACE INTO user_info (user_id, name, about, expires_on, photo_count, video_count) " + + "VALUES (@userId, @name, @about, @expiresOn, @photoCount, @videoCount);", + connection + ); + + cmdInfo.Parameters.AddWithValue("@userId", userInfo.Id); + cmdInfo.Parameters.AddWithValue("@name", userInfo.Name ?? userInfo.Username); + cmdInfo.Parameters.AddWithValue("@about", userInfo.About); + cmdInfo.Parameters.AddWithValue("@expiresOn", userInfo.SubscribedByExpireDate); + cmdInfo.Parameters.AddWithValue("@photoCount", userInfo.PhotosCount ?? 0); + cmdInfo.Parameters.AddWithValue("@videoCount", userInfo.VideosCount ?? 0); + + try + { + await cmdInfo.ExecuteNonQueryAsync(); + Log.Debug("Inserted or updated creator info: {Username:l}", userInfo.Username); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to update User Info for: {Username:l}", userInfo.Username); + } + } + + async Task UpdateBlobAsync() + { + using SqliteCommand cmdInfo = new( + "INSERT OR REPLACE INTO user_info_blob (user_id, name, blob) " + + "VALUES (@userId, @name, @blob);", + connection + ); + + cmdInfo.Parameters.AddWithValue("@userId", userInfo.Id); + cmdInfo.Parameters.AddWithValue("@name", userInfo.Name ?? userInfo.Username); + cmdInfo.Parameters.AddWithValue("@blob", Newtonsoft.Json.JsonConvert.SerializeObject(userInfo)); + + try + { + await cmdInfo.ExecuteNonQueryAsync(); + Log.Debug("Inserted or updated creator blob: {Username:l}", userInfo.Username); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to update User Info Blob for: {Username:l}", userInfo.Username); + } + } + } +} diff --git a/Cajetan.OF-DL/Services/ICajetanApiService.cs b/Cajetan.OF-DL/Services/ICajetanApiService.cs new file mode 100644 index 0000000..e863580 --- /dev/null +++ b/Cajetan.OF-DL/Services/ICajetanApiService.cs @@ -0,0 +1,9 @@ +namespace OF_DL.Services; + +public interface ICajetanApiService : IApiService +{ + Task GetDetailedUserInfoAsync(string endpoint); + Task> GetUsersWithProgressAsync(string typeDisplay, string endpoint, string? typeParam, bool offsetByCount); + Task> GetUsersWithUnreadMessagesAsync(); + Task MarkAsUnreadAsync(string endpoint); +} diff --git a/Cajetan.OF-DL/Services/ICajetanDbService.cs b/Cajetan.OF-DL/Services/ICajetanDbService.cs new file mode 100644 index 0000000..12e204e --- /dev/null +++ b/Cajetan.OF-DL/Services/ICajetanDbService.cs @@ -0,0 +1,9 @@ +namespace OF_DL.Services; + +public interface ICajetanDbService : IDbService +{ + Task InitializeUserInfoTablesAsync(); + + Task> GetUsersAsync(); + Task UpdateUserInfoAsync(UserEntities.UserInfo? userInfo); +} diff --git a/Cajetan.OF-DL/Worker.cs b/Cajetan.OF-DL/Worker.cs index b620ebe..45ead6b 100644 --- a/Cajetan.OF-DL/Worker.cs +++ b/Cajetan.OF-DL/Worker.cs @@ -1,6 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using OF_DL.Models.Downloads; -using OF_DL.Models.Entities.Users; namespace OF_DL; @@ -10,8 +8,8 @@ internal class Worker(IServiceProvider serviceProvider) private readonly IAuthService _authService = serviceProvider.GetRequiredService(); private readonly IStartupService _startupService = serviceProvider.GetRequiredService(); private readonly IDownloadOrchestrationService _orchestrationService = serviceProvider.GetRequiredService(); - private readonly IDbService _dbService = serviceProvider.GetRequiredService(); - private readonly IApiService _apiService = serviceProvider.GetRequiredService(); + private readonly ICajetanDbService _dbService = serviceProvider.GetRequiredService(); + private readonly ICajetanApiService _apiService = serviceProvider.GetRequiredService(); private readonly ExitHelper _exitHelper = serviceProvider.GetRequiredService(); private readonly CajetanConfig _cajetanConfig = serviceProvider.GetRequiredService(); @@ -115,7 +113,7 @@ internal class Worker(IServiceProvider serviceProvider) // Validate cookie string _authService.ValidateCookieString(); - User? user = await _authService.ValidateAuthAsync(); + UserEntities.User? user = await _authService.ValidateAuthAsync(); if (user is null || (user.Name is null && user.Username is null)) { Log.Error("Auth failed"); @@ -288,6 +286,7 @@ internal class Worker(IServiceProvider serviceProvider) AnsiConsole.WriteLine(); await _dbService.CreateUsersDb(result.Users); + await _dbService.InitializeUserInfoTablesAsync(); return result;