Initial commit

This commit is contained in:
2026-02-08 21:30:32 +01:00
commit b2598bdd12
17 changed files with 18249 additions and 0 deletions

375
Shackle/MainForm.cs Normal file
View File

@@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace UnshackleGUI
{
public partial class MainForm : Form
{
private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
private string _paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.json");
private AppSettings _appSettings = new AppSettings();
private List<UnshackleParameter> _parameterDefinitions = new List<UnshackleParameter>();
private Dictionary<string, object> _propertyValues = new Dictionary<string, object>();
private Process? _currentProcess;
public MainForm()
{
InitializeComponent();
ApplyDarkTheme();
LoadParamsAndConfig();
// EVENT: This updates the command line the moment a value changes
pgProfile.PropertyValueChanged += (s, e) =>
{
UpdateCommandPreview();
if (comboProfiles.SelectedItem != null)
{
string selected = comboProfiles.SelectedItem.ToString();
_appSettings.Profiles[selected] =
new Dictionary<string, object>(_propertyValues);
}
SaveConfig();
};
comboService.SelectedIndexChanged += (s, e) => UpdateCommandPreview();
txtURL.TextChanged += (s, e) => UpdateCommandPreview();
txtBinName.TextChanged += (s, e) => UpdateCommandPreview();
btnRun.Click += btnRun_Click;
btnStop.Click += btnStop_Click;
btnBrowse.Click += btnBrowse_Click;
btnOpenCookies.Click += btnOpenCookies_Click;
btnEditServiceConfig.Click += btnEditServiceConfig_Click;
btnClearLog.Click += btnClearLog_Click;
btnEditYaml.Click += btnEditYaml_Click;
btnAddProfile.Click += btnAddProfile_Click;
btnRemoveProfile.Click += btnRemoveProfile_Click;
}
private void LoadParamsAndConfig()
{
// 1. Load Parameters (Rules)
if (File.Exists(_paramsPath))
{
var json = File.ReadAllText(_paramsPath);
_parameterDefinitions = JsonConvert.DeserializeObject<List<UnshackleParameter>>(json) ?? new List<UnshackleParameter>();
foreach (var p in _parameterDefinitions)
{
// Ensure every parameter has a value in the dictionary
if (!_propertyValues.ContainsKey(p.Name))
_propertyValues[p.Name] = p.Default ?? "";
}
}
// 2. Load Config (Paths/Profiles)
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
_appSettings = JsonConvert.DeserializeObject<AppSettings>(json) ?? new AppSettings();
txtRootPath.Text = _appSettings.RootPath;
txtBinName.Text = _appSettings.BinaryName;
}
if (_appSettings.Profiles == null || _appSettings.Profiles.Count == 0)
{
_appSettings.Profiles = new Dictionary<string, Dictionary<string, object>>();
_appSettings.Profiles["Default"] = new Dictionary<string, object>(_propertyValues);
}
comboProfiles.DataSource = null;
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
comboProfiles.SelectedIndexChanged += (s, e) =>
{
if (comboProfiles.SelectedItem == null) return;
string selected = comboProfiles.SelectedItem.ToString();
if (_appSettings.Profiles.TryGetValue(selected, out var values))
{
_propertyValues = new Dictionary<string, object>(values);
pgProfile.SelectedObject =
new DynamicObject(_propertyValues, _parameterDefinitions);
UpdateCommandPreview();
}
};
// 3. Bind to Grid using the FIXED Descriptor
pgProfile.SelectedObject = new DynamicObject(_propertyValues, _parameterDefinitions);
RefreshFolders();
UpdateCommandPreview();
}
private void UpdateCommandPreview()
{
StringBuilder sb = new StringBuilder();
sb.Append($"{txtBinName.Text} run unshackle dl ");
foreach (var p in _parameterDefinitions)
{
if (!_propertyValues.TryGetValue(p.Name, out object val)) continue;
// Handle Booleans
if (p.Type == "Bool" && val is bool b && b)
{
sb.Append($"{p.Flag} ");
}
// Handle Text/Selection/Numbers
else if (p.Type != "Bool" && val != null)
{
string sVal = val.ToString();
// Skip empty, "any", or "0" (for default bitrates)
if (!string.IsNullOrWhiteSpace(sVal) && sVal != "any" && sVal != "0")
{
if (sVal.Contains(" ")) sVal = $"\"{sVal}\"";
sb.Append($"{p.Flag} {sVal} ");
}
}
}
sb.Append($"{comboService.Text} {txtURL.Text}");
txtCommandPreview.Text = sb.ToString();
}
private void btnEditServiceConfig_Click(object? sender, EventArgs e)
{
string yamlPath = Path.Combine(txtRootPath.Text, "unshackle", "services", comboService.Text, "config.yaml");
if (File.Exists(yamlPath))
Process.Start(new ProcessStartInfo(yamlPath) { UseShellExecute = true });
else
MessageBox.Show($"No config.yaml found for {comboService.Text}");
}
private async void btnRun_Click(object? sender, EventArgs e)
{
string finalCmd = txtCommandPreview.Text;
if (string.IsNullOrWhiteSpace(finalCmd)) return;
ToggleUI(true);
txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}");
try { await Task.Run(() => RunCli(finalCmd)); }
catch (Exception ex) { AppendLog($"[ERROR]: {ex.Message}"); }
finally { ToggleUI(false); }
}
private void RunCli(string cmd)
{
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = "cmd.exe",
// /c = Run command and then terminate
// chcp 65001 = Force console to use UTF-8 encoding
// && = Run the actual command immediately after setting encoding
Arguments = $"/c chcp 65001 && {cmd}",
WorkingDirectory = txtRootPath.Text,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
// Ensure Python scripts know to use UTF-8
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
using (_currentProcess = Process.Start(psi))
{
if (_currentProcess == null) return;
_currentProcess.OutputDataReceived += (s, e) =>
{
// Filter out the noisy "Active code page: 65001" message from CMD
if (!string.IsNullOrEmpty(e.Data) && !e.Data.Contains("Active code page"))
AppendLog(e.Data);
};
_currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data);
_currentProcess.BeginOutputReadLine();
_currentProcess.BeginErrorReadLine();
_currentProcess.WaitForExit();
}
}
private void AppendLog(string? text)
{
if (string.IsNullOrEmpty(text)) return;
this.BeginInvoke(new Action(() => { txtLog.AppendText(text + Environment.NewLine); }));
}
private void ApplyDarkTheme()
{
this.BackColor = Color.FromArgb(30, 30, 30);
this.ForeColor = Color.White;
foreach (Control c in this.Controls) UpdateControlTheme(c);
}
private void UpdateControlTheme(Control c)
{
c.BackColor = Color.FromArgb(45, 45, 48);
c.ForeColor = Color.White;
if (c is TextBox tb) tb.BorderStyle = BorderStyle.FixedSingle;
if (c is Button btn) { btn.FlatStyle = FlatStyle.Flat; btn.FlatAppearance.BorderColor = Color.Gray; }
if (c is PropertyGrid pg)
{
pg.BackColor = Color.FromArgb(37, 37, 38);
pg.ViewBackColor = Color.FromArgb(37, 37, 38);
pg.ViewForeColor = Color.White;
pg.LineColor = Color.FromArgb(45, 45, 48);
}
foreach (Control child in c.Controls) UpdateControlTheme(child);
}
private void SaveConfig()
{
_appSettings.RootPath = txtRootPath.Text;
_appSettings.BinaryName = txtBinName.Text;
File.WriteAllText(_configPath, JsonConvert.SerializeObject(_appSettings, Formatting.Indented));
}
private void RefreshFolders()
{
comboService.Items.Clear();
string path = Path.Combine(txtRootPath.Text, "unshackle", "services");
if (Directory.Exists(path))
{
var dirs = Directory.GetDirectories(path).Select(Path.GetFileName).ToArray();
comboService.Items.AddRange(dirs.Cast<object>().ToArray());
if (comboService.Items.Count > 0) comboService.SelectedIndex = 0;
}
}
private void ComboProfiles_SelectedIndexChanged(object? sender, EventArgs e)
{
if (comboProfiles.SelectedItem == null) return;
string selected = comboProfiles.SelectedItem.ToString();
if (_appSettings.Profiles.TryGetValue(selected, out var values))
{
_propertyValues = new Dictionary<string, object>(values);
pgProfile.SelectedObject =
new DynamicObject(_propertyValues, _parameterDefinitions);
UpdateCommandPreview();
}
}
private void btnAddProfile_Click(object? sender, EventArgs e)
{
string name = Microsoft.VisualBasic.Interaction.InputBox(
"Enter profile name:",
"New Profile",
"NewProfile");
if (string.IsNullOrWhiteSpace(name))
return;
if (_appSettings.Profiles.ContainsKey(name))
{
MessageBox.Show("Profile already exists.");
return;
}
// Clone current settings into new profile
_appSettings.Profiles[name] =
new Dictionary<string, object>(_propertyValues);
comboProfiles.DataSource = null;
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
comboProfiles.SelectedItem = name;
SaveConfig();
}
private void btnRemoveProfile_Click(object? sender, EventArgs e)
{
if (comboProfiles.SelectedItem == null)
return;
string selected = comboProfiles.SelectedItem.ToString();
if (selected == "Default")
{
MessageBox.Show("Default profile cannot be deleted.");
return;
}
if (!_appSettings.Profiles.ContainsKey(selected))
return;
_appSettings.Profiles.Remove(selected);
comboProfiles.DataSource = null;
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
comboProfiles.SelectedIndex = 0;
SaveConfig();
}
private void ToggleUI(bool r) => this.BeginInvoke(new Action(() => { btnRun.Enabled = !r; btnStop.Visible = r; }));
private void btnStop_Click(object? sender, EventArgs e) { _currentProcess?.Kill(true); }
private void btnClearLog_Click(object? sender, EventArgs e) => txtLog.Clear();
private void btnBrowse_Click(object? sender, EventArgs e)
{
using (var fbd = new FolderBrowserDialog()) if (fbd.ShowDialog() == DialogResult.OK) { txtRootPath.Text = fbd.SelectedPath; SaveConfig(); RefreshFolders(); }
}
private void btnOpenCookies_Click(object? sender, EventArgs e)
{
string p = Path.Combine(txtRootPath.Text, "unshackle", "cookies", comboService.Text);
Directory.CreateDirectory(p); Process.Start("explorer.exe", p);
}
private void btnEditYaml_Click(object? sender, EventArgs e)
{
string yamlPath = Path.Combine(txtRootPath.Text, "unshackle/unshackle.yaml");
if (File.Exists(yamlPath)) Process.Start(new ProcessStartInfo(yamlPath) { UseShellExecute = true });
}
}
// --- DATA MODELS ---
public class AppSettings
{
public string RootPath { get; set; } = @"C:\DEVINE\unshackle";
public string BinaryName { get; set; } = "uv";
public Dictionary<string, Dictionary<string, object>> Profiles { get; set; }
= new Dictionary<string, Dictionary<string, object>>();
}
public class UnshackleParameter
{
public string Flag { get; set; } = "";
public string Name { get; set; } = "";
public string Type { get; set; } = "Text";
public List<string>? Options { get; set; }
public object? Default { get; set; }
public string Category { get; set; } = "Misc";
}
}