This commit is contained in:
2026-02-01 13:57:36 +01:00
parent 94b0379e98
commit 525ab5c41e
11 changed files with 533 additions and 95 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.vs .vs
obj obj
bin

39
Form1.Designer.cs generated
View File

@@ -1,39 +0,0 @@
namespace VRChat_YouTube_Workaround
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Text = "Form1";
}
#endregion
}
}

View File

@@ -1,10 +0,0 @@
namespace VRChat_YouTube_Workaround
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}

206
MainForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,206 @@
namespace Marro.VRChatYouTubeWorkaround
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
txtInputUrl = new TextBox();
btnGo = new Button();
grpInputUrl = new GroupBox();
btnPasteAndGo = new Button();
txtLog = new TextBox();
grpStatus = new GroupBox();
btnCancel = new Button();
lblStatus = new Label();
prgProgress = new ProgressBar();
grpResult = new GroupBox();
txtResultUrl = new TextBox();
btnCopy = new Button();
grpInputUrl.SuspendLayout();
grpStatus.SuspendLayout();
grpResult.SuspendLayout();
SuspendLayout();
//
// txtInputUrl
//
txtInputUrl.Location = new Point(6, 51);
txtInputUrl.Name = "txtInputUrl";
txtInputUrl.Size = new Size(683, 23);
txtInputUrl.TabIndex = 2;
//
// btnGo
//
btnGo.Location = new Point(695, 51);
btnGo.Name = "btnGo";
btnGo.Size = new Size(75, 23);
btnGo.TabIndex = 3;
btnGo.TabStop = false;
btnGo.Text = "Go!";
btnGo.UseVisualStyleBackColor = true;
btnGo.Click += btnGo_Click;
//
// grpInputUrl
//
grpInputUrl.Controls.Add(btnPasteAndGo);
grpInputUrl.Controls.Add(txtInputUrl);
grpInputUrl.Controls.Add(btnGo);
grpInputUrl.Location = new Point(12, 12);
grpInputUrl.Name = "grpInputUrl";
grpInputUrl.Size = new Size(776, 81);
grpInputUrl.TabIndex = 4;
grpInputUrl.TabStop = false;
grpInputUrl.Text = "Input URL";
//
// btnPasteAndGo
//
btnPasteAndGo.BackColor = Color.Chartreuse;
btnPasteAndGo.ForeColor = SystemColors.ControlText;
btnPasteAndGo.Location = new Point(6, 22);
btnPasteAndGo.Name = "btnPasteAndGo";
btnPasteAndGo.Size = new Size(764, 23);
btnPasteAndGo.TabIndex = 1;
btnPasteAndGo.Text = "Paste and Go!";
btnPasteAndGo.UseVisualStyleBackColor = false;
btnPasteAndGo.Click += btnPasteAndGo_Click;
//
// txtLog
//
txtLog.Location = new Point(12, 259);
txtLog.Multiline = true;
txtLog.Name = "txtLog";
txtLog.ReadOnly = true;
txtLog.ScrollBars = ScrollBars.Both;
txtLog.Size = new Size(776, 172);
txtLog.TabIndex = 0;
txtLog.TabStop = false;
txtLog.WordWrap = false;
//
// grpStatus
//
grpStatus.Controls.Add(btnCancel);
grpStatus.Controls.Add(lblStatus);
grpStatus.Controls.Add(prgProgress);
grpStatus.Location = new Point(12, 99);
grpStatus.Name = "grpStatus";
grpStatus.Size = new Size(776, 97);
grpStatus.TabIndex = 6;
grpStatus.TabStop = false;
grpStatus.Text = "Status";
//
// btnCancel
//
btnCancel.Enabled = false;
btnCancel.Location = new Point(6, 66);
btnCancel.Name = "btnCancel";
btnCancel.Size = new Size(764, 23);
btnCancel.TabIndex = 4;
btnCancel.Text = "Cancel";
btnCancel.UseVisualStyleBackColor = true;
btnCancel.Click += btnCancel_Click;
//
// lblStatus
//
lblStatus.AutoSize = true;
lblStatus.Location = new Point(6, 19);
lblStatus.Name = "lblStatus";
lblStatus.Size = new Size(39, 15);
lblStatus.TabIndex = 6;
lblStatus.Text = "Ready";
//
// prgProgress
//
prgProgress.Location = new Point(6, 37);
prgProgress.Name = "prgProgress";
prgProgress.Size = new Size(764, 23);
prgProgress.TabIndex = 0;
//
// grpResult
//
grpResult.Controls.Add(txtResultUrl);
grpResult.Controls.Add(btnCopy);
grpResult.Location = new Point(12, 202);
grpResult.Name = "grpResult";
grpResult.Size = new Size(776, 51);
grpResult.TabIndex = 5;
grpResult.TabStop = false;
grpResult.Text = "Result";
//
// txtResultUrl
//
txtResultUrl.Location = new Point(6, 19);
txtResultUrl.Name = "txtResultUrl";
txtResultUrl.ReadOnly = true;
txtResultUrl.Size = new Size(683, 23);
txtResultUrl.TabIndex = 5;
//
// btnCopy
//
btnCopy.Location = new Point(695, 19);
btnCopy.Name = "btnCopy";
btnCopy.Size = new Size(75, 23);
btnCopy.TabIndex = 1;
btnCopy.Text = "Copy";
btnCopy.UseVisualStyleBackColor = true;
btnCopy.Click += btnCopy_Click;
//
// MainForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 443);
Controls.Add(grpResult);
Controls.Add(grpInputUrl);
Controls.Add(grpStatus);
Controls.Add(txtLog);
Name = "MainForm";
Text = "VRChat-YouTube-Workaround";
grpInputUrl.ResumeLayout(false);
grpInputUrl.PerformLayout();
grpStatus.ResumeLayout(false);
grpStatus.PerformLayout();
grpResult.ResumeLayout(false);
grpResult.PerformLayout();
ResumeLayout(false);
PerformLayout();
}
#endregion
private TextBox txtInputUrl;
private Button btnGo;
private GroupBox grpInputUrl;
private TextBox txtLog;
private GroupBox grpStatus;
private ProgressBar prgProgress;
private GroupBox grpResult;
private TextBox txtResultUrl;
private Button btnCopy;
private Button btnPasteAndGo;
private Label lblStatus;
private Button btnCancel;
}
}

