Add old Avalonia WIP files

This commit is contained in:
sim0n00ps 2026-01-19 00:43:05 +00:00
parent 43fb74067c
commit 41adc604de
15 changed files with 649 additions and 1 deletions

293
OF DL/AppCommon.cs Normal file
View File

@ -0,0 +1,293 @@
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using OF_DL.Entities;
using OF_DL.Exceptions;
using OF_DL.Helpers;
using Serilog;
namespace OF_DL;
public class AppCommon
{
private readonly Auth _auth;
private readonly Config _config;
private readonly bool _useCdrmProject;
private readonly IAPIHelper _apiHelper;
private readonly IDBHelper _dbHelper;
private readonly IDownloadHelper _downloadHelper;
private Dictionary<string, int> _activeSubscriptions = new();
private Dictionary<string, int> _expiredSubscriptions = new();
public AppCommon()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day)
.WriteTo.Console()
.CreateLogger();
VerifyOperatingSystemCompatibility();
_auth = GetAuth();
_config = GetConfig();
_useCdrmProject = !DetectDrmKeysPresence();
LoadFfmpeg();
_apiHelper = new APIHelper();
_dbHelper = new DBHelper();
_downloadHelper = new DownloadHelper();
}
private static void VerifyOperatingSystemCompatibility()
{
var os = Environment.OSVersion;
if (os.Platform != PlatformID.Win32NT || os.Version.Major >= 10) return;
var platform =
os.Platform switch
{
PlatformID.Win32NT => "Windows",
PlatformID.Unix => "Unix",
PlatformID.MacOSX => "macOS",
_ => "Unknown"
};
Log.Error($"Unsupported operating system: {platform} version {os.VersionString}");
throw new UnsupportedOperatingSystem(platform, os.VersionString);
}
private static Auth GetAuth()
{
if (File.Exists("auth.json"))
{
Log.Debug("auth.json located successfully");
var authJson = JsonConvert.DeserializeObject<Auth>(File.ReadAllText("auth.json"));
if (authJson != null)
{
return authJson;
}
Log.Error("auth.json is invalid");
throw new MalformedFileException("auth.json");
}
Log.Error("auth.json does not exist");
throw new MissingFileException("auth.json");
}
private static Config GetConfig()
{
if (File.Exists("config.json"))
{
Log.Debug("config.json located successfully");
var configJson = JsonConvert.DeserializeObject<Config>(File.ReadAllText("config.json"));
if (configJson != null)
{
return configJson;
}
Log.Error("config.json is invalid");
throw new MalformedFileException("config.json");
}
Log.Error("config.json does not exist");
throw new MissingFileException("config.json");
}
private void LoadFfmpeg()
{
var ffmpegFound = false;
var pathAutoDetected = false;
if (!string.IsNullOrEmpty(_config!.FFmpegPath) && ValidateFilePath(_config.FFmpegPath))
{
// FFmpeg path is set in config.json and is valid
ffmpegFound = true;
}
else if (!string.IsNullOrEmpty(_auth!.FFMPEG_PATH) && ValidateFilePath(_auth.FFMPEG_PATH))
{
// FFmpeg path is set in auth.json and is valid (config.json takes precedence and auth.json is only available for backward compatibility)
ffmpegFound = true;
_config.FFmpegPath = _auth.FFMPEG_PATH;
}
else if (string.IsNullOrEmpty(_config.FFmpegPath))
{
// FFmpeg path is not set in config.json, so we will try to locate it in the PATH or current directory
var ffmpegPath = GetFullPath("ffmpeg");
if (ffmpegPath != null)
{
// FFmpeg is found in the PATH or current directory
ffmpegFound = true;
pathAutoDetected = true;
_config.FFmpegPath = ffmpegPath;
}
else
{
// FFmpeg is not found in the PATH or current directory, so we will try to locate the windows executable
ffmpegPath = GetFullPath("ffmpeg.exe");
if (ffmpegPath != null)
{
// FFmpeg windows executable is found in the PATH or current directory
ffmpegFound = true;
pathAutoDetected = true;
_config.FFmpegPath = ffmpegPath;
}
}
}
if (ffmpegFound)
{
Log.Debug(
pathAutoDetected
? $"FFmpeg located successfully. Path auto-detected: {_config.FFmpegPath}"
: $"FFmpeg located successfully"
);
// Escape backslashes in the path for Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.FFmpegPath!.Contains(@":\") && !_config.FFmpegPath.Contains(@":\\"))
{
_config.FFmpegPath = _config.FFmpegPath.Replace(@"\", @"\\");
}
}
else
{
Log.Error($"Cannot locate FFmpeg with path: {_config.FFmpegPath}");
throw new Exception("Cannot locate FFmpeg");
}
}
private static bool DetectDrmKeysPresence()
{
var clientIdBlobMissing = false;
var devicePrivateKeyMissing = false;
if (!File.Exists("cdm/devices/chrome_1610/device_client_id_blob"))
{
clientIdBlobMissing = true;
}
else
{
Log.Debug($"device_client_id_blob located successfully");
}
if (!File.Exists("cdm/devices/chrome_1610/device_private_key"))
{
devicePrivateKeyMissing = true;
}
else
{
Log.Debug($"device_private_key located successfully");
}
if (!clientIdBlobMissing && !devicePrivateKeyMissing)
{
return true;
}
Log.Information("device_client_id_blob and/or device_private_key missing, https://cdrm-project.com/ will be used instead for DRM protected videos");
return false;
}
private static bool ValidateFilePath(string path)
{
var invalidChars = System.IO.Path.GetInvalidPathChars();
var foundInvalidChars = path.Where(c => invalidChars.Contains(c)).ToArray();
if (foundInvalidChars.Length != 0)
{
Log.Information($"Invalid characters found in path {path}:[/] {string.Join(", ", foundInvalidChars)}");
return false;
}
if (File.Exists(path)) return true;
Log.Information(
Directory.Exists(path)
? $"The provided path {path} improperly points to a directory and not a file."
: $"The provided path {path} does not exist or is not accessible."
);
return false;
}
private static string? GetFullPath(string filename)
{
if (File.Exists(filename))
{
return Path.GetFullPath(filename);
}
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
return pathEnv
.Split(Path.PathSeparator)
.Select(path => Path.Combine(path, filename))
.FirstOrDefault(File.Exists);
}
public async Task<User> GetUser()
{
var user = await _apiHelper.GetUserInfo("/users/me", _auth);
if (user is not { id: not null })
{
Log.Error("Authentication failed. Please check your credentials in auth.json");
throw new AuthenticationFailureException();
}
Log.Debug($"Logged in successfully as {user.name} {user.username}");
return user;
}
private async Task<Dictionary<string, int>> GetActiveSubscriptions()
{
if (_activeSubscriptions.Count > 0)
{
return _activeSubscriptions;
}
_activeSubscriptions = await _apiHelper.GetActiveSubscriptions("/subscriptions/subscribes", _auth, _config.IncludeRestrictedSubscriptions);
return _activeSubscriptions;
}
private async Task<Dictionary<string, int>> GetExpiredSubscriptions()
{
if (_expiredSubscriptions.Count > 0)
{
return _expiredSubscriptions;
}
_expiredSubscriptions = await _apiHelper.GetExpiredSubscriptions("/subscriptions/subscribes", _auth, _config.IncludeRestrictedSubscriptions);
return _expiredSubscriptions;
}
public async Task<Dictionary<string, int>> GetSubscriptions()
{
var subscriptions = new Dictionary<string, int>();
foreach (var (key, value) in await GetActiveSubscriptions())
{
subscriptions.Add(key, value);
}
if (_config.IncludeExpiredSubscriptions)
{
foreach (var (key, value) in await GetExpiredSubscriptions())
{
subscriptions.Add(key, value);
}
}
return subscriptions;
}
public async Task<Dictionary<string, int>> GetLists()
{
return await _apiHelper.GetLists("/lists", _auth);
}
public async Task CreateOrUpdateUsersDatabase()
{
var users = await GetSubscriptions();
await _dbHelper.CreateUsersDB(users);
}
}

