Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96aaebd72d | |||
| 8d783b512c | |||
| 21efebe74f | |||
| a1dce61b33 | |||
| c170bba96c | |||
| 7e9eba54e6 | |||
| ea5261bc62 | |||
| d098355587 | |||
| b3538e5e70 | |||
| a1c4792e61 | |||
| 0ceb0c1cc8 | |||
| 5921b164bd | |||
| 6c7a171f0e | |||
| 9df2490acf | |||
| 4a039b3de4 | |||
| 5c59e09a68 | |||
| 7754bb2252 | |||
| 51f42bddff | |||
| a96f9f3da6 | |||
| 452540bfd1 | |||
| 8d5cc39722 | |||
| 8eaa14ae03 | |||
| baddcfda4e | |||
| 7a4b145641 | |||
| 16096c5815 | |||
| 3c8231d329 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -374,3 +374,7 @@ venv/
|
|||||||
|
|
||||||
# Generated docs
|
# Generated docs
|
||||||
/site
|
/site
|
||||||
|
|
||||||
|
# Cajetan
|
||||||
|
.dev/
|
||||||
|
*Publish/
|
||||||
8
Cajetan.OF-DL.slnx
Normal file
8
Cajetan.OF-DL.slnx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/Cajetan/">
|
||||||
|
<Project Path="Cajetan.OF-DL/Cajetan.OF-DL.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="OF DL.Core/OF DL.Core.csproj" />
|
||||||
|
<Project Path="OF DL.Tests/OF DL.Tests.csproj" />
|
||||||
|
<Project Path="OF DL/OF DL.csproj" />
|
||||||
|
</Solution>
|
||||||
45
Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs
Normal file
45
Cajetan.OF-DL/CLI/CajetanDownloadEventHandler.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using OF_DL.Models.Downloads;
|
||||||
|
|
||||||
|
namespace OF_DL.CLI;
|
||||||
|
|
||||||
|
public interface ICajetanDownloadEventHandler : IDownloadEventHandler
|
||||||
|
{
|
||||||
|
void OnMessage(string message, string color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CajetanDownloadEventHandler : ICajetanDownloadEventHandler
|
||||||
|
{
|
||||||
|
private readonly SpectreDownloadEventHandler _eventHandler = new();
|
||||||
|
|
||||||
|
public void OnContentFound(string contentType, int mediaCount, int objectCount)
|
||||||
|
=> _eventHandler.OnContentFound(contentType, mediaCount, objectCount);
|
||||||
|
|
||||||
|
public void OnDownloadComplete(string contentType, DownloadResult result)
|
||||||
|
=> _eventHandler.OnDownloadComplete(contentType, result);
|
||||||
|
|
||||||
|
public void OnMessage(string message)
|
||||||
|
=> _eventHandler.OnMessage(message);
|
||||||
|
|
||||||
|
public void OnMessage(string message, string color)
|
||||||
|
=> AnsiConsole.MarkupLine($"[{color.ToLowerInvariant()}]{Markup.Escape(message)}[/]");
|
||||||
|
|
||||||
|
public void OnNoContentFound(string contentType)
|
||||||
|
=> _eventHandler.OnNoContentFound(contentType);
|
||||||
|
|
||||||
|
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount)
|
||||||
|
=> _eventHandler.OnPurchasedTabUserComplete(username, paidPostCount, paidMessagesCount);
|
||||||
|
|
||||||
|
public void OnScrapeComplete(TimeSpan elapsed)
|
||||||
|
=> _eventHandler.OnScrapeComplete(elapsed);
|
||||||
|
|
||||||
|
public void OnUserComplete(string username, CreatorDownloadResult result)
|
||||||
|
=> _eventHandler.OnUserComplete(username, result);
|
||||||
|
|
||||||
|
public void OnUserStarting(string username) { }
|
||||||
|
|
||||||
|
public Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize, Func<IProgressReporter, Task<T>> work)
|
||||||
|
=> _eventHandler.WithProgressAsync(description, maxValue, showSize, work);
|
||||||
|
|
||||||
|
public Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
|
||||||
|
=> _eventHandler.WithStatusAsync(statusMessage, work);
|
||||||
|
}
|
||||||
50
Cajetan.OF-DL/Cajetan.OF-DL.csproj
Normal file
50
Cajetan.OF-DL/Cajetan.OF-DL.csproj
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RootNamespace>OF_DL</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationIcon>Icon\download.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Icon\download.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Spectre.Console">
|
||||||
|
<HintPath>..\OF DL\References\Spectre.Console.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OF DL\OF DL.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Akka" Version="1.5.60"/>
|
||||||
|
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1"/>
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4"/>
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56"/>
|
||||||
|
<PackageReference Include="PuppeteerSharp" Version="20.2.6"/>
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.1"/>
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1"/>
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0"/>
|
||||||
|
<PackageReference Include="System.Reactive" Version="6.1.0"/>
|
||||||
|
<PackageReference Include="xFFmpeg.NET" Version="7.2.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
Cajetan.OF-DL/Exceptions/ExitCodeException.cs
Normal file
6
Cajetan.OF-DL/Exceptions/ExitCodeException.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Exceptions;
|
||||||
|
|
||||||
|
public sealed class ExitCodeException(int exitCode) : Exception
|
||||||
|
{
|
||||||
|
public int ExitCode { get; } = exitCode;
|
||||||
|
}
|
||||||
BIN
Cajetan.OF-DL/Icon/download.ico
Normal file
BIN
Cajetan.OF-DL/Icon/download.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
9
Cajetan.OF-DL/Models/CajetanConfig.cs
Normal file
9
Cajetan.OF-DL/Models/CajetanConfig.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace OF_DL.Models;
|
||||||
|
|
||||||
|
public class CajetanConfig
|
||||||
|
{
|
||||||
|
public string[] NonInteractiveSpecificLists { get; set; } = [];
|
||||||
|
public string[] NonInteractiveSpecificUsers { get; set; } = [];
|
||||||
|
|
||||||
|
public EMode Mode { get; set; } = EMode.DownloadCreatorContent;
|
||||||
|
}
|
||||||
8
Cajetan.OF-DL/Models/Dtos/Lists/ListUsersDto.cs
Normal file
8
Cajetan.OF-DL/Models/Dtos/Lists/ListUsersDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace OF_DL.Models.Dtos.Lists;
|
||||||
|
|
||||||
|
public class ListUsersDto
|
||||||
|
{
|
||||||
|
[JsonProperty("list")] public List<UsersListDto> List { get; set; } = [];
|
||||||
|
[JsonProperty("hasMore")] public bool? HasMore { get; set; }
|
||||||
|
[JsonProperty("nextOffset")] public int NextOffset { get; set; }
|
||||||
|
}
|
||||||
19
Cajetan.OF-DL/Models/Dtos/Messages/ChatsDto.cs
Normal file
19
Cajetan.OF-DL/Models/Dtos/Messages/ChatsDto.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace OF_DL.Models.Dtos.Messages;
|
||||||
|
|
||||||
|
public class ChatsDto
|
||||||
|
{
|
||||||
|
[JsonProperty("list")] public List<ChatItemDto> List { get; set; } = [];
|
||||||
|
[JsonProperty("hasMore")] public bool HasMore { get; set; }
|
||||||
|
[JsonProperty("nextOffset")] public int NextOffset { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChatItemDto
|
||||||
|
{
|
||||||
|
[JsonProperty("withUser")] public ChatUserDto WithUser { get; set; } = new();
|
||||||
|
[JsonProperty("unreadMessagesCount")] public int UnreadMessagesCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChatUserDto
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public long Id { get; set; }
|
||||||
|
}
|
||||||
8
Cajetan.OF-DL/Models/EMode.cs
Normal file
8
Cajetan.OF-DL/Models/EMode.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace OF_DL.Models;
|
||||||
|
|
||||||
|
public enum EMode
|
||||||
|
{
|
||||||
|
DownloadCreatorContent,
|
||||||
|
OutputBlockedUsers,
|
||||||
|
UpdateAllUserInfo
|
||||||
|
}
|
||||||
19
Cajetan.OF-DL/Models/Extensions.cs
Normal file
19
Cajetan.OF-DL/Models/Extensions.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace OF_DL.Models.Downloads;
|
||||||
|
|
||||||
|
internal static class DownloadsExtensions
|
||||||
|
{
|
||||||
|
extension(CreatorDownloadResult result)
|
||||||
|
{
|
||||||
|
public void Add(CreatorDownloadResult other)
|
||||||
|
{
|
||||||
|
result.PaidPostCount += other.PaidPostCount;
|
||||||
|
result.PostCount += other.PostCount;
|
||||||
|
result.ArchivedCount += other.ArchivedCount;
|
||||||
|
result.StreamsCount += other.StreamsCount;
|
||||||
|
result.StoriesCount += other.StoriesCount;
|
||||||
|
result.HighlightsCount += other.HighlightsCount;
|
||||||
|
result.MessagesCount += other.MessagesCount;
|
||||||
|
result.PaidMessagesCount += other.PaidMessagesCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Cajetan.OF-DL/ProgramCajetan.cs
Normal file
184
Cajetan.OF-DL/ProgramCajetan.cs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
|
||||||
|
await RunAsync(args);
|
||||||
|
|
||||||
|
static async Task RunAsync(string[] args)
|
||||||
|
{
|
||||||
|
ServiceCollection services = await ConfigureServices(args);
|
||||||
|
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
ExitIfOtherProcess(serviceProvider);
|
||||||
|
|
||||||
|
Worker worker = serviceProvider.GetRequiredService<Worker>();
|
||||||
|
await worker.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task<ServiceCollection> ConfigureServices(string[] args)
|
||||||
|
{
|
||||||
|
// Set up dependency injection with LoggingService and ConfigService
|
||||||
|
ServiceCollection services = new();
|
||||||
|
services.AddSingleton<ILoggingService, CajetanLoggingService>();
|
||||||
|
services.AddSingleton<IConfigService, ConfigService>();
|
||||||
|
services.AddSingleton(new ExitHelper(new SpectreDownloadEventHandler()));
|
||||||
|
ServiceProvider tempServiceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
ILoggingService loggingService = tempServiceProvider.GetRequiredService<ILoggingService>();
|
||||||
|
IConfigService configService = tempServiceProvider.GetRequiredService<IConfigService>();
|
||||||
|
ExitHelper exitHelper = tempServiceProvider.GetRequiredService<ExitHelper>();
|
||||||
|
|
||||||
|
if (!await configService.LoadConfigurationAsync(args))
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[red]config.conf is not valid, check your syntax![/]\n");
|
||||||
|
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
|
||||||
|
Console.ReadKey();
|
||||||
|
|
||||||
|
exitHelper.ExitWithCode(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configService.CurrentConfig.NonInteractiveMode is false)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[red]Cannot run in Interactive Mode![/]\n");
|
||||||
|
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
|
||||||
|
Console.ReadKey();
|
||||||
|
|
||||||
|
exitHelper.ExitWithCode(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Markup("[green]config.conf located successfully!\n[/]");
|
||||||
|
|
||||||
|
CajetanConfig cajetanConfig = ParseCommandlineArgs(args, configService.CurrentConfig);
|
||||||
|
|
||||||
|
// Set up full dependency injection with loaded config
|
||||||
|
services = [];
|
||||||
|
services.AddSingleton(loggingService);
|
||||||
|
services.AddSingleton(configService);
|
||||||
|
services.AddSingleton(exitHelper);
|
||||||
|
services.AddSingleton(cajetanConfig);
|
||||||
|
|
||||||
|
services.AddSingleton<IAuthService, AuthService>();
|
||||||
|
services.AddSingleton<IStartupService, StartupService>();
|
||||||
|
services.AddSingleton<IFileNameService, FileNameService>();
|
||||||
|
|
||||||
|
services.AddSingleton<ICajetanDownloadService, CajetanDownloadService>();
|
||||||
|
services.AddSingleton<IDownloadService>(sp => sp.GetRequiredService<ICajetanDownloadService>());
|
||||||
|
|
||||||
|
services.AddSingleton<ICajetanApiService, CajetanApiService>();
|
||||||
|
services.AddSingleton<IApiService>(sp => sp.GetRequiredService<ICajetanApiService>());
|
||||||
|
|
||||||
|
services.AddSingleton<ICajetanDbService, CajetanDbService>();
|
||||||
|
services.AddSingleton<IDbService>(sp => sp.GetRequiredService<ICajetanDbService>());
|
||||||
|
|
||||||
|
|
||||||
|
services.AddSingleton<ICajetanDownloadOrchestrationService, CajetanDownloadOrchestrationService>();
|
||||||
|
services.AddSingleton<IDownloadOrchestrationService>(sp => sp.GetRequiredService<ICajetanDownloadOrchestrationService>());
|
||||||
|
|
||||||
|
services.AddSingleton<ICajetanDownloadEventHandler, CajetanDownloadEventHandler>();
|
||||||
|
services.AddSingleton<IDownloadEventHandler>(sp => sp.GetRequiredService<ICajetanDownloadEventHandler>());
|
||||||
|
|
||||||
|
services.AddSingleton<Worker>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CajetanConfig ParseCommandlineArgs(string[] args, Config currentConfig)
|
||||||
|
{
|
||||||
|
const string SPECIFIC_LISTS_ARG = "--specific-lists";
|
||||||
|
const string SPECIFIC_USERS_ARG = "--specific-users";
|
||||||
|
const string OUTPUT_BLOCKED_USERS_ARG = "--output-blocked";
|
||||||
|
const string UPDATE_ALL_USER_INFO_ARG = "--update-userinfo";
|
||||||
|
|
||||||
|
CajetanConfig parsedConfig = new();
|
||||||
|
|
||||||
|
if (ParseListAndUserArguments(ref parsedConfig))
|
||||||
|
return parsedConfig;
|
||||||
|
|
||||||
|
if (ParseFlagArgument(OUTPUT_BLOCKED_USERS_ARG, EMode.OutputBlockedUsers, ref parsedConfig))
|
||||||
|
return parsedConfig;
|
||||||
|
|
||||||
|
if (ParseFlagArgument(UPDATE_ALL_USER_INFO_ARG, EMode.UpdateAllUserInfo, ref parsedConfig))
|
||||||
|
return parsedConfig;
|
||||||
|
|
||||||
|
// Will process all active subscriptions
|
||||||
|
return parsedConfig;
|
||||||
|
|
||||||
|
bool ParseListAndUserArguments(ref CajetanConfig parsedConfig)
|
||||||
|
{
|
||||||
|
bool hasSpecificListsArg = ParseCommaSeparatedListArgument(SPECIFIC_LISTS_ARG, ref parsedConfig, (c, v) =>
|
||||||
|
{
|
||||||
|
c.NonInteractiveSpecificLists = v;
|
||||||
|
Log.Logger = Log.Logger.ForContext(nameof(CajetanConfig.NonInteractiveSpecificLists), string.Join(",", v));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasSpecificListsArg)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
|
||||||
|
bool hasSpecificUsersArg = ParseCommaSeparatedListArgument(SPECIFIC_USERS_ARG, ref parsedConfig, (c, v) =>
|
||||||
|
{
|
||||||
|
c.NonInteractiveSpecificUsers = v;
|
||||||
|
Log.Logger = Log.Logger.ForContext(nameof(CajetanConfig.NonInteractiveSpecificUsers), string.Join(",", v));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasSpecificUsersArg)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseCommaSeparatedListArgument(string argName, ref CajetanConfig parsedConfig, Action<CajetanConfig, string[]> assignmentFunc)
|
||||||
|
{
|
||||||
|
char[] separator = [','];
|
||||||
|
int indexOfArg = Array.FindIndex(args, a => argName.Equals(a, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (indexOfArg < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int indexOfListValues = indexOfArg + 1;
|
||||||
|
string[] strListValues = args.ElementAtOrDefault(indexOfListValues)?.Split(separator, StringSplitOptions.RemoveEmptyEntries) ?? [];
|
||||||
|
|
||||||
|
if (strListValues.Length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
assignmentFunc(parsedConfig, strListValues);
|
||||||
|
parsedConfig.Mode = EMode.DownloadCreatorContent;
|
||||||
|
|
||||||
|
Log.Logger = Log.Logger.ForContext("Mode", $"{EMode.DownloadCreatorContent}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseFlagArgument(string argName, EMode modeIfArgIsSet, ref CajetanConfig parsedConfig)
|
||||||
|
{
|
||||||
|
if (!args.Any(a => argName.Equals(a, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
currentConfig.NonInteractiveMode = true;
|
||||||
|
parsedConfig.Mode = modeIfArgIsSet;
|
||||||
|
|
||||||
|
Log.Logger = Log.Logger.ForContext("Mode", $"{modeIfArgIsSet}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExitIfOtherProcess(ServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
Assembly? entryAssembly = Assembly.GetEntryAssembly();
|
||||||
|
AssemblyName? entryAssemblyName = entryAssembly?.GetName();
|
||||||
|
|
||||||
|
if (entryAssemblyName?.Name is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Process thisProcess = Process.GetCurrentProcess();
|
||||||
|
Process[] otherProcesses = [.. Process.GetProcessesByName(entryAssemblyName.Name).Where(p => p.Id != thisProcess.Id)];
|
||||||
|
|
||||||
|
if (otherProcesses.Length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AnsiConsole.Markup($"[green]Other OF DL process detected, exiting..\n[/]");
|
||||||
|
Log.Warning("Other OF DL process detected, exiting..");
|
||||||
|
|
||||||
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
19
Cajetan.OF-DL/Properties/GlobalUsings.cs
Normal file
19
Cajetan.OF-DL/Properties/GlobalUsings.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
global using Newtonsoft.Json;
|
||||||
|
global using OF_DL;
|
||||||
|
global using OF_DL.CLI;
|
||||||
|
global using OF_DL.Enumerations;
|
||||||
|
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 Serilog;
|
||||||
|
global using Serilog.Context;
|
||||||
|
global using Spectre.Console;
|
||||||
|
global using ListsDtos = OF_DL.Models.Dtos.Lists;
|
||||||
|
global using MessageDtos = OF_DL.Models.Dtos.Messages;
|
||||||
|
global using MessageEntities = OF_DL.Models.Entities.Messages;
|
||||||
|
global using SubscriptionsDtos = OF_DL.Models.Dtos.Subscriptions;
|
||||||
|
global using UserDtos = OF_DL.Models.Dtos.Users;
|
||||||
|
global using UserEntities = OF_DL.Models.Entities.Users;
|
||||||
10
Cajetan.OF-DL/Properties/launchSettings.json
Normal file
10
Cajetan.OF-DL/Properties/launchSettings.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Cajetan.OF-DL": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"workingDirectory": "..\\.dev\\",
|
||||||
|
//"commandLineArgs": "--non-interactive --specific-lists Capture"
|
||||||
|
"commandLineArgs": "--non-interactive --specific-users amyboz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
462
Cajetan.OF-DL/Services/CajetanApiService.cs
Normal file
462
Cajetan.OF-DL/Services/CajetanApiService.cs
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public class CajetanApiService(IAuthService authService, IConfigService configService, ICajetanDbService dbService, ICajetanDownloadEventHandler eventHandler)
|
||||||
|
: ApiService(authService, configService, dbService), ICajetanApiService
|
||||||
|
{
|
||||||
|
private readonly ICajetanDownloadEventHandler _eventHandler = eventHandler;
|
||||||
|
|
||||||
|
public new async Task<UserEntities.User?> GetUserInfo(string endpoint)
|
||||||
|
{
|
||||||
|
UserEntities.UserInfo? userInfo = await GetDetailedUserInfoAsync(endpoint);
|
||||||
|
|
||||||
|
if (userInfo is not null && !endpoint.EndsWith("/me"))
|
||||||
|
await dbService.UpdateUserInfoAsync(userInfo);
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Task<Dictionary<string, long>?> GetActiveSubscriptions(string endpoint, bool includeRestricted)
|
||||||
|
{
|
||||||
|
Log.Debug("Calling GetActiveSubscriptions");
|
||||||
|
return GetAllSubscriptions(endpoint, includeRestricted, "active");
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Task<Dictionary<string, long>?> GetExpiredSubscriptions(string endpoint, bool includeRestricted)
|
||||||
|
{
|
||||||
|
Log.Debug("Calling GetExpiredSubscriptions");
|
||||||
|
return GetAllSubscriptions(endpoint, includeRestricted, "expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
public new async Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder, IStatusReporter statusReporter)
|
||||||
|
{
|
||||||
|
(bool couldExtract, long userId) = ExtractUserId(endpoint);
|
||||||
|
|
||||||
|
_eventHandler.OnMessage("Getting Unread Chats", "grey");
|
||||||
|
HashSet<long> usersWithUnread = couldExtract ? await GetUsersWithUnreadMessagesAsync() : [];
|
||||||
|
|
||||||
|
MessageEntities.MessageCollection messages = await base.GetMessages(endpoint, folder, statusReporter);
|
||||||
|
|
||||||
|
if (usersWithUnread.Contains(userId))
|
||||||
|
{
|
||||||
|
_eventHandler.OnMessage("Restoring unread state", "grey");
|
||||||
|
await MarkAsUnreadAsync($"/chats/{userId}/mark-as-read");
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
|
||||||
|
static (bool couldExtract, long userId) ExtractUserId(string endpoint)
|
||||||
|
{
|
||||||
|
string withoutChatsAndMessages = endpoint
|
||||||
|
.Replace("chats", "", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("messages", "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
string trimmed = withoutChatsAndMessages.Trim(' ', '/', '\\');
|
||||||
|
|
||||||
|
if (long.TryParse(trimmed, out long userId))
|
||||||
|
return (true, userId);
|
||||||
|
|
||||||
|
return (false, default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new async Task<Dictionary<string, long>?> GetListUsers(string endpoint)
|
||||||
|
{
|
||||||
|
if (!HasSignedRequestAuth())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, long> users = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
Log.Debug($"Calling GetListUsers - {endpoint}");
|
||||||
|
|
||||||
|
const int limit = 50;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
Dictionary<string, string> getParams = new()
|
||||||
|
{
|
||||||
|
["format"] = "infinite",
|
||||||
|
["limit"] = limit.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
getParams["offset"] = offset.ToString();
|
||||||
|
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
break;
|
||||||
|
|
||||||
|
ListsDtos.ListUsersDto? listUsers = DeserializeJson<ListsDtos.ListUsersDto>(body, s_mJsonSerializerSettings);
|
||||||
|
|
||||||
|
if (listUsers?.List is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (ListsDtos.UsersListDto item in listUsers.List)
|
||||||
|
{
|
||||||
|
if (item.Id is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
users.TryAdd(item.Username, item.Id.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listUsers.HasMore is false)
|
||||||
|
break;
|
||||||
|
|
||||||
|
offset = listUsers.NextOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserEntities.UserInfo?> GetDetailedUserInfoAsync(string endpoint)
|
||||||
|
{
|
||||||
|
Log.Debug($"Calling GetDetailedUserInfo: {endpoint}");
|
||||||
|
|
||||||
|
if (!HasSignedRequestAuth())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UserEntities.UserInfo userInfo = new();
|
||||||
|
Dictionary<string, string> 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<UserDtos.UserDto>(body, s_mJsonSerializerSettings);
|
||||||
|
userInfo = FromDto(userDto);
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, long>> GetUsersWithProgressAsync(string typeDisplay, string endpoint, string? typeParam, bool offsetByCount)
|
||||||
|
{
|
||||||
|
Dictionary<string, long> usersOfType = await _eventHandler.WithStatusAsync(
|
||||||
|
statusMessage: $"Getting {typeDisplay} Users",
|
||||||
|
work: FetchAsync
|
||||||
|
);
|
||||||
|
|
||||||
|
return usersOfType;
|
||||||
|
|
||||||
|
async Task<Dictionary<string, long>> FetchAsync(IStatusReporter statusReporter)
|
||||||
|
{
|
||||||
|
Dictionary<string, long> users = [];
|
||||||
|
|
||||||
|
int limit = 50;
|
||||||
|
int offset = 0;
|
||||||
|
bool includeRestricted = true;
|
||||||
|
|
||||||
|
Dictionary<string, string> getParams = new()
|
||||||
|
{
|
||||||
|
["format"] = "infinite",
|
||||||
|
["limit"] = limit.ToString(),
|
||||||
|
["offset"] = offset.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(typeParam))
|
||||||
|
getParams["type"] = typeParam;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Debug("Calling GetUsersWithProgress");
|
||||||
|
|
||||||
|
HttpClient client = GetHttpClient();
|
||||||
|
|
||||||
|
bool isLastLoop = false;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, client);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
break;
|
||||||
|
|
||||||
|
SubscriptionsDtos.SubscriptionsDto? subscriptions = DeserializeJson<SubscriptionsDtos.SubscriptionsDto>(body, s_mJsonSerializerSettings);
|
||||||
|
|
||||||
|
if (subscriptions?.List is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (SubscriptionsDtos.ListItemDto item in subscriptions.List)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(item?.Username))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (users.ContainsKey(item.Username))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool isRestricted = item.IsRestricted ?? false;
|
||||||
|
bool isRestrictedButAllowed = isRestricted && includeRestricted;
|
||||||
|
|
||||||
|
if (!isRestricted || isRestrictedButAllowed)
|
||||||
|
users.Add(item.Username, item.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
statusReporter.ReportStatus($"[blue]Getting {typeDisplay} Users\n[/] [blue]Found {users.Count}[/]");
|
||||||
|
|
||||||
|
if (isLastLoop)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!subscriptions.HasMore || subscriptions.List.Count == 0)
|
||||||
|
isLastLoop = true;
|
||||||
|
|
||||||
|
offset += offsetByCount
|
||||||
|
? subscriptions.List.Count
|
||||||
|
: limit;
|
||||||
|
|
||||||
|
getParams["offset"] = offset.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HashSet<long>> GetUsersWithUnreadMessagesAsync()
|
||||||
|
{
|
||||||
|
MessageDtos.ChatsDto unreadChats = await GetChatsAsync("/chats", onlyUnread: true);
|
||||||
|
HashSet<long> userWithUnread = [];
|
||||||
|
|
||||||
|
foreach (MessageDtos.ChatItemDto chatItem in unreadChats.List)
|
||||||
|
{
|
||||||
|
if (chatItem?.WithUser?.Id is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (chatItem.UnreadMessagesCount <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
userWithUnread.Add(chatItem.WithUser.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userWithUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkAsUnreadAsync(string endpoint)
|
||||||
|
{
|
||||||
|
Log.Debug($"Calling MarkAsUnread - {endpoint}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = new { success = false };
|
||||||
|
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests([], endpoint, GetHttpClient(), HttpMethod.Delete);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(body))
|
||||||
|
result = JsonConvert.DeserializeAnonymousType(body, result);
|
||||||
|
|
||||||
|
if (result?.success != true)
|
||||||
|
_eventHandler.OnMessage($"Failed to mark chat as unread! Endpoint: {endpoint}", "yellow");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SortBlockedAsync(string endpoint, string order = "recent", string direction = "desc")
|
||||||
|
{
|
||||||
|
Log.Debug($"Calling SortBlocked - {endpoint}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reqBody = new { order, direction };
|
||||||
|
var result = new { success = false, canAddFriends = false };
|
||||||
|
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests([], endpoint, GetHttpClient(), HttpMethod.Post, reqBody);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(body))
|
||||||
|
result = JsonConvert.DeserializeAnonymousType(body, result);
|
||||||
|
|
||||||
|
if (result?.success != true)
|
||||||
|
_eventHandler.OnMessage($"Failed to sort blocked (order: {order}, direction; {direction})! Endpoint: {endpoint}", "yellow");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, long>?> GetAllSubscriptions(string endpoint, bool includeRestricted, string type)
|
||||||
|
{
|
||||||
|
if (!HasSignedRequestAuth())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, long> users = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
Log.Debug("Calling GetAllSubscrptions");
|
||||||
|
|
||||||
|
const int limit = 50;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
Dictionary<string, string> getParams = new()
|
||||||
|
{
|
||||||
|
["type"] = type,
|
||||||
|
["format"] = "infinite",
|
||||||
|
["limit"] = limit.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
getParams["offset"] = offset.ToString();
|
||||||
|
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, new HttpClient());
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
break;
|
||||||
|
|
||||||
|
SubscriptionsDtos.SubscriptionsDto? subscriptionsDto = DeserializeJson<SubscriptionsDtos.SubscriptionsDto>(body, s_mJsonSerializerSettings);
|
||||||
|
|
||||||
|
if (subscriptionsDto?.List is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (SubscriptionsDtos.ListItemDto item in subscriptionsDto.List)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(item.Username))
|
||||||
|
{
|
||||||
|
Log.Warning("Found '{Type:l}' subscription user with empty username (id: {Id})", type, item.Id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.ContainsKey(item.Username))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool isRestricted = item.IsRestricted ?? false;
|
||||||
|
bool isRestrictedButAllowed = isRestricted && includeRestricted;
|
||||||
|
|
||||||
|
if (!isRestricted || isRestrictedButAllowed)
|
||||||
|
users.Add(item.Username, item.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionsDto.HasMore is false)
|
||||||
|
break;
|
||||||
|
|
||||||
|
offset += limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<MessageDtos.ChatsDto> GetChatsAsync(string endpoint, bool onlyUnread)
|
||||||
|
{
|
||||||
|
Log.Debug($"Calling GetChats - {endpoint}");
|
||||||
|
|
||||||
|
MessageDtos.ChatsDto allChats = new();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const int limit = 60;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
Dictionary<string, string> getParams = new()
|
||||||
|
{
|
||||||
|
["order"] = "recent",
|
||||||
|
["skip_users"] = "all",
|
||||||
|
["filter"] = "unread",
|
||||||
|
["limit"] = $"{limit}",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (onlyUnread is false)
|
||||||
|
getParams.Remove("filter");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
getParams["offset"] = $"{offset}";
|
||||||
|
|
||||||
|
string? body = await BuildHeaderAndExecuteRequests(getParams, endpoint, GetHttpClient());
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
break;
|
||||||
|
|
||||||
|
MessageDtos.ChatsDto? chats = DeserializeJson<MessageDtos.ChatsDto>(body, s_mJsonSerializerSettings);
|
||||||
|
|
||||||
|
if (chats?.List is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
allChats.List.AddRange(chats.List);
|
||||||
|
|
||||||
|
if (chats.HasMore is false)
|
||||||
|
break;
|
||||||
|
|
||||||
|
offset = chats.NextOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExceptionLoggerHelper.LogException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allChats;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
145
Cajetan.OF-DL/Services/CajetanDbService.cs
Normal file
145
Cajetan.OF-DL/Services/CajetanDbService.cs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (SqliteCommand cmdNonNude = new("CREATE TABLE IF NOT EXISTS user_non_nude (user_id INTEGER NOT NULL, name VARCHAR NOT NULL, PRIMARY KEY(user_id));", connection))
|
||||||
|
{
|
||||||
|
await cmdNonNude.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, long>> GetUsersAsync()
|
||||||
|
{
|
||||||
|
await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using SqliteCommand cmd = new("SELECT user_id, username FROM users", connection);
|
||||||
|
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||||
|
|
||||||
|
Dictionary<string, long> 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");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
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", 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateNonNudeCollectionAsync(Dictionary<string, long> usersInNonNudeLists)
|
||||||
|
{
|
||||||
|
if (usersInNonNudeLists.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await using SqliteConnection connection = new($"Data Source={Directory.GetCurrentDirectory()}/users.db");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
Log.Debug("Database data source: " + connection.DataSource);
|
||||||
|
|
||||||
|
foreach ((string username, long userId) in usersInNonNudeLists)
|
||||||
|
{
|
||||||
|
using SqliteCommand cmdInfo = new(
|
||||||
|
"INSERT OR REPLACE INTO user_non_nude (user_id, name) " +
|
||||||
|
"VALUES (@userId, @name);",
|
||||||
|
connection
|
||||||
|
);
|
||||||
|
|
||||||
|
cmdInfo.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmdInfo.Parameters.AddWithValue("@name", username);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await cmdInfo.ExecuteNonQueryAsync();
|
||||||
|
Log.Debug("Updating Non-Nude collection with: {Username:l}", username);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to update Non-Nude collection with: {Username:l}", username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public class CajetanDownloadOrchestrationService(ICajetanApiService apiService, IConfigService configService, ICajetanDownloadService downloadService, ICajetanDbService dbService)
|
||||||
|
: DownloadOrchestrationService(apiService, configService, downloadService, dbService), ICajetanDownloadOrchestrationService
|
||||||
|
{
|
||||||
|
}
|
||||||
6
Cajetan.OF-DL/Services/CajetanDownloadService.cs
Normal file
6
Cajetan.OF-DL/Services/CajetanDownloadService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public class CajetanDownloadService(IAuthService authService, IConfigService configService, ICajetanDbService dbService, IFileNameService fileNameService, ICajetanApiService apiService)
|
||||||
|
: DownloadService(authService, configService, dbService, fileNameService, apiService), ICajetanDownloadService
|
||||||
|
{
|
||||||
|
}
|
||||||
54
Cajetan.OF-DL/Services/CajetanLoggingService.cs
Normal file
54
Cajetan.OF-DL/Services/CajetanLoggingService.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public class CajetanLoggingService : ILoggingService
|
||||||
|
{
|
||||||
|
public CajetanLoggingService()
|
||||||
|
{
|
||||||
|
LevelSwitch = new LoggingLevelSwitch();
|
||||||
|
InitializeLoggerWithSeq();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the level switch that controls runtime logging verbosity.
|
||||||
|
/// </summary>
|
||||||
|
public LoggingLevelSwitch LevelSwitch { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the minimum logging level at runtime.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newLevel">The new minimum log level.</param>
|
||||||
|
public void UpdateLoggingLevel(LoggingLevel newLevel)
|
||||||
|
{
|
||||||
|
LevelSwitch.MinimumLevel = (LogEventLevel)newLevel;
|
||||||
|
Log.Debug("Logging level updated to: {LoggingLevel}", newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current minimum logging level.
|
||||||
|
/// </summary>
|
||||||
|
public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel;
|
||||||
|
|
||||||
|
private void InitializeLoggerWithSeq()
|
||||||
|
{
|
||||||
|
LevelSwitch.MinimumLevel = LogEventLevel.Warning;
|
||||||
|
|
||||||
|
LoggerConfiguration loggerConfig = new LoggerConfiguration()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Application", "OF_DL")
|
||||||
|
.Enrich.WithProperty("StartTime", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} ")
|
||||||
|
.Enrich.WithProperty("MachineName", Environment.MachineName)
|
||||||
|
.MinimumLevel.Verbose()
|
||||||
|
.WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Error, levelSwitch: LevelSwitch)
|
||||||
|
.WriteTo.Seq("https://seq.cajetan.dk", controlLevelSwitch: LevelSwitch);
|
||||||
|
|
||||||
|
if (System.Diagnostics.Debugger.IsAttached)
|
||||||
|
loggerConfig.WriteTo.Debug(restrictedToMinimumLevel: LogEventLevel.Debug);
|
||||||
|
|
||||||
|
Log.Logger = loggerConfig.CreateLogger();
|
||||||
|
|
||||||
|
Log.Debug("Logging service initialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Cajetan.OF-DL/Services/ICajetanApiService.cs
Normal file
12
Cajetan.OF-DL/Services/ICajetanApiService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public interface ICajetanApiService : IApiService
|
||||||
|
{
|
||||||
|
new Task<Dictionary<string, long>?> GetListUsers(string endpoint);
|
||||||
|
|
||||||
|
Task<UserEntities.UserInfo?> GetDetailedUserInfoAsync(string endpoint);
|
||||||
|
Task<Dictionary<string, long>> GetUsersWithProgressAsync(string typeDisplay, string endpoint, string? typeParam, bool offsetByCount);
|
||||||
|
Task<HashSet<long>> GetUsersWithUnreadMessagesAsync();
|
||||||
|
Task MarkAsUnreadAsync(string endpoint);
|
||||||
|
Task SortBlockedAsync(string endpoint, string order = "recent", string direction = "desc");
|
||||||
|
}
|
||||||
10
Cajetan.OF-DL/Services/ICajetanDbService.cs
Normal file
10
Cajetan.OF-DL/Services/ICajetanDbService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public interface ICajetanDbService : IDbService
|
||||||
|
{
|
||||||
|
Task InitializeUserInfoTablesAsync();
|
||||||
|
|
||||||
|
Task<Dictionary<string, long>> GetUsersAsync();
|
||||||
|
Task UpdateUserInfoAsync(UserEntities.UserInfo? userInfo);
|
||||||
|
Task UpdateNonNudeCollectionAsync(Dictionary<string, long> usersInNonNudeLists);
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public interface ICajetanDownloadOrchestrationService : IDownloadOrchestrationService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
6
Cajetan.OF-DL/Services/ICajetanDownloadService.cs
Normal file
6
Cajetan.OF-DL/Services/ICajetanDownloadService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace OF_DL.Services;
|
||||||
|
|
||||||
|
public interface ICajetanDownloadService : IDownloadService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
454
Cajetan.OF-DL/Worker.cs
Normal file
454
Cajetan.OF-DL/Worker.cs
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace OF_DL;
|
||||||
|
|
||||||
|
internal class Worker(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
private readonly IConfigService _configService = serviceProvider.GetRequiredService<IConfigService>();
|
||||||
|
private readonly IAuthService _authService = serviceProvider.GetRequiredService<IAuthService>();
|
||||||
|
private readonly IStartupService _startupService = serviceProvider.GetRequiredService<IStartupService>();
|
||||||
|
private readonly IDownloadOrchestrationService _orchestrationService = serviceProvider.GetRequiredService<IDownloadOrchestrationService>();
|
||||||
|
private readonly ICajetanDbService _dbService = serviceProvider.GetRequiredService<ICajetanDbService>();
|
||||||
|
private readonly ICajetanApiService _apiService = serviceProvider.GetRequiredService<ICajetanApiService>();
|
||||||
|
private readonly ExitHelper _exitHelper = serviceProvider.GetRequiredService<ExitHelper>();
|
||||||
|
|
||||||
|
private readonly CajetanConfig _cajetanConfig = serviceProvider.GetRequiredService<CajetanConfig>();
|
||||||
|
private bool _clientIdBlobMissing = true;
|
||||||
|
private bool _devicePrivateKeyMissing = true;
|
||||||
|
|
||||||
|
public async Task RunAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await InitializeAsync();
|
||||||
|
|
||||||
|
Task tMode = _cajetanConfig.Mode switch
|
||||||
|
{
|
||||||
|
EMode.DownloadCreatorContent => DownloadCreatorContentAsync(),
|
||||||
|
EMode.OutputBlockedUsers => OutputBlockedUsersAsync(),
|
||||||
|
EMode.UpdateAllUserInfo => UpdateUserInfoAsync(),
|
||||||
|
_ => Task.CompletedTask
|
||||||
|
};
|
||||||
|
|
||||||
|
await tMode;
|
||||||
|
}
|
||||||
|
catch (ExitCodeException ex)
|
||||||
|
{
|
||||||
|
_exitHelper.ExitWithCode(ex.ExitCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Fatal(ex, "Unhandled Exception! {ExceptionMessage:l}", ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_exitHelper.ExitWithCode(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
StartupResult startupResult = await _startupService.ValidateEnvironmentAsync();
|
||||||
|
|
||||||
|
if (startupResult.IsWindowsVersionValid is false)
|
||||||
|
throw new ExitCodeException(1);
|
||||||
|
|
||||||
|
if (startupResult.FfmpegFound is false)
|
||||||
|
throw new ExitCodeException(4);
|
||||||
|
|
||||||
|
if (startupResult.ClientIdBlobMissing || startupResult.DevicePrivateKeyMissing)
|
||||||
|
AnsiConsole.Markup("[yellow]device_client_id_blob and/or device_private_key missing, https://ofdl.tools/ or https://cdrm-project.com/ will be used instead for DRM protected videos\n[/]");
|
||||||
|
|
||||||
|
_clientIdBlobMissing = startupResult.ClientIdBlobMissing;
|
||||||
|
_devicePrivateKeyMissing = startupResult.DevicePrivateKeyMissing;
|
||||||
|
|
||||||
|
if (IsRulesJsonValid() is false)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[red]Press any key to exit.[/]");
|
||||||
|
Console.ReadKey();
|
||||||
|
throw new ExitCodeException(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await IsAuthorizedAsync() is false)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("\n[red]Press any key to exit.[/]");
|
||||||
|
Console.ReadKey();
|
||||||
|
throw new ExitCodeException(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRulesJsonValid()
|
||||||
|
{
|
||||||
|
if (startupResult.RulesJsonExists is false)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (startupResult.RulesJsonValid)
|
||||||
|
{
|
||||||
|
AnsiConsole.Markup("[green]rules.json located successfully!\n[/]");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("\n[red]rules.json is not valid, check your JSON syntax![/]\n");
|
||||||
|
Log.Error("processing of rules.json failed: {Error:l}", startupResult.RulesJsonError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<bool> IsAuthorizedAsync()
|
||||||
|
{
|
||||||
|
if (await _authService.LoadFromFileAsync() is false)
|
||||||
|
{
|
||||||
|
if (File.Exists("auth.json"))
|
||||||
|
{
|
||||||
|
Log.Error("Auth file was found but could not be deserialized!");
|
||||||
|
AnsiConsole.MarkupLine("\n[red]auth.json could not be deserialized.[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error("Auth file was not found!");
|
||||||
|
AnsiConsole.MarkupLine("\n[red]auth.json is missing.[/]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Markup("[green]auth.json located successfully!\n[/]");
|
||||||
|
|
||||||
|
// Validate cookie string
|
||||||
|
_authService.ValidateCookieString();
|
||||||
|
|
||||||
|
UserEntities.User? user = await _authService.ValidateAuthAsync();
|
||||||
|
if (user is null || (user.Name is null && user.Username is null))
|
||||||
|
{
|
||||||
|
Log.Error("Auth failed");
|
||||||
|
_authService.CurrentAuth = null;
|
||||||
|
AnsiConsole.MarkupLine("\n[red]Auth failed. Please try again or use other authentication methods detailed here:[/]\n");
|
||||||
|
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth[/]\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string displayName = $"{user.Name} {user.Username}".Trim();
|
||||||
|
AnsiConsole.MarkupLine($"[green]Logged In successfully as {displayName}\n[/]");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadCreatorContentAsync()
|
||||||
|
{
|
||||||
|
DateTime startTime = DateTime.Now;
|
||||||
|
|
||||||
|
UserListResult allUsersAndLists = await GetAvailableUsersAsync();
|
||||||
|
Dictionary<string, long> usersToDownload = allUsersAndLists.Users;
|
||||||
|
|
||||||
|
if (_cajetanConfig.NonInteractiveSpecificLists is not null && _cajetanConfig.NonInteractiveSpecificLists.Length > 0)
|
||||||
|
usersToDownload = await GetUsersFromSpecificListsAsync(allUsersAndLists, _cajetanConfig.NonInteractiveSpecificLists);
|
||||||
|
|
||||||
|
else if (_cajetanConfig.NonInteractiveSpecificUsers is not null && _cajetanConfig.NonInteractiveSpecificUsers.Length > 0)
|
||||||
|
usersToDownload = GetUsersFromSpecificUsernames(allUsersAndLists, [.. _cajetanConfig.NonInteractiveSpecificUsers]);
|
||||||
|
|
||||||
|
int userNum = 0;
|
||||||
|
int userCount = usersToDownload.Count;
|
||||||
|
CajetanDownloadEventHandler eventHandler = new();
|
||||||
|
CreatorDownloadResult totalResults = new();
|
||||||
|
|
||||||
|
LoggerWithConfigContext(_configService.CurrentConfig, _cajetanConfig)
|
||||||
|
.Information("Scraping Data for {UserCount} user(s)", usersToDownload.Count);
|
||||||
|
|
||||||
|
eventHandler.OnMessage(
|
||||||
|
$"\nScraping Data for {usersToDownload.Count} user(s)\n" +
|
||||||
|
$"{"======================================================================================================"}\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ((string username, long userId) in usersToDownload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DateTime userStartTime = DateTime.Now;
|
||||||
|
|
||||||
|
Log.Information("Scraping Data for '{Username:l}' ({UserNum} of {UserCount})", username, ++userNum, userCount);
|
||||||
|
eventHandler.OnMessage($"\nScraping Data for {Markup.Escape(username)} ({userNum} of {userCount})\n");
|
||||||
|
|
||||||
|
string path = _orchestrationService.ResolveDownloadPath(username);
|
||||||
|
Log.Debug($"Download path: {path}");
|
||||||
|
|
||||||
|
CreatorDownloadResult results = await _orchestrationService.DownloadCreatorContentAsync(
|
||||||
|
username: username,
|
||||||
|
userId: userId,
|
||||||
|
path: path,
|
||||||
|
users: usersToDownload,
|
||||||
|
clientIdBlobMissing: _clientIdBlobMissing,
|
||||||
|
devicePrivateKeyMissing: _devicePrivateKeyMissing,
|
||||||
|
eventHandler: eventHandler
|
||||||
|
);
|
||||||
|
CreatorDownloadResult newResults = results.NewDownloads ?? results;
|
||||||
|
|
||||||
|
totalResults.Add(newResults);
|
||||||
|
|
||||||
|
DateTime userEndTime = DateTime.Now;
|
||||||
|
TimeSpan userTotalTime = userEndTime - userStartTime;
|
||||||
|
|
||||||
|
Log.ForContext("Posts", newResults.PostCount)
|
||||||
|
.ForContext("PaidPosts", newResults.PaidPostCount)
|
||||||
|
.ForContext("AllPosts", newResults.PostCount + newResults.PaidPostCount)
|
||||||
|
.ForContext("Archived", newResults.ArchivedCount)
|
||||||
|
.ForContext("Streams", newResults.StreamsCount)
|
||||||
|
.ForContext("Stories", newResults.StoriesCount)
|
||||||
|
.ForContext("Highlights", newResults.HighlightsCount)
|
||||||
|
.ForContext("Messages", newResults.MessagesCount)
|
||||||
|
.ForContext("PaidMessages", newResults.PaidMessagesCount)
|
||||||
|
.ForContext("AllMessages", newResults.MessagesCount + newResults.PaidMessagesCount)
|
||||||
|
.ForContext("Username", username)
|
||||||
|
.ForContext("TotalMinutes", userTotalTime.TotalMinutes)
|
||||||
|
.Information("Scraped Data for '{Username:l}', took {TotalMinutes:0.000} minutes [P: {AllPosts}] [M: {AllMessages}]");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.ForContext("Username", username)
|
||||||
|
.ForContext("UserNum", userNum)
|
||||||
|
.ForContext("UserCount", userCount)
|
||||||
|
.ForContext("ExceptionMessage", ex.Message)
|
||||||
|
.Error(ex, "Scrape for '{Username:l}' ({UserNum} of {UserCount}) failed! {ExceptionMessage:l}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime endTime = DateTime.Now;
|
||||||
|
TimeSpan totalTime = endTime - startTime;
|
||||||
|
|
||||||
|
eventHandler.OnScrapeComplete(totalTime);
|
||||||
|
|
||||||
|
Log.ForContext("Posts", totalResults.PostCount)
|
||||||
|
.ForContext("PaidPosts", totalResults.PaidPostCount)
|
||||||
|
.ForContext("AllPosts", totalResults.PostCount + totalResults.PaidPostCount)
|
||||||
|
.ForContext("Archived", totalResults.ArchivedCount)
|
||||||
|
.ForContext("Streams", totalResults.StreamsCount)
|
||||||
|
.ForContext("Stories", totalResults.StoriesCount)
|
||||||
|
.ForContext("Highlights", totalResults.HighlightsCount)
|
||||||
|
.ForContext("Messages", totalResults.MessagesCount)
|
||||||
|
.ForContext("PaidMessages", totalResults.PaidMessagesCount)
|
||||||
|
.ForContext("AllMessages", totalResults.MessagesCount + totalResults.PaidMessagesCount)
|
||||||
|
.ForContext("TotalMinutes", totalTime.TotalMinutes)
|
||||||
|
.Information("Scrape Completed in {TotalMinutes:0.00} minutes [P: {AllPosts}] [M: {AllMessages}]");
|
||||||
|
|
||||||
|
await Task.Delay(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OutputBlockedUsersAsync()
|
||||||
|
{
|
||||||
|
const string OUTPUT_FILE_BLOCKED = "blocked-users.json";
|
||||||
|
const string OUTPUT_FILE_EXPIRED = "expired-users.json";
|
||||||
|
|
||||||
|
await _apiService.SortBlockedAsync("/lists/blocked/sort");
|
||||||
|
await GetUsersAsync("Blocked", "/users/blocked", OUTPUT_FILE_BLOCKED);
|
||||||
|
|
||||||
|
await GetUsersAsync("Expired", "/subscriptions/subscribes", OUTPUT_FILE_EXPIRED, typeParam: "expired", offsetByCount: false);
|
||||||
|
|
||||||
|
async Task GetUsersAsync(string typeDisplay, string uri, string outputFile, string? typeParam = null, bool offsetByCount = true)
|
||||||
|
{
|
||||||
|
Dictionary<string, long> users = await _apiService.GetUsersWithProgressAsync(typeDisplay, uri, typeParam, offsetByCount);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
if (users is null || users.Count == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.Markup($"[green]No {typeDisplay} Users found.\n[/]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnsiConsole.Markup($"[green]Found {users.Count} {typeDisplay} Users, saving to '{outputFile}'\n[/]");
|
||||||
|
string json = JsonConvert.SerializeObject(users, Formatting.Indented);
|
||||||
|
await File.WriteAllTextAsync(outputFile, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateUserInfoAsync()
|
||||||
|
{
|
||||||
|
await _dbService.CreateUsersDb([]);
|
||||||
|
await _dbService.InitializeUserInfoTablesAsync();
|
||||||
|
|
||||||
|
Dictionary<string, long> users = await _dbService.GetUsersAsync();
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Log.Information("Updating User Info for '{UserCount}' users", users.Count);
|
||||||
|
AnsiConsole.Markup($"[green]Updating User Info for '{users.Count}' users\n[/]");
|
||||||
|
|
||||||
|
await AnsiConsole.Progress()
|
||||||
|
.Columns(new ProgressBarColumn(), new PercentageColumn(), new TaskDescriptionColumn { Alignment = Justify.Left })
|
||||||
|
.StartAsync(RunUpdateAsync);
|
||||||
|
|
||||||
|
async Task RunUpdateAsync(ProgressContext context)
|
||||||
|
{
|
||||||
|
ProgressTask? updateTask = null;
|
||||||
|
|
||||||
|
int maxUsernameLength = users.Keys.Max(s => s.Length);
|
||||||
|
|
||||||
|
foreach ((string username, long userId) in users)
|
||||||
|
{
|
||||||
|
string description = $"Updating '{username}'".PadRight(11 + maxUsernameLength);
|
||||||
|
double prevValue = updateTask?.Value ?? 0;
|
||||||
|
|
||||||
|
updateTask = context.AddTask(description, true, users.Count);
|
||||||
|
updateTask.Value = prevValue;
|
||||||
|
|
||||||
|
using (LogContext.PushProperty("Username", username))
|
||||||
|
using (LogContext.PushProperty("UserId", userId))
|
||||||
|
using (LogContext.PushProperty("UserNum", prevValue + 1))
|
||||||
|
using (LogContext.PushProperty("UserTotal", users.Count))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("[{UserNum:0} of {UserTotal}] Updating User Info for for: {Username:l}");
|
||||||
|
UserEntities.UserInfo? userInfo = await _apiService.GetDetailedUserInfoAsync($"/users/{username}");
|
||||||
|
await _dbService.UpdateUserInfoAsync(userInfo);
|
||||||
|
|
||||||
|
updateTask.Description = $"{description} - COMPLETE";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "[{UserNum:0} of {UserTotal}] Failed to update User Info for: {Username:l}");
|
||||||
|
AnsiConsole.Markup($"[red]Failed to update User Info for '{username}'\n[/]");
|
||||||
|
|
||||||
|
updateTask.Description = $"{description} - FAILED: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
updateTask.Increment(1);
|
||||||
|
updateTask.StopTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, long>> GetUsersFromSpecificListsAsync(UserListResult allUsersAndLists, string[] listNames)
|
||||||
|
{
|
||||||
|
Config currentConfig = _configService.CurrentConfig;
|
||||||
|
Dictionary<string, long> usersFromLists = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (string name in listNames)
|
||||||
|
{
|
||||||
|
if (!allUsersAndLists.Lists.TryGetValue(name, out long listId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Log.Information("Getting Users from list '{ListName:l}' (Include Restricted: {IncludeRestrictedSubscriptions})", name, currentConfig.IncludeRestrictedSubscriptions);
|
||||||
|
AnsiConsole.MarkupLine($"[green]Getting Users from list '{name}' (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})[/]");
|
||||||
|
|
||||||
|
Dictionary<string, long> listUsernames = await _apiService.GetListUsers($"/lists/{listId}/users") ?? [];
|
||||||
|
|
||||||
|
foreach ((string username, long userId) in listUsernames)
|
||||||
|
{
|
||||||
|
if (usersFromLists.ContainsKey(username))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
usersFromLists[username] = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersFromLists;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, long> GetUsersFromSpecificUsernames(UserListResult allUsersAndLists, HashSet<string> usernames)
|
||||||
|
{
|
||||||
|
Dictionary<string, long> filteredUsers = allUsersAndLists.Users
|
||||||
|
.Where(u => usernames.Contains(u.Key))
|
||||||
|
.ToDictionary(u => u.Key, u => u.Value);
|
||||||
|
|
||||||
|
return filteredUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<UserListResult> GetAvailableUsersAsync()
|
||||||
|
{
|
||||||
|
Config currentConfig = _configService.CurrentConfig;
|
||||||
|
UserListResult result = new()
|
||||||
|
{
|
||||||
|
Users = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
};
|
||||||
|
|
||||||
|
await FetchUsersAsync();
|
||||||
|
await FetchListsAsync();
|
||||||
|
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
await _dbService.CreateUsersDb(result.Users);
|
||||||
|
await _dbService.InitializeUserInfoTablesAsync();
|
||||||
|
|
||||||
|
await UpdateNonNudeListAsync();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
async Task FetchUsersAsync()
|
||||||
|
{
|
||||||
|
Log.Information("Getting Active Subscriptions (Include Restricted: {IncludeRestrictedSubscriptions})", currentConfig.IncludeRestrictedSubscriptions);
|
||||||
|
AnsiConsole.MarkupLine($"[green]Getting Active Subscriptions (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})[/]");
|
||||||
|
|
||||||
|
Dictionary<string, long>? activeSubs = await _apiService.GetActiveSubscriptions("/subscriptions/subscribes", currentConfig.IncludeRestrictedSubscriptions);
|
||||||
|
AddToResult(activeSubs);
|
||||||
|
|
||||||
|
if (currentConfig.IncludeExpiredSubscriptions)
|
||||||
|
{
|
||||||
|
Log.Information("Getting Expired Subscriptions (Include Restricted: {IncludeRestrictedSubscriptions})", currentConfig.IncludeRestrictedSubscriptions);
|
||||||
|
AnsiConsole.MarkupLine($"[green]Getting Expired Subscriptions (Include Restricted: {currentConfig.IncludeRestrictedSubscriptions})[/]");
|
||||||
|
|
||||||
|
Dictionary<string, long>? expiredSubs = await _apiService.GetExpiredSubscriptions("/subscriptions/subscribes", currentConfig.IncludeRestrictedSubscriptions);
|
||||||
|
AddToResult(expiredSubs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task FetchListsAsync()
|
||||||
|
{
|
||||||
|
Log.Information("Getting Lists");
|
||||||
|
result.Lists = await _apiService.GetLists("/lists") ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UpdateNonNudeListAsync()
|
||||||
|
{
|
||||||
|
const string LIST_NAME = "NonNude";
|
||||||
|
const long LIST_ID = 1220021758;
|
||||||
|
|
||||||
|
HashSet<string> listNames = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (result.Lists.ContainsKey(LIST_NAME))
|
||||||
|
listNames.Add(LIST_NAME);
|
||||||
|
|
||||||
|
string? nameById = result.Lists.FirstOrDefault(l => l.Value == LIST_ID).Key;
|
||||||
|
if (!string.IsNullOrWhiteSpace(nameById))
|
||||||
|
listNames.Add(nameById);
|
||||||
|
|
||||||
|
Dictionary<string, long> usersInNonNudeLists = await GetUsersFromSpecificListsAsync(result, [.. listNames]);
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine($"[grey]Updating Non-Nude collection with {usersInNonNudeLists.Count} Users[/]");
|
||||||
|
await _dbService.UpdateNonNudeCollectionAsync(usersInNonNudeLists);
|
||||||
|
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddToResult(Dictionary<string, long>? subscriptions)
|
||||||
|
{
|
||||||
|
foreach ((string username, long userId) in subscriptions ?? [])
|
||||||
|
{
|
||||||
|
if (result.Users.TryAdd(username, userId))
|
||||||
|
Log.Debug($"Name: {username} ID: {userId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ILogger LoggerWithConfigContext(Config config, CajetanConfig cajetanConfig)
|
||||||
|
=> Log.Logger
|
||||||
|
.ForContext(nameof(Config.DownloadPath), config.DownloadPath)
|
||||||
|
.ForContext(nameof(Config.DownloadPosts), config.DownloadPosts)
|
||||||
|
.ForContext(nameof(Config.DownloadPaidPosts), config.DownloadPaidPosts)
|
||||||
|
.ForContext(nameof(Config.DownloadMessages), config.DownloadMessages)
|
||||||
|
.ForContext(nameof(Config.DownloadPaidMessages), config.DownloadPaidMessages)
|
||||||
|
.ForContext(nameof(Config.DownloadStories), config.DownloadStories)
|
||||||
|
.ForContext(nameof(Config.DownloadStreams), config.DownloadStreams)
|
||||||
|
.ForContext(nameof(Config.DownloadHighlights), config.DownloadHighlights)
|
||||||
|
.ForContext(nameof(Config.DownloadArchived), config.DownloadArchived)
|
||||||
|
.ForContext(nameof(Config.DownloadAvatarHeaderPhoto), config.DownloadAvatarHeaderPhoto)
|
||||||
|
.ForContext(nameof(Config.DownloadImages), config.DownloadImages)
|
||||||
|
.ForContext(nameof(Config.DownloadVideos), config.DownloadVideos)
|
||||||
|
.ForContext(nameof(Config.DownloadAudios), config.DownloadAudios)
|
||||||
|
.ForContext(nameof(Config.IgnoreOwnMessages), config.IgnoreOwnMessages)
|
||||||
|
.ForContext(nameof(Config.DownloadPostsIncrementally), config.DownloadPostsIncrementally)
|
||||||
|
.ForContext(nameof(Config.BypassContentForCreatorsWhoNoLongerExist), config.BypassContentForCreatorsWhoNoLongerExist)
|
||||||
|
.ForContext(nameof(Config.SkipAds), config.SkipAds)
|
||||||
|
.ForContext(nameof(Config.IncludeExpiredSubscriptions), config.IncludeExpiredSubscriptions)
|
||||||
|
.ForContext(nameof(Config.IncludeRestrictedSubscriptions), config.IncludeRestrictedSubscriptions)
|
||||||
|
.ForContext(nameof(CajetanConfig.NonInteractiveSpecificLists), cajetanConfig.NonInteractiveSpecificLists)
|
||||||
|
.ForContext(nameof(CajetanConfig.NonInteractiveSpecificUsers), cajetanConfig.NonInteractiveSpecificUsers);
|
||||||
|
}
|
||||||
22
OF DL.Core/Helpers/ExitHelper.cs
Normal file
22
OF DL.Core/Helpers/ExitHelper.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using OF_DL.Services;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace OF_DL.Helpers;
|
||||||
|
|
||||||
|
public class ExitHelper(IDownloadEventHandler eventHandler)
|
||||||
|
{
|
||||||
|
private readonly IDownloadEventHandler _eventHandler = eventHandler;
|
||||||
|
|
||||||
|
public void ExitWithCode(int exitCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
_eventHandler?.OnMessage($"Exiting run with Code '{exitCode}'..");
|
||||||
|
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
Task.Delay(3000).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Environment.Exit(exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@ public class CreatorDownloadResult
|
|||||||
public int MessagesCount { get; set; }
|
public int MessagesCount { get; set; }
|
||||||
|
|
||||||
public int PaidMessagesCount { get; set; }
|
public int PaidMessagesCount { get; set; }
|
||||||
|
|
||||||
|
public CreatorDownloadResult? NewDownloads { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserListResult
|
public class UserListResult
|
||||||
|
|||||||
36
OF DL.Core/Models/Entities/Users/UserInfo.cs
Normal file
36
OF DL.Core/Models/Entities/Users/UserInfo.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace OF_DL.Models.Entities.Users;
|
||||||
|
|
||||||
|
public class UserInfo : User
|
||||||
|
{
|
||||||
|
public long? Id { get; set; }
|
||||||
|
|
||||||
|
public string? SubscribePrice { get; set; }
|
||||||
|
public string? CurrentSubscribePrice { get; set; }
|
||||||
|
|
||||||
|
public bool? IsPaywallRequired { get; set; }
|
||||||
|
public bool? IsActive { get; set; }
|
||||||
|
public bool? IsRestricted { get; set; }
|
||||||
|
|
||||||
|
public bool? SubscribedBy { get; set; }
|
||||||
|
public bool? SubscribedByExpire { get; set; }
|
||||||
|
public DateTimeOffset? SubscribedByExpireDate { get; set; }
|
||||||
|
public bool? SubscribedByAutoprolong { get; set; }
|
||||||
|
public bool? IsPendingAutoprolong { get; set; }
|
||||||
|
|
||||||
|
public bool? SubscribedIsExpiredNow { get; set; }
|
||||||
|
|
||||||
|
public bool? SubscribedOn { get; set; }
|
||||||
|
public bool? SubscribedOnExpiredNow { get; set; }
|
||||||
|
public string? SubscribedOnDuration { get; set; }
|
||||||
|
|
||||||
|
public string? About { get; set; }
|
||||||
|
|
||||||
|
public int? PostsCount { get; set; }
|
||||||
|
public int? ArchivedPostsCount { get; set; }
|
||||||
|
public int? PrivateArchivedPostsCount { get; set; }
|
||||||
|
|
||||||
|
public int? PhotosCount { get; set; }
|
||||||
|
public int? VideosCount { get; set; }
|
||||||
|
public int? AudiosCount { get; set; }
|
||||||
|
public int? MediasCount { get; set; }
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
<RootNamespace>OF_DL</RootNamespace>
|
<RootNamespace>OF_DL</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -23,4 +24,8 @@
|
|||||||
<PackageReference Include="xFFmpeg.NET" Version="7.2.0"/>
|
<PackageReference Include="xFFmpeg.NET" Version="7.2.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Cajetan.OF-DL" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
{
|
{
|
||||||
private const int MaxAttempts = 30;
|
private const int MaxAttempts = 30;
|
||||||
private const int DelayBetweenAttempts = 3000;
|
private const int DelayBetweenAttempts = 3000;
|
||||||
private static readonly JsonSerializerSettings s_mJsonSerializerSettings;
|
protected static readonly JsonSerializerSettings s_mJsonSerializerSettings;
|
||||||
private static DateTime? s_cachedDynamicRulesExpiration;
|
private static DateTime? s_cachedDynamicRulesExpiration;
|
||||||
private static DynamicRules? s_cachedDynamicRules;
|
private static DynamicRules? s_cachedDynamicRules;
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasSignedRequestAuth()
|
protected bool HasSignedRequestAuth()
|
||||||
{
|
{
|
||||||
Auth? currentAuth = authService.CurrentAuth;
|
Auth? currentAuth = authService.CurrentAuth;
|
||||||
return currentAuth is { UserId: not null, Cookie: not null, UserAgent: not null, XBc: not null };
|
return currentAuth is { UserId: not null, Cookie: not null, UserAgent: not null, XBc: not null };
|
||||||
@ -2754,12 +2754,12 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<string?> BuildHeaderAndExecuteRequests(Dictionary<string, string> getParams, string endpoint,
|
protected async Task<string?> BuildHeaderAndExecuteRequests(Dictionary<string, string> getParams, string endpoint,
|
||||||
HttpClient client)
|
HttpClient client, HttpMethod? method = null, object? reqBody = null)
|
||||||
{
|
{
|
||||||
Log.Debug("Calling BuildHeaderAndExecuteRequests");
|
Log.Debug("Calling BuildHeaderAndExecuteRequests");
|
||||||
|
|
||||||
HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint);
|
HttpRequestMessage request = await BuildHttpRequestMessage(getParams, endpoint, method, reqBody);
|
||||||
using HttpResponseMessage response = await client.SendAsync(request);
|
using HttpResponseMessage response = await client.SendAsync(request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
string body = await response.Content.ReadAsStringAsync();
|
string body = await response.Content.ReadAsStringAsync();
|
||||||
@ -2770,16 +2770,22 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams,
|
protected Task<HttpRequestMessage> BuildHttpRequestMessage(Dictionary<string, string> getParams,
|
||||||
string endpoint)
|
string endpoint, HttpMethod? method = null, object? reqBody = null)
|
||||||
{
|
{
|
||||||
Log.Debug("Calling BuildHttpRequestMessage");
|
Log.Debug("Calling BuildHttpRequestMessage");
|
||||||
|
|
||||||
string queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
string queryParams = "";
|
||||||
|
|
||||||
|
if (getParams.Count != 0)
|
||||||
|
queryParams = "?" + string.Join("&", getParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||||
|
|
||||||
Dictionary<string, string> headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams);
|
Dictionary<string, string> headers = GetDynamicHeaders($"/api2/v2{endpoint}", queryParams);
|
||||||
|
|
||||||
HttpRequestMessage request = new(HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{queryParams}");
|
HttpRequestMessage request = new(method ?? HttpMethod.Get, $"{Constants.ApiUrl}{endpoint}{queryParams}");
|
||||||
|
|
||||||
|
if ((method == HttpMethod.Post || method == HttpMethod.Put || method == HttpMethod.Patch) && reqBody != null)
|
||||||
|
request.Content = new StringContent(JsonConvert.SerializeObject(reqBody, s_mJsonSerializerSettings), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
Log.Debug($"Full request URL: {Constants.ApiUrl}{endpoint}{queryParams}");
|
Log.Debug($"Full request URL: {Constants.ApiUrl}{endpoint}{queryParams}");
|
||||||
|
|
||||||
@ -2821,7 +2827,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
private static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit);
|
private static bool IsStringOnlyDigits(string input) => input.All(char.IsDigit);
|
||||||
|
|
||||||
|
|
||||||
private HttpClient GetHttpClient()
|
protected HttpClient GetHttpClient()
|
||||||
{
|
{
|
||||||
HttpClient client = new();
|
HttpClient client = new();
|
||||||
if (configService.CurrentConfig.Timeout is > 0)
|
if (configService.CurrentConfig.Timeout is > 0)
|
||||||
@ -2832,7 +2838,7 @@ public class ApiService(IAuthService authService, IConfigService configService,
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? DeserializeJson<T>(string? body, JsonSerializerSettings? settings = null)
|
protected static T? DeserializeJson<T>(string? body, JsonSerializerSettings? settings = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(body))
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -160,6 +160,7 @@ public class DownloadOrchestrationService(
|
|||||||
{
|
{
|
||||||
Config config = configService.CurrentConfig;
|
Config config = configService.CurrentConfig;
|
||||||
CreatorDownloadResult counts = new();
|
CreatorDownloadResult counts = new();
|
||||||
|
CreatorDownloadResult newCounts = new();
|
||||||
|
|
||||||
eventHandler.OnUserStarting(username);
|
eventHandler.OnUserStarting(username);
|
||||||
Log.Debug($"Scraping Data for {username}");
|
Log.Debug($"Scraping Data for {username}");
|
||||||
@ -185,7 +186,8 @@ public class DownloadOrchestrationService(
|
|||||||
posts => posts.PaidPosts.Values.ToList(),
|
posts => posts.PaidPosts.Values.ToList(),
|
||||||
async (posts, reporter) => await downloadService.DownloadPaidPosts(username, userId, path, users,
|
async (posts, reporter) => await downloadService.DownloadPaidPosts(username, userId, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.PaidPostCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DownloadPosts)
|
if (config.DownloadPosts)
|
||||||
@ -202,7 +204,8 @@ public class DownloadOrchestrationService(
|
|||||||
posts => posts.Posts.Values.ToList(),
|
posts => posts.Posts.Values.ToList(),
|
||||||
async (posts, reporter) => await downloadService.DownloadFreePosts(username, userId, path, users,
|
async (posts, reporter) => await downloadService.DownloadFreePosts(username, userId, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, posts, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.PostCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DownloadArchived)
|
if (config.DownloadArchived)
|
||||||
@ -215,7 +218,8 @@ public class DownloadOrchestrationService(
|
|||||||
archived => archived.ArchivedPosts.Values.ToList(),
|
archived => archived.ArchivedPosts.Values.ToList(),
|
||||||
async (archived, reporter) => await downloadService.DownloadArchived(username, userId, path, users,
|
async (archived, reporter) => await downloadService.DownloadArchived(username, userId, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, archived, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, archived, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.ArchivedCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DownloadStreams)
|
if (config.DownloadStreams)
|
||||||
@ -228,7 +232,8 @@ public class DownloadOrchestrationService(
|
|||||||
streams => streams.Streams.Values.ToList(),
|
streams => streams.Streams.Values.ToList(),
|
||||||
async (streams, reporter) => await downloadService.DownloadStreams(username, userId, path, users,
|
async (streams, reporter) => await downloadService.DownloadStreams(username, userId, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, streams, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, streams, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.StreamsCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DownloadStories)
|
if (config.DownloadStories)
|
||||||
@ -252,6 +257,7 @@ public class DownloadOrchestrationService(
|
|||||||
|
|
||||||
eventHandler.OnDownloadComplete("Stories", result);
|
eventHandler.OnDownloadComplete("Stories", result);
|
||||||
counts.StoriesCount = result.TotalCount;
|
counts.StoriesCount = result.TotalCount;
|
||||||
|
newCounts.StoriesCount = result.NewDownloads;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -280,6 +286,7 @@ public class DownloadOrchestrationService(
|
|||||||
|
|
||||||
eventHandler.OnDownloadComplete("Highlights", result);
|
eventHandler.OnDownloadComplete("Highlights", result);
|
||||||
counts.HighlightsCount = result.TotalCount;
|
counts.HighlightsCount = result.TotalCount;
|
||||||
|
newCounts.HighlightsCount = result.NewDownloads;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -297,7 +304,8 @@ public class DownloadOrchestrationService(
|
|||||||
messages => messages.Messages.Values.ToList(),
|
messages => messages.Messages.Values.ToList(),
|
||||||
async (messages, reporter) => await downloadService.DownloadMessages(username, userId, path, users,
|
async (messages, reporter) => await downloadService.DownloadMessages(username, userId, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, messages, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, messages, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.MessagesCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DownloadPaidMessages)
|
if (config.DownloadPaidMessages)
|
||||||
@ -310,10 +318,13 @@ public class DownloadOrchestrationService(
|
|||||||
paidMessages => paidMessages.PaidMessages.Values.ToList(),
|
paidMessages => paidMessages.PaidMessages.Values.ToList(),
|
||||||
async (paidMessages, reporter) => await downloadService.DownloadPaidMessages(username, path, users,
|
async (paidMessages, reporter) => await downloadService.DownloadPaidMessages(username, path, users,
|
||||||
clientIdBlobMissing, devicePrivateKeyMissing, paidMessages, reporter),
|
clientIdBlobMissing, devicePrivateKeyMissing, paidMessages, reporter),
|
||||||
eventHandler);
|
eventHandler,
|
||||||
|
n => newCounts.PaidMessagesCount = n);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventHandler.OnUserComplete(username, counts);
|
eventHandler.OnUserComplete(username, counts);
|
||||||
|
|
||||||
|
counts.NewDownloads = newCounts;
|
||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +623,8 @@ public class DownloadOrchestrationService(
|
|||||||
Func<T, int> getObjectCount,
|
Func<T, int> getObjectCount,
|
||||||
Func<T, List<string>?> getUrls,
|
Func<T, List<string>?> getUrls,
|
||||||
Func<T, IProgressReporter, Task<DownloadResult>> downloadData,
|
Func<T, IProgressReporter, Task<DownloadResult>> downloadData,
|
||||||
IDownloadEventHandler eventHandler)
|
IDownloadEventHandler eventHandler,
|
||||||
|
Action<int>? newPostAssignmentAction)
|
||||||
{
|
{
|
||||||
T data = await eventHandler.WithStatusAsync($"Getting {contentType}",
|
T data = await eventHandler.WithStatusAsync($"Getting {contentType}",
|
||||||
async statusReporter => await fetchData(statusReporter));
|
async statusReporter => await fetchData(statusReporter));
|
||||||
@ -643,6 +655,8 @@ public class DownloadOrchestrationService(
|
|||||||
Log.Debug(
|
Log.Debug(
|
||||||
$"{contentType} Media Already Downloaded: {result.ExistingDownloads} New {contentType} Media Downloaded: {result.NewDownloads}");
|
$"{contentType} Media Already Downloaded: {result.ExistingDownloads} New {contentType} Media Downloaded: {result.NewDownloads}");
|
||||||
|
|
||||||
|
newPostAssignmentAction?.Invoke(result.NewDownloads);
|
||||||
|
|
||||||
return result.TotalCount;
|
return result.TotalCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>OF_DL</RootNamespace>
|
<RootNamespace>OF_DL</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ApplicationIcon>Icon\download.ico</ApplicationIcon>
|
<ApplicationIcon>Icon\download.ico</ApplicationIcon>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -42,16 +42,23 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="auth.json">
|
<None Update="auth.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
</None>
|
</None>
|
||||||
<None Update="config.conf">
|
<None Update="config.conf">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
</None>
|
</None>
|
||||||
<None Update="rules.json">
|
<None Update="rules.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
</None>
|
</None>
|
||||||
<None Update="chromium-scripts/stealth.min.js">
|
<None Update="chromium-scripts/stealth.min.js">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Cajetan.OF-DL" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ using OF_DL.Models.Entities.Users;
|
|||||||
using OF_DL.Services;
|
using OF_DL.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
|
using OF_DL.Helpers;
|
||||||
|
|
||||||
namespace OF_DL;
|
namespace OF_DL;
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
|
AnsiConsole.MarkupLine("[red]Press any key to exit.[/]");
|
||||||
Log.Error("auth invalid after attempt to get auth from browser");
|
Log.Error("auth invalid after attempt to get auth from browser");
|
||||||
|
|
||||||
Environment.Exit(2);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await authService.SaveToFileAsync();
|
await authService.SaveToFileAsync();
|
||||||
@ -83,11 +84,12 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
ServiceCollection services = new();
|
ServiceCollection services = new();
|
||||||
services.AddSingleton<ILoggingService, LoggingService>();
|
services.AddSingleton<ILoggingService, LoggingService>();
|
||||||
services.AddSingleton<IConfigService, ConfigService>();
|
services.AddSingleton<IConfigService, ConfigService>();
|
||||||
|
services.AddSingleton(new ExitHelper(new SpectreDownloadEventHandler()));
|
||||||
ServiceProvider tempServiceProvider = services.BuildServiceProvider();
|
ServiceProvider tempServiceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
ILoggingService loggingService = tempServiceProvider.GetRequiredService<ILoggingService>();
|
ILoggingService loggingService = tempServiceProvider.GetRequiredService<ILoggingService>();
|
||||||
IConfigService configService = tempServiceProvider.GetRequiredService<IConfigService>();
|
IConfigService configService = tempServiceProvider.GetRequiredService<IConfigService>();
|
||||||
|
ExitHelper exitHelper = tempServiceProvider.GetRequiredService<ExitHelper>();
|
||||||
|
|
||||||
if (!await configService.LoadConfigurationAsync(args))
|
if (!await configService.LoadConfigurationAsync(args))
|
||||||
{
|
{
|
||||||
@ -98,7 +100,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(3);
|
exitHelper.ExitWithCode(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
AnsiConsole.Markup("[green]config.conf located successfully!\n[/]");
|
AnsiConsole.Markup("[green]config.conf located successfully!\n[/]");
|
||||||
@ -107,6 +109,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
services = [];
|
services = [];
|
||||||
services.AddSingleton(loggingService);
|
services.AddSingleton(loggingService);
|
||||||
services.AddSingleton(configService);
|
services.AddSingleton(configService);
|
||||||
|
services.AddSingleton(exitHelper);
|
||||||
services.AddSingleton<IAuthService, AuthService>();
|
services.AddSingleton<IAuthService, AuthService>();
|
||||||
services.AddSingleton<IApiService, ApiService>();
|
services.AddSingleton<IApiService, ApiService>();
|
||||||
services.AddSingleton<IDbService, DbService>();
|
services.AddSingleton<IDbService, DbService>();
|
||||||
@ -150,7 +153,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(1);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!startupResult.FfmpegFound)
|
if (!startupResult.FfmpegFound)
|
||||||
@ -167,7 +170,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
"[red]Cannot locate FFmpeg; please modify config.conf with the correct path.[/]");
|
"[red]Cannot locate FFmpeg; please modify config.conf with the correct path.[/]");
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(4);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!startupResult.FfprobeFound)
|
if (!startupResult.FfprobeFound)
|
||||||
@ -210,13 +213,13 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Log.Error("Auth failed");
|
Log.Error("Auth failed");
|
||||||
authService.CurrentAuth = null;
|
authService.CurrentAuth = null;
|
||||||
|
|
||||||
if (!configService.CurrentConfig.DisableBrowserAuth)
|
//if (!configService.CurrentConfig.DisableBrowserAuth)
|
||||||
{
|
//{
|
||||||
if (File.Exists("auth.json"))
|
// if (File.Exists("auth.json"))
|
||||||
{
|
// {
|
||||||
File.Delete("auth.json");
|
// File.Delete("auth.json");
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
if (!configService.CurrentConfig.NonInteractiveMode &&
|
if (!configService.CurrentConfig.NonInteractiveMode &&
|
||||||
!configService.CurrentConfig.DisableBrowserAuth)
|
!configService.CurrentConfig.DisableBrowserAuth)
|
||||||
@ -235,7 +238,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(2);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +267,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(5);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -748,11 +751,11 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
else if (File.Exists("auth.json"))
|
else if (File.Exists("auth.json"))
|
||||||
{
|
{
|
||||||
Log.Information("Auth file found but could not be deserialized");
|
Log.Information("Auth file found but could not be deserialized");
|
||||||
if (!configService.CurrentConfig.DisableBrowserAuth)
|
//if (!configService.CurrentConfig.DisableBrowserAuth)
|
||||||
{
|
//{
|
||||||
Log.Debug("Deleting auth.json");
|
// Log.Debug("Deleting auth.json");
|
||||||
File.Delete("auth.json");
|
// File.Delete("auth.json");
|
||||||
}
|
//}
|
||||||
|
|
||||||
if (configService.CurrentConfig.NonInteractiveMode)
|
if (configService.CurrentConfig.NonInteractiveMode)
|
||||||
{
|
{
|
||||||
@ -761,7 +764,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
AnsiConsole.MarkupLine(
|
AnsiConsole.MarkupLine(
|
||||||
"[red]You may also want to try using the browser extension which is documented here:[/]\n");
|
"[red]You may also want to try using the browser extension which is documented here:[/]\n");
|
||||||
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
|
AnsiConsole.MarkupLine("[link]https://docs.ofdl.tools/config/auth/#legacy-methods[/]");
|
||||||
Environment.Exit(2);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configService.CurrentConfig.DisableBrowserAuth)
|
if (!configService.CurrentConfig.DisableBrowserAuth)
|
||||||
@ -790,7 +793,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowAuthMissingError(bool nonInteractiveMode)
|
private void ShowAuthMissingError(bool nonInteractiveMode)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine(
|
AnsiConsole.MarkupLine(
|
||||||
"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n");
|
"\n[red]auth.json is missing. The file can be generated automatically when OF-DL is run in the standard, interactive mode.[/]\n");
|
||||||
@ -804,7 +807,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(2);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DisplayVersionResult(VersionCheckResult result)
|
private static void DisplayVersionResult(VersionCheckResult result)
|
||||||
@ -897,7 +900,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DisplayRulesJsonResult(StartupResult result, IConfigService configService)
|
private void DisplayRulesJsonResult(StartupResult result, IConfigService configService)
|
||||||
{
|
{
|
||||||
if (result.RulesJsonExists)
|
if (result.RulesJsonExists)
|
||||||
{
|
{
|
||||||
@ -917,7 +920,7 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(2);
|
serviceProvider.GetRequiredService<ExitHelper>().ExitWithCode(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
Publish_OF-DL.bat
Normal file
39
Publish_OF-DL.bat
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
ECHO.
|
||||||
|
|
||||||
|
ECHO ==============================
|
||||||
|
ECHO == Cleaning Output ===========
|
||||||
|
ECHO ==============================
|
||||||
|
dotnet clean ".\Cajetan.OF-DL\Cajetan.OF-DL.csproj" -v minimal
|
||||||
|
DEL /Q /F ".\Cajetan.OF-DL_Publish"
|
||||||
|
|
||||||
|
ECHO.
|
||||||
|
|
||||||
|
ECHO ==============================
|
||||||
|
ECHO == Publishing Cajetan OF-DL ==
|
||||||
|
ECHO ==============================
|
||||||
|
dotnet publish ".\Cajetan.OF-DL\Cajetan.OF-DL.csproj" -o ".\Cajetan.OF-DL_Publish" -c Debug
|
||||||
|
|
||||||
|
ECHO.
|
||||||
|
|
||||||
|
ECHO ==============================
|
||||||
|
ECHO == Renaming to 'OF DL.exe' ===
|
||||||
|
ECHO ==============================
|
||||||
|
REN ".\Cajetan.OF-DL_Publish\Cajetan.OF-DL.exe" "OF DL.exe"
|
||||||
|
|
||||||
|
ECHO.
|
||||||
|
ECHO ==============================
|
||||||
|
ECHO == Copy to network drive? ====
|
||||||
|
ECHO ==============================
|
||||||
|
CHOICE /C yn /m "Copy published files to network drive? "
|
||||||
|
|
||||||
|
IF %ERRORLEVEL%==1 (GOTO Copy) ELSE (GOTO Exit)
|
||||||
|
|
||||||
|
:Copy
|
||||||
|
xcopy .\Cajetan.OF-DL_Publish\* p:\_Utils\OF_DL /I /Y /Q /EXCLUDE:.\excludes.txt
|
||||||
|
|
||||||
|
:Exit
|
||||||
|
ECHO.
|
||||||
|
ECHO.
|
||||||
|
PAUSE
|
||||||
2
excludes.txt
Normal file
2
excludes.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
excludes.txt
|
||||||
|
rules.json
|
||||||
Loading…
x
Reference in New Issue
Block a user