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)
{
}
}
}