using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Web;
using BepInEx;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("onlystar")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.6.0")]
[assembly: AssemblyInformationalVersion("1.0.6+b87414552e5874a8f69eb175a28f6c8fe21b09d5")]
[assembly: AssemblyProduct("onlystar-ytModule")]
[assembly: AssemblyTitle("YTModule")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.6.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[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]
[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]
[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 BepInEx
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[Conditional("CodeGeneration")]
internal sealed class BepInAutoPluginAttribute : Attribute
{
public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null)
{
}
}
}
namespace BepInEx.Preloader.Core.Patching
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[Conditional("CodeGeneration")]
internal sealed class PatcherAutoPluginAttribute : Attribute
{
public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null)
{
}
}
}
namespace ytModule
{
[BepInPlugin("onlystar.ytModule", "ytModule", "1.0.6")]
public class ytModulePlugin : BaseUnityPlugin
{
public const string PluginGUID = "onlystar.ytModule";
public const string PluginName = "ytModule";
public const string PluginVersion = "1.0.6";
internal static ManualLogSource Logger;
private string ytDlpPath;
private string ffmpegPath;
public static bool Prod { get; set; } = true;
internal static ytModulePlugin Instance { get; private set; }
private void Awake()
{
Instance = this;
Logger = ((BaseUnityPlugin)this).Logger;
string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location);
ytDlpPath = Path.Combine(directoryName, "yt-dlp.exe");
ffmpegPath = Path.Combine(directoryName, "ffmpeg.exe");
string path = Path.Combine(directoryName, "deno.exe");
if (!File.Exists(ytDlpPath))
{
Logger.LogError((object)"[ytModule] yt-dlp.exe not found!");
}
if (!File.Exists(ffmpegPath))
{
Logger.LogError((object)"[ytModule] ffmpeg.exe not found!");
}
if (!File.Exists(path))
{
Logger.LogWarning((object)"[ytModule] deno.exe not found. Note that it is optional for now, but it might be a requirement in the future.");
}
string environmentVariable = Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("PATH", directoryName + Path.PathSeparator + environmentVariable);
Logger.LogInfo((object)"ytModule 1.0.6 loaded successfully!");
}
public static ytModuleAPI GetAPI()
{
if ((Object)(object)Instance == (Object)null)
{
ManualLogSource logger = Logger;
if (logger != null)
{
logger.LogError((object)"[ytModule] ytModule plugin not found!");
}
return null;
}
return new ytModuleAPI(Instance);
}
public static bool IsModuleReady()
{
if ((Object)(object)Instance == (Object)null)
{
return false;
}
string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)Instance).Info.Location);
string path = Path.Combine(directoryName, "yt-dlp.exe");
string path2 = Path.Combine(directoryName, "ffmpeg.exe");
string path3 = Path.Combine(directoryName, "deno.exe");
bool result = File.Exists(path) && File.Exists(path2);
if (!File.Exists(path3))
{
ManualLogSource logger = Logger;
if (logger != null)
{
logger.LogWarning((object)"[ytModule] Deno runtime not found. YouTube downloads may have an increased chance to fail.");
}
}
return result;
}
}
public class ytModuleAPI
{
public enum Platform
{
YouTube,
SoundCloud,
Vimeo,
Bandcamp,
Other
}
private readonly ytModulePlugin plugin;
private readonly string ytDlpPath;
private readonly string ffmpegPath;
private readonly string tempDir;
internal ytModuleAPI(ytModulePlugin plugin)
{
this.plugin = plugin;
string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)plugin).Info.Location);
ytDlpPath = Path.Combine(directoryName, "yt-dlp.exe");
ffmpegPath = Path.Combine(directoryName, "ffmpeg.exe");
tempDir = Path.Combine(Path.GetTempPath(), "sPEAKerYT");
Directory.CreateDirectory(tempDir);
}
public IEnumerator GetMetadata(string url, Action<VideoMetadata> onComplete, Action<string> onError = null)
{
List<string> args = BuildMetadataArgs(url);
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
try
{
VideoMetadata obj = ParseMetadata(output);
onComplete?.Invoke(obj);
}
catch (Exception ex)
{
onError?.Invoke("Metadata parsing failed: " + ex.Message);
}
}, onError));
}
public IEnumerator DownloadAudio(string url, string outputPath, DownloadOptions options, Action<DownloadResult> onComplete, Action<string> onError = null)
{
List<string> args = BuildDownloadArgs(url, outputPath, options);
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
DownloadResult downloadResult = new DownloadResult
{
FilePath = outputPath,
Success = File.Exists(outputPath + ".ogg")
};
try
{
downloadResult.Metadata = ParseMetadata(output);
}
catch (Exception ex)
{
ytModulePlugin.Logger.LogWarning((object)("[ytModule] Metadata parsing failed: " + ex.Message));
downloadResult.Metadata = null;
}
onComplete?.Invoke(downloadResult);
}, onError));
}
public IEnumerator DownloadAudioOnly(string url, string outputPath, string metadataPath, int maxSizeMb = 100, Action<string> onError = null)
{
DownloadOptions options = new DownloadOptions
{
AudioFormat = "vorbis",
Quality = 5,
MaxSizeMb = maxSizeMb
};
yield return ((MonoBehaviour)plugin).StartCoroutine(DownloadAudio(url, outputPath, options, delegate(DownloadResult result)
{
ytModulePlugin.Logger.LogInfo((object)$"[ytModule] Download complete - Success: {result.Success}, Metadata null: {result.Metadata == null}");
if (result.Metadata != null)
{
ytModulePlugin.Logger.LogInfo((object)("[ytModule] Metadata title: '" + result.Metadata.title + "'"));
}
if (result.Metadata != null)
{
SongMetadata songMetadata = new SongMetadata
{
title = (result.Metadata.title ?? "Unknown")
};
string text = JsonUtility.ToJson((object)songMetadata);
ytModulePlugin.Logger.LogInfo((object)("[ytModule] Writing metadata to: " + metadataPath + ", JSON: " + text));
File.WriteAllText(metadataPath, text);
}
else
{
ytModulePlugin.Logger.LogWarning((object)$"[ytModule] Metadata not written - Success: {result.Success}, Metadata: {result.Metadata != null}");
}
}, onError));
}
public IEnumerator DownloadVideo(string url, string outputPath, VideoDownloadOptions options, Action<DownloadResult> onComplete, Action<string> onError = null)
{
List<string> args = BuildVideoDownloadArgs(url, outputPath, options);
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
DownloadResult downloadResult = new DownloadResult
{
FilePath = outputPath,
Success = File.Exists(outputPath + "." + options.Container)
};
try
{
downloadResult.Metadata = ParseMetadata(output);
}
catch (Exception ex)
{
ytModulePlugin.Logger.LogWarning((object)("[ytModule] Metadata parsing failed: " + ex.Message));
downloadResult.Metadata = null;
}
onComplete?.Invoke(downloadResult);
}, onError));
}
public IEnumerator GetRelatedVideos(string sourceUrl, int maxCount = 10, Action<List<string>> onComplete = null, Action<string> onError = null, string playlistId = null)
{
List<PlaylistEntry> entries = null;
yield return ((MonoBehaviour)plugin).StartCoroutine(GetRelatedVideosWithMetadata(sourceUrl, maxCount, delegate(List<PlaylistEntry> result)
{
entries = result;
}, onError, playlistId));
if (entries == null)
{
yield break;
}
string sourceId = (string.IsNullOrEmpty(playlistId) ? GetVideoIdFromUrl(sourceUrl) : null);
List<string> urls = new List<string>(entries.Count);
foreach (PlaylistEntry e in entries)
{
if (sourceId == null || !(GetVideoIdFromUrl(e.url) == sourceId))
{
urls.Add(e.url);
}
}
onComplete?.Invoke(urls);
}
public IEnumerator GetRelatedVideosWithMetadata(string sourceUrl, int maxCount = 10, Action<List<PlaylistEntry>> onComplete = null, Action<string> onError = null, string playlistId = null)
{
string targetUrl;
if (!string.IsNullOrEmpty(playlistId))
{
targetUrl = "https://www.youtube.com/playlist?list=" + playlistId;
}
else
{
string videoId = GetVideoIdFromUrl(sourceUrl);
if (string.IsNullOrEmpty(videoId))
{
onError?.Invoke("Invalid YouTube URL");
yield break;
}
targetUrl = "https://www.youtube.com/watch?v=" + videoId + "&list=RD" + videoId;
}
List<string> args = new List<string> { "--extractor-args", "youtube:player_client=web,default", "--flat-playlist", "--dump-json", targetUrl };
AddCookieArgs(args);
if (string.IsNullOrEmpty(playlistId))
{
args.Add("--playlist-end");
args.Add(maxCount.ToString());
}
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
List<PlaylistEntry> list = new List<PlaylistEntry>();
string[] array = output.Split('\n');
string[] array2 = array;
foreach (string text in array2)
{
string text2 = text.Trim();
if (text2.StartsWith("{"))
{
try
{
PlaylistEntry playlistEntry = JsonUtility.FromJson<PlaylistEntry>(text2);
if (!string.IsNullOrEmpty(playlistEntry.url))
{
list.Add(playlistEntry);
}
}
catch
{
}
}
}
onComplete?.Invoke(list);
}, onError));
}
public IEnumerator GetPlaylistInfo(string playlistUrl, Action<PlaylistInfo> onComplete, Action<string> onError = null)
{
List<string> args = BuildPlaylistInfoArgs(playlistUrl);
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
try
{
PlaylistInfo obj = ParsePlaylistInfo(output);
onComplete(obj);
}
catch (Exception ex)
{
onError("Playlist parsing failed: " + ex.Message);
}
}, onError));
}
public IEnumerator SearchVideos(string query, Action<List<VideoMetadata>> onComplete, Action<string> onError = null, SearchOptions options = null)
{
if (options == null)
{
options = new SearchOptions();
}
List<string> args = BuildSearchArgs(query, options);
yield return ((MonoBehaviour)plugin).StartCoroutine(ExecuteYtDlp(args, delegate(string output)
{
try
{
List<VideoMetadata> obj = ParseSearchResults(output);
onComplete?.Invoke(obj);
}
catch (Exception ex)
{
onError?.Invoke("Search parsing failed: " + ex.Message);
}
}, onError));
}
private List<string> BuildMetadataArgs(string url)
{
List<string> list = new List<string>(2) { "--dump-json", "--no-download" };
AddPlatformSpecificArgs(list, url);
AddCookieArgs(list);
list.Add(url);
return list;
}
private List<string> BuildDownloadArgs(string url, string outputPath, DownloadOptions options)
{
List<string> list = new List<string>
{
"--no-playlist",
"--retries",
"3",
"--fragment-retries",
"3",
"--abort-on-unavailable-fragment",
"-x",
"--audio-format",
options.AudioFormat,
"--audio-quality",
options.Quality.ToString(),
"--ffmpeg-location",
ffmpegPath,
"--max-filesize",
$"{options.MaxSizeMb}M",
"--dump-json",
"--no-simulate",
"-o",
outputPath
};
AddPlatformSpecificArgs(list, url);
if (options.AudioFormat == "vorbis")
{
list.Add("--postprocessor-args");
list.Add("ffmpeg:-ar 44100 -ac 2");
}
AddCookieArgs(list);
list.Add(url);
return list;
}
private List<string> BuildVideoDownloadArgs(string url, string outputPath, VideoDownloadOptions options)
{
List<string> list = new List<string>
{
"--no-playlist",
"--retries",
"3",
"--fragment-retries",
"3",
"--abort-on-unavailable-fragment",
"--format",
options.FormatSelector,
"--merge-output-format",
options.Container,
"--max-filesize",
$"{options.MaxSizeMb}M",
"--dump-json",
"--no-simulate",
"-o",
outputPath
};
AddPlatformSpecificArgs(list, url);
if (!string.IsNullOrEmpty(ffmpegPath))
{
list.Add("--ffmpeg-location");
list.Add(ffmpegPath);
}
if (!string.IsNullOrEmpty(options.PostProcessorArgs))
{
list.Add("--postprocessor-args");
list.Add(options.PostProcessorArgs);
}
AddCookieArgs(list);
list.Add(url);
return list;
}
private List<string> BuildPlaylistInfoArgs(string playlistUrl)
{
List<string> list = new List<string> { "--flat-playlist", "--dump-json", "--compat-options", "no-youtube-unavailable-videos" };
AddPlatformSpecificArgs(list, playlistUrl);
AddCookieArgs(list);
list.Add(playlistUrl);
return list;
}
private List<string> BuildSearchArgs(string query, SearchOptions options)
{
string platform = options.Platform;
if (1 == 0)
{
}
string text = platform switch
{
"youtube" => "ytsearch",
"youtube-date" => "ytsearchdate",
"soundcloud" => "scsearch",
"bandcamp" => "bcsearch",
"vimeo" => "vmsearch",
_ => "ytsearch",
};
if (1 == 0)
{
}
string arg = text;
List<string> list = new List<string>
{
"--flat-playlist",
"-i",
"--dump-json",
$"{arg}{options.MaxResults}:{query}"
};
AddCookieArgs(list);
return list;
}
private IEnumerator ExecuteYtDlp(List<string> args, Action<string> onSuccess, Action<string> onError)
{
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = ytDlpPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
string instanceTempDir;
if (!ytModulePlugin.Prod)
{
instanceTempDir = Path.Combine(path2: $"ytdlp_instance_{Guid.NewGuid():N}", path1: tempDir);
Directory.CreateDirectory(instanceTempDir);
psi.WorkingDirectory = instanceTempDir;
psi.EnvironmentVariables["XDG_CACHE_HOME"] = instanceTempDir;
psi.EnvironmentVariables["TEMP"] = instanceTempDir;
psi.EnvironmentVariables["TMP"] = instanceTempDir;
ytModulePlugin.Logger.LogInfo((object)("[ytModule] DEV MODE: Started process in isolated dir: " + instanceTempDir));
}
else
{
instanceTempDir = tempDir;
psi.WorkingDirectory = tempDir;
ytModulePlugin.Logger.LogInfo((object)"[ytModule] Started process in shared temp dir");
}
foreach (string arg in args)
{
psi.ArgumentList.Add(arg);
}
Process proc = new Process
{
StartInfo = psi
};
StringBuilder output = new StringBuilder();
StringBuilder errors = new StringBuilder();
proc.OutputDataReceived += delegate(object _, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
output.AppendLine(e.Data);
}
};
proc.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
errors.AppendLine(e.Data);
ytModulePlugin.Logger.LogWarning((object)("[ytModule] yt-dlp: " + e.Data));
}
};
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
yield return (object)new WaitUntil((Func<bool>)(() => proc.HasExited));
if (!ytModulePlugin.Prod && instanceTempDir != tempDir)
{
try
{
if (Directory.Exists(instanceTempDir))
{
Directory.Delete(instanceTempDir, recursive: true);
}
}
catch
{
}
}
if (proc.ExitCode == 0)
{
onSuccess?.Invoke(output.ToString());
yield break;
}
string errorMsg = ((errors.Length > 0) ? errors.ToString() : "Unknown yt-dlp error");
onError?.Invoke(errorMsg);
}
private VideoMetadata ParseMetadata(string jsonOutput)
{
string[] array = jsonOutput.Split('\n');
string[] array2 = array;
foreach (string text in array2)
{
string text2 = text.Trim();
if (text2.StartsWith("{") && text2.EndsWith("}"))
{
return JsonUtility.FromJson<VideoMetadata>(text2);
}
}
throw new Exception("No valid JSON metadata found");
}
private PlaylistInfo ParsePlaylistInfo(string jsonOutput)
{
string[] array = jsonOutput.Split('\n');
List<string> list = new List<string>();
string[] array2 = array;
foreach (string text in array2)
{
if (!string.IsNullOrWhiteSpace(text))
{
list.Add(text);
}
}
if (list.Count == 0)
{
throw new Exception("No playlist data found");
}
PlaylistInfo playlistInfo = JsonUtility.FromJson<PlaylistInfo>(list[0]);
List<PlaylistEntry> list2 = new List<PlaylistEntry>();
foreach (string item in list)
{
string text2 = item.Trim();
if (!text2.StartsWith("{"))
{
continue;
}
try
{
PlaylistEntry playlistEntry = JsonUtility.FromJson<PlaylistEntry>(text2);
if (!string.IsNullOrEmpty(playlistEntry.url))
{
list2.Add(playlistEntry);
}
}
catch
{
}
}
playlistInfo.entries = list2.ToArray();
return playlistInfo;
}
private List<VideoMetadata> ParseSearchResults(string jsonOutput)
{
List<VideoMetadata> list = new List<VideoMetadata>();
string[] array = jsonOutput.Split('\n');
string[] array2 = array;
foreach (string text in array2)
{
string text2 = text.Trim();
if (text2.StartsWith("{") && text2.EndsWith("}"))
{
try
{
VideoMetadata item = JsonUtility.FromJson<VideoMetadata>(text2);
list.Add(item);
}
catch
{
}
}
}
return list;
}
public string GetVideoIdFromUrl(string url)
{
try
{
Uri uri = new Uri(url);
if (uri.Host.Contains("youtu.be"))
{
return uri.AbsolutePath.Trim('/');
}
if (uri.Host.Contains("youtube.com"))
{
return HttpUtility.ParseQueryString(uri.Query)["v"];
}
}
catch
{
}
using SHA1 sHA = SHA1.Create();
return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(url)), 0, 4).Replace("-", "");
}
public bool IsValidYouTubeUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result))
{
return false;
}
string text = result.Host.ToLowerInvariant();
return text.Contains("youtube.com") || text.Contains("youtu.be");
}
private void AddCookieArgs(List<string> args)
{
string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)plugin).Info.Location);
string text = FindCookiesFile(directoryName);
if (text != null)
{
ytModulePlugin.Logger.LogInfo((object)("[ytModule] Using cookies from " + Path.GetFileName(text)));
args.Add("--cookies");
args.Add(text);
}
}
private string FindCookiesFile(string directory)
{
if (!Directory.Exists(directory))
{
return null;
}
string text = Path.Combine(directory, "cookies.txt");
if (File.Exists(text))
{
return text;
}
string[] files = Directory.GetFiles(directory, "*cookies.txt", SearchOption.TopDirectoryOnly);
if (files.Length != 0)
{
if (files.Length > 1)
{
ytModulePlugin.Logger.LogWarning((object)$"[ytModule] Found {files.Length} cookie files, using: {Path.GetFileName(files[0])}");
}
return files[0];
}
return null;
}
public Platform DetectPlatform(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result))
{
return Platform.Other;
}
string text = result.Host.ToLowerInvariant();
if (text.Contains("youtube.com") || text.Contains("youtu.be"))
{
return Platform.YouTube;
}
if (text.Contains("soundcloud.com"))
{
return Platform.SoundCloud;
}
if (text.Contains("vimeo.com"))
{
return Platform.Vimeo;
}
if (text.Contains("bandcamp.com"))
{
return Platform.Bandcamp;
}
return Platform.Other;
}
private void AddPlatformSpecificArgs(List<string> args, string url)
{
if (DetectPlatform(url) == Platform.YouTube)
{
args.Add("--extractor-args");
args.Add("youtube:player_client=web,default");
}
}
}
public class SearchOptions
{
public int MaxResults = 10;
public string Platform = "youtube";
}
public class DownloadOptions
{
public string AudioFormat = "vorbis";
public int Quality = 5;
public int MaxSizeMb = 100;
public bool ExtractMetadata = true;
}
public class VideoDownloadOptions
{
public string FormatSelector = "bestvideo+bestaudio/best";
public string Container = "mp4";
public int MaxSizeMb = 500;
public string PostProcessorArgs = null;
}
public class DownloadResult
{
public string FilePath;
public VideoMetadata Metadata;
public bool Success;
public string ErrorMessage;
}
public class VideoMetadata
{
public string title;
public string id;
public string uploader;
public float duration;
public string webpage_url;
public string playlist;
public string playlist_title;
public int playlist_index;
}
public class PlaylistInfo
{
public string title;
public string id;
public string uploader;
public int playlist_count;
public PlaylistEntry[] entries;
}
public class PlaylistEntry
{
public string id;
public string title;
public string url;
public int playlist_index;
}
public class SongMetadata
{
public string title;
}
}
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class IgnoresAccessChecksToAttribute : Attribute
{
public IgnoresAccessChecksToAttribute(string assemblyName)
{
}
}
}