26
OF DL/ConsoleApp.cs Normal file
View File

@ -0,0 +1,26 @@
using Serilog;
using Spectre.Console;
namespace OF_DL;
public static class ConsoleApp
{
public static async Task Run()
{
try
{
var common = new AppCommon();
await common.GetUser();
await common.CreateOrUpdateUsersDatabase();
}
catch (Exception ex)
{
Log.Error("Exception caught: {0}\n\nStackTrace: {1}", ex.Message, ex.StackTrace);
if (ex.InnerException != null)
{
Log.Error("Inner Exception: {0}\n\nStackTrace: {1}", ex.InnerException.Message, ex.InnerException.StackTrace);
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace OF_DL.Entities;
public class Subscription(string username, int id)
{
public string Username { get; set; } = username;
public int Id { get; set; } = id;
}

View File

@ -0,0 +1,3 @@
namespace OF_DL.Exceptions;
public class AuthenticationFailureException() : Exception("Authentication failed");

View File

@ -0,0 +1,6 @@
namespace OF_DL.Exceptions;
public class MalformedFileException(string filename) : Exception("File malformed: " + filename)
{
public string Filename { get; } = filename;
}

View File

@ -0,0 +1,6 @@
namespace OF_DL.Exceptions;
public class MissingFileException(string filename) : Exception("File missing: " + filename)
{
public string Filename { get; } = filename;
}

View File

@ -0,0 +1,7 @@
namespace OF_DL.Exceptions;
public class UnsupportedOperatingSystem(string platform, string version) : Exception($"{platform} version {version} is not supported")
{
public string Platform { get; } = platform;
public string Version { get; } = version;
}

11
OF DL/GuiApp.axaml Normal file
View File

@ -0,0 +1,11 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="OF_DL.GuiApp"
RequestedThemeVariant="Light">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
</Application.Styles>
</Application>

24
OF DL/GuiApp.axaml.cs Normal file
View File

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace OF_DL;
public partial class GuiApp : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

88
OF DL/MainWindow.axaml Normal file
View File

@ -0,0 +1,88 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:OF_DL.ViewModels"
xmlns:avaloniaProgressRing="clr-namespace:AvaloniaProgressRing;assembly=AvaloniaProgressRing"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="720"
Width="1280" Height="720"
x:Class="OF_DL.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="OF-DL">
<Grid ColumnDefinitions="*, *">
<Border Grid.Column="0" Grid.ColumnSpan="2" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="White" ZIndex="2" Padding="0" IsVisible="{Binding IsLoading}">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center">
<avaloniaProgressRing:ProgressRing Width="80"
Height="80"
Name="LoadingRing"
IsActive="{Binding IsLoading}"
Foreground="Green"
ZIndex="3"
Margin="10,24,0,32"/>
<Label FontSize="32" Content="{Binding LoadingText}" />
</StackPanel>
</Border>
<StackPanel Grid.Column="0" Orientation="Vertical">
<StackPanel Margin="20">
<TextBlock FontSize="24">Media Sources</TextBlock>
<StackPanel Margin="20">
<CheckBox FontSize="24">Purchased Tab</CheckBox>
</StackPanel>
<StackPanel Margin="20 20 300 20">
<CheckBox FontSize="24">Users</CheckBox>
<ListBox SelectionMode="Multiple,Toggle" Margin="30 0 0 0" ItemsSource="{Binding SubscriptionsList}" Height="300">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Username}" Margin="0, 0, 10, 0" />
<TextBlock Text="("></TextBlock>
<TextBlock Text="{Binding Id}" />
<TextBlock Text=")"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Vertical">
<StackPanel Margin="20">
<TextBlock FontSize="24">Media Types</TextBlock>
<StackPanel Orientation="Vertical" Margin="30 0 0 0">
<CheckBox>Images</CheckBox>
<CheckBox>Videos</CheckBox>
<CheckBox>Audios</CheckBox>
</StackPanel>
</StackPanel>
<StackPanel Margin="20">
<TextBlock FontSize="24">Options</TextBlock>
<StackPanel Margin="20">
<TextBlock>Start Date</TextBlock>
<DatePicker DayFormat="ddd dd"/>
<Button>Clear</Button>
<TextBlock Margin="0 30 0 0">End Date</TextBlock>
<DatePicker DayFormat="ddd dd"/>
<Button>Clear</Button>
<Button Margin="0 30 0 0">Folder options</Button>
<Button Margin="0 30 0 0">Additional settings</Button>
</StackPanel>
</StackPanel>
<StackPanel Margin="20">
<Button Width="250" Height="100" FontSize="50">START</Button>
</StackPanel>
</StackPanel>
</Grid>
</Window>

15
OF DL/MainWindow.axaml.cs Normal file
View File

@ -0,0 +1,15 @@
using Avalonia.Controls;
using OF_DL.ViewModels;
namespace OF_DL;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}

View File

@ -15,16 +15,24 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Akka" Version="1.5.39" /> <PackageReference Include="Akka" Version="1.5.39" />
<PackageReference Include="Avalonia" Version="11.3.11" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" /> <PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" Version="0.10.11-preview20251127001" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="protobuf-net" Version="3.2.46" /> <PackageReference Include="protobuf-net" Version="3.2.46" />
<PackageReference Include="PuppeteerSharp" Version="20.2.5" /> <PackageReference Include="PuppeteerSharp" Version="20.2.5" />
<PackageReference Include="ReactiveUI" Version="22.3.1" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="System.Reactive" Version="6.0.1" /> <PackageReference Include="System.Reactive" Version="6.1.0" />
<PackageReference Include="xFFmpeg.NET" Version="7.2.0" /> <PackageReference Include="xFFmpeg.NET" Version="7.2.0" />
</ItemGroup> </ItemGroup>

View File

@ -20,6 +20,7 @@ using System.Text.RegularExpressions;
using static OF_DL.Entities.Messages.Messages; using static OF_DL.Entities.Messages.Messages;
using Akka.Configuration; using Akka.Configuration;
using System.Text; using System.Text;
using Avalonia;
using static Akka.Actor.ProviderSelection; using static Akka.Actor.ProviderSelection;
namespace OF_DL; namespace OF_DL;
@ -35,6 +36,32 @@ public class Program
private static Auth? auth = null; private static Auth? auth = null;
private static LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(); private static LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch();
[STAThread]
public static async Task Main(string[] args)
{
if (args.Length == 0)
{
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
else
{
await ConsoleApp.Run();
}
}
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<GuiApp>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
private async static Task ConsoleMain()
{
var apiHelper = new APIHelper(auth, config);
await DownloadAllData(apiHelper, auth, config);
}
private static async Task LoadAuthFromBrowser() private static async Task LoadAuthFromBrowser()
{ {
bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null;

View File

@ -0,0 +1,109 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Reactive.Concurrency;
using OF_DL.Entities;
using OF_DL.Exceptions;
using ReactiveUI;
using Serilog;
namespace OF_DL.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
#region Public Properties
[ObservableProperty] private bool _isLoading = true;
[ObservableProperty] private string _loadingText = "";
[ObservableProperty] private bool _hasSubscriptionsLoaded = false;
[ObservableProperty] private bool _hasAuthenticationFailed = false;
[ObservableProperty]
private ObservableCollection<Subscription> _subscriptionsList = [];
#endregion
private readonly AppCommon? _appCommon;
public MainWindowViewModel()
{
try
{
_appCommon = new AppCommon();
RxApp.MainThreadScheduler.Schedule(LoadSubscriptions);
}
catch (MissingFileException ex)
{
Log.Error(ex, ex.ToString());
if (ex.Filename == "auth.json")
{
// Missing auth.json
HasAuthenticationFailed = true;
}
else if (ex.Filename == "config.json")
{
// Missing config.json
// TODO: Show a dialog to create a new config.json (OK to create new config, cancel to exit)
}
}
catch (MalformedFileException ex)
{
Log.Error(ex, ex.ToString());
if (ex.Filename == "auth.json")
{
// Malformed auth.json
HasAuthenticationFailed = true;
}
else if (ex.Filename == "config.json")
{
// Malformed config.json
// TODO: Show a dialog to create a new config.json (OK to create new config, cancel to exit)
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to initialize");
}
}
private async void LoadSubscriptions()
{
if (_appCommon == null) return;
try
{
LoadingText = "Getting account";
await _appCommon.GetUser();
LoadingText = "Getting subscriptions";
await _appCommon.CreateOrUpdateUsersDatabase();
var subscriptions = await _appCommon.GetSubscriptions();
Log.Information($"Found {subscriptions.Count} subscriptions");
var subscriptionsList = new ObservableCollection<Subscription>();
foreach (var (key, value) in subscriptions)
{
subscriptionsList.Add(new Subscription(key, value));
}
SubscriptionsList = subscriptionsList;
HasSubscriptionsLoaded = true;
}
catch (UnsupportedOperatingSystem ex)
{
Log.Error(ex, ex.ToString());
// TODO: Show error dialog (exit on confirmation)
}
catch (AuthenticationFailureException ex)
{
Log.Error(ex, ex.ToString());
HasAuthenticationFailed = true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to load subscriptions");
}
IsLoading = false;
}
}

18
OF DL/app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="OF-DL.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>