Decompiled source of SemiBoombox v1.0.0

SemiBoombox.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using SemiBoombox.Utils;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("SemiBoombox")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("Semi Boombox")]
[assembly: AssemblyTitle("SemiBoombox")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace SemiBoombox
{
	public class Boombox : MonoBehaviour
	{
		public PhotonView photonView;

		public AudioSource audioSource;

		private static Dictionary<string, AudioClip> downloadedClips = new Dictionary<string, AudioClip>();

		private static Dictionary<string, HashSet<int>> downloadsReady = new Dictionary<string, HashSet<int>>();

		private void Awake()
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Expected O, but got Unknown
			audioSource = ((Component)this).gameObject.AddComponent<AudioSource>();
			audioSource.spatialBlend = 1f;
			audioSource.playOnAwake = false;
			audioSource.minDistance = 1f;
			audioSource.maxDistance = 30f;
			audioSource.rolloffMode = (AudioRolloffMode)2;
			AnimationCurve val = new AnimationCurve();
			val.AddKey(1f, 1f);
			val.AddKey(30f, 0f);
			audioSource.SetCustomCurve((AudioSourceCurveType)0, val);
			photonView = ((Component)this).GetComponent<PhotonView>();
			if ((Object)(object)photonView == (Object)null)
			{
				Debug.LogError((object)"PhotonView not found on Boombox object.");
			}
			else if (photonView.IsMine)
			{
				audioSource.volume = 0.1f;
				((Component)this).gameObject.AddComponent<BoomboxUI>();
			}
			else
			{
				audioSource.volume = 0.2f;
			}
		}

		[PunRPC]
		public async void RequestSong(string url, int requesterId)
		{
			Debug.Log((object)$"RequestSong RPC received: url={url}, requesterId={requesterId}");
			if (!downloadedClips.ContainsKey(url))
			{
				try
				{
					AudioClip value = await GetAudioClipAsync(await YoutubeDL.DownloadAudioAsync(url));
					downloadedClips[url] = value;
					Debug.Log((object)("Downloaded and cached clip for url: " + url));
				}
				catch (Exception ex)
				{
					Debug.LogError((object)("Failed to download audio: " + ex.Message));
					return;
				}
			}
			else
			{
				Debug.Log((object)("Clip already cached for url: " + url));
			}
			photonView.RPC("ReportDownloadComplete", (RpcTarget)0, new object[2]
			{
				PhotonNetwork.LocalPlayer.ActorNumber,
				url
			});
			await WaitForAllPlayersReady(url);
			photonView.RPC("SyncPlayback", (RpcTarget)0, new object[2] { url, requesterId });
		}

		[PunRPC]
		public void ReportDownloadComplete(int actorNumber, string url)
		{
			if (!downloadsReady.ContainsKey(url))
			{
				downloadsReady[url] = new HashSet<int>();
			}
			downloadsReady[url].Add(actorNumber);
			Debug.Log((object)$"Player {actorNumber} reported ready for url: {url}. Total ready: {downloadsReady[url].Count}");
		}

		private async Task WaitForAllPlayersReady(string url)
		{
			int totalPlayers = PhotonNetwork.PlayerList.Length;
			while (!downloadsReady.ContainsKey(url) || downloadsReady[url].Count < totalPlayers)
			{
				await Task.Delay(100);
			}
		}

		[PunRPC]
		public void SyncPlayback(string url, int requesterId)
		{
			Debug.Log((object)$"SyncPlayback RPC received: url={url}, requesterId={requesterId}");
			Boombox boombox = FindBoomboxForPlayer(requesterId);
			if ((Object)(object)boombox == (Object)null)
			{
				Debug.LogError((object)$"No boombox found for player {requesterId}");
				return;
			}
			if (!downloadedClips.ContainsKey(url))
			{
				Debug.LogError((object)("Clip not found for url: " + url));
				return;
			}
			boombox.audioSource.clip = downloadedClips[url];
			boombox.audioSource.Play();
		}

		[PunRPC]
		public void StopPlayback(int requesterId)
		{
			if (photonView.Owner != null && photonView.Owner.ActorNumber == requesterId)
			{
				Debug.Log((object)$"Stopping playback on Boombox owned by player {requesterId}");
				if (audioSource.isPlaying)
				{
					audioSource.Stop();
				}
			}
		}

		private static Boombox FindBoomboxForPlayer(int playerId)
		{
			Boombox[] array = Object.FindObjectsOfType<Boombox>();
			foreach (Boombox boombox in array)
			{
				if (boombox.photonView.Owner != null && boombox.photonView.Owner.ActorNumber == playerId)
				{
					return boombox;
				}
			}
			return null;
		}

		public static async Task<AudioClip> GetAudioClipAsync(string filePath)
		{
			string text = "file://" + filePath;
			UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(text, (AudioType)13);
			try
			{
				TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
				((AsyncOperation)www.SendWebRequest()).completed += delegate
				{
					//IL_0006: Unknown result type (might be due to invalid IL or missing references)
					//IL_000c: Invalid comparison between Unknown and I4
					if ((int)www.result != 1)
					{
						tcs.SetException(new Exception("Failed to load audio file: " + www.error));
					}
					else
					{
						tcs.SetResult(result: true);
					}
				};
				await tcs.Task;
				AudioClip content = DownloadHandlerAudioClip.GetContent(www);
				if (Directory.Exists(Path.GetDirectoryName(filePath)))
				{
					Directory.Delete(Path.GetDirectoryName(filePath), recursive: true);
				}
				return content;
			}
			finally
			{
				if (www != null)
				{
					((IDisposable)www).Dispose();
				}
			}
		}
	}
	public class BoomboxUI : MonoBehaviour
	{
		public PhotonView photonView;

		private bool showUI;

		private string urlInput = "";

		private float volume = 0.15f;

		private Rect windowRect = new Rect(100f, 100f, 350f, 200f);

		private Boombox boombox;

		private void Awake()
		{
			boombox = ((Component)this).GetComponent<Boombox>();
			photonView = boombox.photonView;
		}

		private void Update()
		{
			if (Input.GetKeyDown((KeyCode)120))
			{
				showUI = !showUI;
			}
		}

		private void OnGUI()
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			if (showUI)
			{
				windowRect = GUI.Window(0, windowRect, new WindowFunction(DrawUI), "Boombox Controller");
			}
		}

		private void DrawUI(int windowID)
		{
			GUILayout.Label("Enter YouTube URL:", Array.Empty<GUILayoutOption>());
			urlInput = GUILayout.TextField(urlInput, 200, Array.Empty<GUILayoutOption>());
			GUILayout.Space(10f);
			GUILayout.Label($"Volume: {Mathf.Round(volume * 100f)}%", Array.Empty<GUILayoutOption>());
			volume = GUILayout.HorizontalSlider(volume, 0f, 1f, Array.Empty<GUILayoutOption>());
			if ((Object)(object)boombox.audioSource != (Object)null)
			{
				boombox.audioSource.volume = volume;
			}
			GUILayout.Space(10f);
			GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>());
			if (GUILayout.Button("Play", Array.Empty<GUILayoutOption>()))
			{
				if (IsValidUrl(urlInput))
				{
					photonView.RPC("RequestSong", (RpcTarget)0, new object[2]
					{
						urlInput,
						PhotonNetwork.LocalPlayer.ActorNumber
					});
				}
				else
				{
					Debug.LogError((object)"Invalid URL!");
				}
			}
			if (GUILayout.Button("Stop", Array.Empty<GUILayoutOption>()))
			{
				photonView.RPC("StopPlayback", (RpcTarget)0, new object[1] { PhotonNetwork.LocalPlayer.ActorNumber });
			}
			if (GUILayout.Button("Close", Array.Empty<GUILayoutOption>()))
			{
				showUI = false;
			}
			GUILayout.EndHorizontal();
			GUI.DragWindow();
		}

		private bool IsValidUrl(string url)
		{
			string pattern = "^https?:\\/\\/(www\\.)?youtube\\.com\\/watch\\?v=[a-zA-Z0-9_-]+$";
			return Regex.IsMatch(url, pattern);
		}
	}
	[BepInPlugin("SemiBoombox", "Semi Boombox", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Logger;

		private static Harmony _harmony;

		private void Awake()
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			Logger = ((BaseUnityPlugin)this).Logger;
			Logger.LogInfo((object)"Plugin SemiBoombox is loaded!");
			Task.Run(delegate
			{
				YoutubeDL.InitializeAsync().Wait();
			});
			_harmony = new Harmony("SemiBoombox");
			_harmony.PatchAll();
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "SemiBoombox";

		public const string PLUGIN_NAME = "Semi Boombox";

		public const string PLUGIN_VERSION = "1.0.0";
	}
}
namespace SemiBoombox.Utils
{
	public static class YoutubeDL
	{
		private static readonly string baseFolder = Path.Combine(Directory.GetCurrentDirectory(), "SemiBoombox");