237
MainForm.cs Normal file
View File

@@ -0,0 +1,237 @@
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic.ApplicationServices;
using Newtonsoft.Json;
using System.Media;
using System.Security.Policy;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
using YoutubeDLSharp;
namespace Marro.VRChatYouTubeWorkaround;
public partial class MainForm : Form
{
private readonly YoutubeDL ytdl;
private readonly Progress<DownloadProgress> downloadProgress;
private readonly Progress<string> outputProgress;
private CancellationTokenSource cts;
private class FailedException(string message) : Exception(message) { }
public MainForm()
{
InitializeComponent();
ytdl = new YoutubeDL();
downloadProgress = new Progress<DownloadProgress>(CaptureProgress);
outputProgress = new Progress<string>(WriteLog);
prgProgress.Maximum = 100;
}
private void btnGo_Click(object sender, EventArgs e)
{
ClearLog();
ResetProgress();
var url = txtInputUrl.Text.Trim();
if (string.IsNullOrEmpty(url))
{
WriteStatus("Enter a url!");
return;
}
cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
var task = Task.Run(() => ProcessVideo(url, cancellationToken));
task.ContinueWith(ProcessVideoShowResult);
grpInputUrl.Enabled = false;
btnCancel.Enabled = true;
}
private async Task<string> ProcessVideo(string sourceUrl, CancellationToken cancellationToken)
{
string filePath = "";
try
{
filePath = await DownloadVideoToFile(sourceUrl, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
WriteStatus("Uploading video...");
var url = await UploadFile(filePath, cancellationToken);
return url;
}
finally
{
File.Delete(filePath);
}
}
private void ProcessVideoShowResult(Task<string> task)
{
if (InvokeRequired)
{
Invoke(() => ProcessVideoShowResult(task));
return;
}
grpInputUrl.Enabled = true;
btnCancel.Enabled = false;
if (cts.IsCancellationRequested || task.IsCanceled)
{
WriteStatus("Cancelled.");
ResetProgress();
return;
}
if (task.IsFaulted)
{
WriteLog($"{task.Exception.InnerException}");
WriteStatus("Failed.");
ResetProgress();
return;
}
var url = task.Result;
txtResultUrl.Text = url;
WriteLog($"Final URL: {url}");
WriteStatus("Finished.");
Clipboard.SetText(url);
Chime();
}
private async Task<string> DownloadVideoToFile(string url, CancellationToken cancellationToken)
{
WriteStatus("Downloading required files...");
await Utils.DownloadBinaries();
WriteStatus("Downloading video...");
var res = await ytdl.RunVideoDownload(url, format: "mp4", progress: downloadProgress, output: outputProgress, ct: cancellationToken);
if (!res.Success)
{
throw new FailedException("Failed to download video!");
}
return res.Data;
}
private static async Task<string> UploadFile(string filePath, CancellationToken cancellationToken)
{
var url = new Uri("https://media.komawo.gay/");
url = new Uri(url, $"{Path.GetFileName(filePath)}?j");
using var client = new HttpClient();
using var fileStream = File.OpenRead(filePath);
using var content = new StreamContent(fileStream);
content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("video/mp4");
content.Headers.Add("pw", "Luminance7-Trapper0-Choice6-Brunette9-Abacus7");
content.Headers.Add("want", "url");
content.Headers.Add("ck", "no");
content.Headers.Add("replace", "1");
var response = await client.PutAsync(url, content, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
var responseObject = JsonConvert.DeserializeObject<CopyPartyResult>(responseJson);
return responseObject?.FileUrl ?? throw new FailedException("Failed to get url for uploaded video!");
}
private class CopyPartyResult
{
[JsonProperty("filesz")]
public int? FileSZ { get; set; }
[JsonProperty("fileurl")]
public string? FileUrl { get; set; }
}
private void WriteLog(string message)
{
if (InvokeRequired)
{
Invoke(() => WriteLog(message));
return;
}
txtLog.AppendText($"{message}{Environment.NewLine}");
}
private void WriteStatus(string message)
{
if (InvokeRequired)
{
Invoke(() => WriteStatus(message));
return;
}
lblStatus.Text = message;
}
private void ClearLog()
{
txtLog.Clear();
}
private void Chime()
{
SoundPlayer simpleSound = new SoundPlayer(@"C:\Windows\Media\Windows Background.wav");
try
{
simpleSound.Play();
}
catch
{
// Playing sound is not critical
}
}
private void CaptureProgress(DownloadProgress progress)
{
var newValue = (int)(progress.Progress * 100);
var step = newValue - prgProgress.Value;
if (step <= 0)
{
return;
}
prgProgress.Step = step;
prgProgress.PerformStep();
}
private void ResetProgress()
{
prgProgress.Value = 0;
}
private void btnCopy_Click(object sender, EventArgs e)
{
Clipboard.SetText(txtResultUrl.Text);
}
private void btnPasteAndGo_Click(object sender, EventArgs e)
{
txtInputUrl.Text = Clipboard.GetText();
btnGo.PerformClick();
}
private void btnCancel_Click(object sender, EventArgs e)
{
cts?.Cancel();
}
}

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->

View File

@@ -1,17 +1,27 @@
namespace VRChat_YouTube_Workaround using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Marro.VRChatYouTubeWorkaround;
internal static class Program
{ {
internal static class Program [STAThread]
static void Main()
{ {
/// <summary> Application.SetHighDpiMode(HighDpiMode.SystemAware);
/// The main entry point for the application. Application.EnableVisualStyles();
/// </summary> Application.SetCompatibleTextRenderingDefault(false);
[STAThread]
static void Main() var services = new ServiceCollection();
{ ConfigureServices(services);
// To customize application configuration such as set high DPI settings or default font, using ServiceProvider serviceProvider = services.BuildServiceProvider();
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize(); var form = serviceProvider.GetRequiredService<MainForm>();
Application.Run(new Form1()); Application.Run(form);
} }
private static void ConfigureServices(ServiceCollection services)
{
services.AddSingleton<MainForm>();
} }
} }

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net10.0-windows\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<History>True|2026-01-31T21:08:30.9376550Z||;True|2026-01-31T22:07:21.7187910+01:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>

View File

@@ -9,4 +9,15 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<RootNamespace>Marro.VRChatYouTubeWorkaround</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2" />
<PackageReference Include="YoutubeDLSharp" Version="1.2.0" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <PropertyGroup>
<Compile Update="Form1.cs"> <_LastSelectedProfileId>C:\Users\marro\Etc\DotNET_Projects\VRChat-YouTube-Workaround\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
<SubType>Form</SubType> </PropertyGroup>
</Compile> <ItemGroup>
</ItemGroup> <Compile Update="MainForm.cs">
</Project> <SubType>Form</SubType>
</Compile>
</ItemGroup>
</Project>