Decompiled source of Configured Youtube Boombox v0.3.1

BepInEx/plugins/com.github.lordfirespeed.configured_youtube_boombox.dll

Decompiled 10 months ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ConfiguredYoutubeBoombox.util;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using YoutubeDLSharp;
using YoutubeDLSharp.Metadata;
using YoutubeDLSharp.Options;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: IgnoresAccessChecksTo("")]
[assembly: AssemblyCompany("com.github.lordfirespeed.configured_youtube_boombox")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.3.1.0")]
[assembly: AssemblyInformationalVersion("0.3.1+94197c943dd1e332ed0b0b6d30ba7a9aea585a9e")]
[assembly: AssemblyProduct("Configured Youtube Boombox")]
[assembly: AssemblyTitle("com.github.lordfirespeed.configured_youtube_boombox")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.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.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 ConfiguredYoutubeBoombox
{
	public class ConfiguredTrackListFile
	{
		[JsonProperty("tracks")]
		public ConfiguredTrack[]? Tracks { get; set; }
	}
	public class ConfiguredTrack
	{
		[JsonProperty("youtubeVideoId")]
		public string? VideoId { get; set; }

		[JsonProperty("trackName")]
		public string? TrackName { get; set; }

		[JsonProperty("startTimestamp")]
		public string? StartTimestamp { get; set; }

		[JsonProperty("endTimestamp")]
		public string? EndTimestamp { get; set; }

		[JsonProperty("volumeScalar")]
		public float? VolumeScalar { get; set; }
	}
	public class VideoTooLongException : Exception
	{
		public VideoTooLongException()
		{
		}

		public VideoTooLongException(string message)
			: base(message)
		{
		}

		public VideoTooLongException(string message, Exception inner)
			: base(message, inner)
		{
		}
	}
	public class VideoTooLongExceptiona : Exception
	{
		public VideoTooLongExceptiona()
		{
		}

		public VideoTooLongExceptiona(string message)
			: base(message)
		{
		}

		public VideoTooLongExceptiona(string message, Exception inner)
			: base(message, inner)
		{
		}
	}
	public class YoutubeDLProcessFailed : Exception
	{
		public YoutubeDLProcessFailed()
		{
		}

		public YoutubeDLProcessFailed(string message)
			: base(message)
		{
		}

		public YoutubeDLProcessFailed(string message, Exception inner)
			: base(message, inner)
		{
		}
	}
	[BepInPlugin("com.github.lordfirespeed.configured_youtube_boombox", "Configured Youtube Boombox", "0.3.1")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource? Logger;

		internal static string? PluginDataPath;

		internal static string? DownloadsPath;

		internal static ConfigEntry<float> MaxSongDuration { get; private set; } = null;


		internal static YoutubeDL YoutubeDL { get; } = new YoutubeDL((byte)4);


		internal static JsonSerializer TrackListSerializer { get; } = new JsonSerializer();


		public Plugin()
		{
			Logger = ((BaseUnityPlugin)this).Logger;
		}

		private async void Awake()
		{
			MaxSongDuration = ((BaseUnityPlugin)this).Config.Bind<float>(new ConfigDefinition("General", "Max Song Duration"), 600f, new ConfigDescription("Maximum song duration in seconds. Any video longer than this will not be downloaded.", (AcceptableValueBase)null, Array.Empty<object>()));
			await InitializeToolsAndDirectories();
			await DownloadConfiguredTracks();
		}

		private async Task InitializeToolsAndDirectories()
		{
			PluginDataPath = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "configured-youtube-boombox-data");
			DownloadsPath = Path.Combine(Paths.BepInExRootPath, "Custom Songs", "Boombox Music");
			if (!Directory.Exists(PluginDataPath))
			{
				Directory.CreateDirectory(PluginDataPath);
			}
			if (!Directory.Exists(DownloadsPath))
			{
				Directory.CreateDirectory(DownloadsPath);
			}
			Func<Task> ensureYtDlp = async delegate
			{
				if (!Directory.GetFiles(PluginDataPath).Any((string file) => file.Contains("yt-dl")))
				{
					await Utils.DownloadYtDlp(PluginDataPath);
				}
			};
			Func<Task> ensureFfMpeg = async delegate
			{
				if (!Directory.GetFiles(PluginDataPath).Any((string file) => file.Contains("ffmpeg")))
				{
					await Utils.DownloadFFmpeg(PluginDataPath);
				}
			};
			await Task.WhenAll(ensureYtDlp(), ensureFfMpeg());
			YoutubeDL.YoutubeDLPath = Directory.GetFiles(PluginDataPath).First((string file) => file.Contains("yt-dl"));
			YoutubeDL.FFmpegPath = Directory.GetFiles(PluginDataPath).First((string file) => file.Contains("ffmpeg"));
			YoutubeDL.OutputFolder = DownloadsPath;
			YoutubeDL.OutputFileTemplate = "%(id)s.%(ext)s";
		}

		private IEnumerable<string> DiscoverConfiguredTrackListFiles()
		{
			return (from pluginDirectory in Directory.GetDirectories(Paths.PluginPath)
				select Path.Join((ReadOnlySpan<char>)pluginDirectory, (ReadOnlySpan<char>)"configured-youtube-boombox-tracks.json")).Where(File.Exists);
		}

		private ConfiguredTrack[] DeserializeConfiguredTrackListFile(string trackListFilePath)
		{
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Expected O, but got Unknown
			using StreamReader streamReader = new StreamReader(trackListFilePath);
			JsonTextReader val = new JsonTextReader((TextReader)streamReader);
			try
			{
				ConfiguredTrackListFile configuredTrackListFile = TrackListSerializer.Deserialize<ConfiguredTrackListFile>((JsonReader)(object)val);
				if (configuredTrackListFile?.Tracks != null)
				{
					return configuredTrackListFile.Tracks;
				}
				ManualLogSource? logger = Logger;
				if (logger != null)
				{
					logger.LogWarning((object)("Failed to deserialize any tracks from " + trackListFilePath + "."));
				}
				return Array.Empty<ConfiguredTrack>();
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}

		private IEnumerable<ConfiguredTrack> DiscoverConfiguredTracks()
		{
			return DiscoverConfiguredTrackListFiles().SelectMany(DeserializeConfiguredTrackListFile);
		}

		private async Task DownloadConfiguredTracks()
		{
			await Task.WhenAll(DiscoverConfiguredTracks().Select(TrackDownloader.DownloadTrack));
		}
	}
	public class TrackDownloader
	{
		protected static async Task<float> FetchSongDuration(string id)
		{
			ManualLogSource? logger = Plugin.Logger;
			if (logger != null)
			{
				logger.LogDebug((object)"Fetching video metadata.");
			}
			RunResult<VideoData> videoDataResult = await Plugin.YoutubeDL.RunVideoDataFetch(id, default(CancellationToken), true, false, (OptionSet)null);
			int num;
			if (videoDataResult != null && videoDataResult.Success)
			{
				VideoData data = videoDataResult.Data;
				if (data != null)
				{
					num = (data.Duration.HasValue ? 1 : 0);
					goto IL_00ed;
				}
			}
			num = 0;
			goto IL_00ed;
			IL_00ed:
			if (num != 0)
			{
				return videoDataResult.Data.Duration.Value;
			}
			throw new Exception("Failed to fetch video duration.");
		}

		protected static TimeSpan ParseTimestamp(string timestamp)
		{
			if (string.IsNullOrWhiteSpace(timestamp))
			{
				throw new ArgumentException("Timestamp cannot be null or whitespace");
			}
			if (float.TryParse(timestamp, out var result))
			{
				return TimeSpan.FromSeconds(result);
			}
			return TimeSpan.Parse(timestamp, new CultureInfo("en-us"));
		}

		protected static async Task<float> GetEffectiveDuration(ConfiguredTrack track)
		{
			if (track.VideoId == null)
			{
				throw new NullReferenceException("Track VideoId must not be null.");
			}
			float duration = await InfoCache.DurationCache.ComputeIfAbsentAsync(track.VideoId, FetchSongDuration);
			float effectiveDuration = duration;
			if (track.StartTimestamp != null)
			{
				float startTime = (float)ParseTimestamp(track.StartTimestamp).TotalSeconds;
				if (duration <= startTime)
				{
					return 0f;
				}
				effectiveDuration -= startTime;
			}
			if (track.EndTimestamp != null)
			{
				float endTime = (float)ParseTimestamp(track.EndTimestamp).TotalSeconds;
				if (duration <= endTime)
				{
					return effectiveDuration;
				}
				effectiveDuration -= duration - endTime;
			}
			return effectiveDuration;
		}

		public static async Task DownloadTrack(ConfiguredTrack track)
		{
			if (track.VideoId == null)
			{
				throw new NullReferenceException("Track VideoId must not be null.");
			}
			if (track.TrackName == null)
			{
				throw new NullReferenceException("Track TrackName must not be null.");
			}
			ManualLogSource? logger = Plugin.Logger;
			if (logger != null)
			{
				logger.LogDebug((object)("Downloading '" + track.TrackName + "' (" + track.VideoId + ")"));
			}
			string newPath = Path.Combine(Plugin.DownloadsPath, "cytbb." + track.TrackName + "-" + track.VideoId + ".mp3");
			if (File.Exists(newPath))
			{
				ManualLogSource? logger2 = Plugin.Logger;
				if (logger2 != null)
				{
					logger2.LogDebug((object)"Track already downloaded, nothing to do.");
				}
				return;
			}
			if (await GetEffectiveDuration(track) > Plugin.MaxSongDuration.Value)
			{
				throw new VideoTooLongException("Track too long, skipping.");
			}
			string?[] downloaderArgs = new string[4]
			{
				"ffmpeg:-nostats",
				"ffmpeg:-loglevel 0",
				(track.StartTimestamp != null) ? ("ffmpeg:-ss " + track.StartTimestamp) : null,
				(track.EndTimestamp != null) ? ("ffmpeg:-to " + track.EndTimestamp) : null
			};
			string?[] extractAudioFilterArgs = new string[1] { track.VolumeScalar.HasValue ? $"volume={track.VolumeScalar}" : null };
			extractAudioFilterArgs = extractAudioFilterArgs.Where((string x) => x != null).Cast<string>().ToArray();
			string?[] postProcessorArgs = new string[1] { (extractAudioFilterArgs.Length != 0) ? ("ExtractAudio:-filter:a " + string.Join(":", extractAudioFilterArgs)) : null };
			ManualLogSource? logger3 = Plugin.Logger;
			if (logger3 != null)
			{
				logger3.LogDebug((object)("Starting download (" + track.TrackName + ")."));
			}
			YoutubeDL youtubeDL = Plugin.YoutubeDL;
			string? videoId = track.VideoId;
			OptionSet val = new OptionSet();
			val.Downloader = MultiValue<string>.op_Implicit(new string[1] { "ffmpeg" });
			val.DownloaderArgs = MultiValue<string>.op_Implicit(downloaderArgs.Where((string x) => x != null).Cast<string>().ToArray());
			val.PostprocessorArgs = MultiValue<string>.op_Implicit(postProcessorArgs.Where((string x) => x != null).Cast<string>().ToArray());
			OptionSet val2 = val;
			RunResult<string> res = await youtubeDL.RunAudioDownload(videoId, (AudioConversionFormat)3, default(CancellationToken), (IProgress<DownloadProgress>)null, (IProgress<string>)null, val2);
			ManualLogSource? logger4 = Plugin.Logger;
			if (logger4 != null)
			{
				logger4.LogDebug((object)("Download complete (" + track.TrackName + ")."));
			}
			if (!res.Success)
			{
				throw new YoutubeDLProcessFailed("Failed to download '" + track.TrackName + "' (" + track.VideoId + ").");
			}
			File.Move(res.Data, newPath);
			ManualLogSource? logger5 = Plugin.Logger;
			if (logger5 != null)
			{
				logger5.LogDebug((object)("'" + track.TrackName + "' (" + track.VideoId + ") downloaded successfully."));
			}
		}
	}
	public static class InfoCache
	{
		public static readonly Dictionary<string, float> DurationCache = new Dictionary<string, float>();
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "com.github.lordfirespeed.configured_youtube_boombox";

		public const string PLUGIN_NAME = "Configured Youtube Boombox";

		public const string PLUGIN_VERSION = "0.3.1";
	}
}
namespace ConfiguredYoutubeBoombox.util
{
	public static class DictionaryComputeIfAbsent
	{
		public delegate TValue ValueProvider<TKey, TValue>(TKey key);

		public delegate Task<TValue> ValueProviderAsync<TKey, TValue>(TKey key);

		public static TValue ComputeIfAbsent<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, ValueProvider<TKey, TValue> provider)
		{
			if (dictionary.TryGetValue(key, out TValue value))
			{
				return value;
			}
			return dictionary[key] = provider(key);
		}

		public static async Task<TValue> ComputeIfAbsentAsync<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, ValueProviderAsync<TKey, TValue> provider)
		{
			if (dictionary.TryGetValue(key, out TValue value))
			{
				return value;
			}
			return dictionary[key] = await provider(key);
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}