		private const string YtDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/download/2025.02.19/yt-dlp.exe";

		private const string FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip";

		private static readonly string ytDlpPath = Path.Combine(baseFolder, "yt-dlp.exe");

		private static readonly string ffmpegFolder = Path.Combine(baseFolder, "ffmpeg");

		private static readonly string ffmpegBinPath = Path.Combine(ffmpegFolder, "ffmpeg-master-latest-win64-gpl", "bin", "ffmpeg.exe");

		public static async Task InitializeAsync()
		{
			if (!Directory.Exists(baseFolder))
			{
				Directory.CreateDirectory(baseFolder);
			}
			if (!File.Exists(ytDlpPath))
			{
				Console.WriteLine("yt-dlp not found. Downloading...");
				await DownloadFileAsync("https://github.com/yt-dlp/yt-dlp/releases/download/2025.02.19/yt-dlp.exe", ytDlpPath);
			}
			if (!Directory.Exists(ffmpegFolder))
			{
				Console.WriteLine("ffmpeg not found. Downloading and extracting...");
				await DownloadAndExtractFFmpegAsync();
			}
			if (!File.Exists(ffmpegBinPath))
			{
				throw new Exception("ffmpeg executable was not found after extraction.");
			}
			Console.WriteLine("Initialization complete.");
		}

		private static async Task DownloadFileAsync(string url, string destinationPath)
		{
			HttpClient client = new HttpClient();
			try
			{
				File.WriteAllBytes(destinationPath, await client.GetByteArrayAsync(url));
			}
			finally
			{
				((IDisposable)client)?.Dispose();
			}
		}

		private static async Task DownloadAndExtractFFmpegAsync()
		{
			string zipPath = Path.Combine(baseFolder, "ffmpeg.zip");
			await DownloadFileAsync("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", zipPath);
			ZipFile.ExtractToDirectory(zipPath, ffmpegFolder);
			File.Delete(zipPath);
		}

		public static async Task<string> DownloadAudioAsync(string videoUrl)
		{
			await InitializeAsync();
			string tempFolder = Path.Combine(baseFolder, Guid.NewGuid().ToString());
			Directory.CreateDirectory(tempFolder);
			Console.WriteLine("Downloading audio...");
			return await Task.Run(delegate
			{
				try
				{
					string arguments = "-x --audio-format mp3 --ffmpeg-location \"" + ffmpegBinPath + "\" --output \"" + Path.Combine(tempFolder, "%(title)s.%(ext)s") + "\" " + videoUrl;
					using (Process process = Process.Start(new ProcessStartInfo
					{
						FileName = ytDlpPath,
						Arguments = arguments,
						RedirectStandardOutput = true,
						RedirectStandardError = true,
						UseShellExecute = false,
						CreateNoWindow = true
					}))
					{
						if (process == null)
						{
							throw new Exception("Failed to start yt-dlp process.");
						}
						process.WaitForExit();
						if (process.ExitCode != 0)
						{
							string text = process.StandardError.ReadToEnd();
							throw new Exception("yt-dlp error: " + text);
						}
					}
					return Directory.GetFiles(tempFolder, "*.mp3").FirstOrDefault() ?? throw new Exception("Audio download failed.");
				}
				catch (Exception ex)
				{
					Directory.Delete(tempFolder, recursive: true);
					throw new Exception("Error downloading audio: " + ex.Message);
				}
			});
		}
	}
}
namespace SemiBoombox.Patches
{
	[HarmonyPatch(typeof(PlayerAvatar), "Awake")]
	public class PlayerAvatarPatch
	{
		private static void Postfix(PlayerAvatar __instance)
		{
			if ((Object)(object)((Component)__instance).GetComponent<Boombox>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<Boombox>();
			}
		}
	}
}