Merge pull request #162 from Assistant/OCI-overhaul

OCI overhaul
This commit is contained in:
Assistant 2020-05-19 11:59:00 -06:00 committed by GitHub
commit 16dd4ba72f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 349 additions and 104 deletions

View file

@ -23,6 +23,7 @@ namespace ModAssistant
public static bool ReinstallInstalledMods;
public static string Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
public static List<string> SavedMods = ModAssistant.Properties.Settings.Default.SavedMods.Split(',').ToList();
public static MainWindow window;
public static bool Update = true;
public static bool GUI = true;
@ -82,12 +83,12 @@ namespace ModAssistant
if (GUI)
{
MainWindow window = new MainWindow();
window = new MainWindow();
window.Show();
}
else
{
Application.Current.Shutdown();
//Application.Current.Shutdown();
}
}
@ -107,8 +108,6 @@ namespace ModAssistant
await OneClickInstaller.InstallAsset(args[1]);
}
Current.Shutdown();
Update = false;
GUI = false;
args = Shift(args, 2);

View file

@ -4,6 +4,7 @@ using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using static ModAssistant.Http;
@ -18,11 +19,13 @@ namespace ModAssistant.API
public static async Task<BeatSaverMap> GetFromKey(string Key, bool showNotification = true)
{
if (showNotification) OneClickInstaller.Status.Show();
return await GetMap(Key, "key", showNotification);
}
public static async Task<BeatSaverMap> GetFromHash(string Hash, bool showNotification = true)
{
if (showNotification) OneClickInstaller.Status.Show();
return await GetMap(Hash, "hash", showNotification);
}
@ -43,24 +46,34 @@ namespace ModAssistant.API
BeatSaverMap map = new BeatSaverMap();
map.Success = false;
if (showNotification) Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), id)}");
try
{
BeatSaverApiResponse beatsaver = await GetResponse(BeatSaverURLPrefix + urlSegment + id);
if (beatsaver != null && beatsaver.map != null)
{
map.response = beatsaver;
map.Name = await InstallMap(beatsaver.map, showNotification);
map.Success = true;
}
}
catch (Exception e)
{
ModAssistant.Utils.Log($"Failed downloading BeatSaver map: {id} | Error: {e}", "ERROR");
ModAssistant.Utils.Log($"Failed downloading BeatSaver map: {id} | Error: {e.Message}", "ERROR");
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), (map.Name ?? id))}");
}
return map;
}
private static async Task<BeatSaverApiResponse> GetResponse(string url, bool showNotification = true)
private static async Task<BeatSaverApiResponse> GetResponse(string url, bool showNotification = true, int retries = 3)
{
if (retries == 0)
{
ModAssistant.Utils.Log($"Max tries reached: Skipping {url}", "ERROR");
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitSkip"), url)}");
throw new Exception("Max retries allowed");
}
BeatSaverApiResponse response = new BeatSaverApiResponse();
try
{
@ -69,22 +82,21 @@ namespace ModAssistant.API
response.ratelimit = GetRatelimit(resp.Headers);
string body = await resp.Content.ReadAsStringAsync();
if ((int)resp.StatusCode == 429)
{
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), response.ratelimit.ResetTime)}");
await response.ratelimit.Wait();
return await GetResponse(url, showNotification, retries - 1);
}
if (response.statusCode == HttpStatusCode.OK)
{
if (response.ratelimit.IsSafe)
{
response.map = JsonSerializer.Deserialize<BeatSaverApiResponseMap>(body);
return response;
}
else
{
ModAssistant.Utils.Log($"Ratelimit: ({response.ratelimit.Remaining}/{response.ratelimit.Total}) {response.ratelimit.ResetTime}");
return response;
}
response.map = JsonSerializer.Deserialize<BeatSaverApiResponseMap>(body);
return response;
}
else
{
ModAssistant.Utils.Log($"Ratelimit: [{response.statusCode}]({response.ratelimit.Remaining}/{response.ratelimit.Total}) {response.ratelimit.ResetTime} \n{body}", "ERROR");
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), url)}");
return response;
}
}
@ -98,9 +110,96 @@ namespace ModAssistant.API
}
}
private static BeatSaverRatelimit GetRatelimit(HttpResponseHeaders headers)
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);
public static async Task<string> InstallMap(BeatSaverApiResponseMap Map, bool showNotification = true)
{
BeatSaverRatelimit ratelimit = new BeatSaverRatelimit();
string zip = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, Map.hash) + ".zip";
string mapName = string.Concat(($"{Map.key} ({Map.metadata.songName} - {Map.metadata.levelAuthorName})")
.Split(ModAssistant.Utils.Constants.IllegalCharacters));
string directory = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, mapName);
#pragma warning disable CS0162 // Unreachable code detected
if (BypassDownloadCounter)
{
await Utils.DownloadAsset(BeatSaverURLPrefix + Map.directDownload, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true);
}
else
{
await Utils.DownloadAsset(BeatSaverURLPrefix + Map.downloadURL, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true);
}
#pragma warning restore CS0162 // Unreachable code detected
if (File.Exists(zip))
{
byte[] zipMagicNumber = { 80, 75, 3, 4 };
byte[] magicNumber = new byte[4];
try
{
using (FileStream fs = new FileStream(zip, FileMode.Open, FileAccess.Read))
{
fs.Read(magicNumber, 0, magicNumber.Length);
fs.Close();
}
}
catch
{
return null;
}
if (!(magicNumber.Length == zipMagicNumber.Length && memcmp(magicNumber, zipMagicNumber, magicNumber.Length) == 0))
{
ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR");
throw new Exception("File not a zip.");
}
try
{
using (FileStream stream = new FileStream(zip, FileMode.Open))
using (ZipArchive archive = new ZipArchive(stream))
{
foreach (ZipArchiveEntry file in archive.Entries)
{
string fileDirectory = Path.GetDirectoryName(Path.Combine(directory, file.FullName));
if (!Directory.Exists(fileDirectory))
{
Directory.CreateDirectory(fileDirectory);
}
if (!string.IsNullOrEmpty(file.Name))
{
file.ExtractToFile(Path.Combine(directory, file.FullName), true);
}
}
}
}
catch (Exception e)
{
File.Delete(zip);
ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} | Error: {e} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR");
throw new Exception("File extraction failed.");
}
File.Delete(zip);
}
else
{
if (showNotification)
{
string line1 = (string)Application.Current.FindResource("OneClick:SongDownload:Failed");
string line2 = (string)Application.Current.FindResource("OneClick:SongDownload:NetworkIssues");
string title = (string)Application.Current.FindResource("OneClick:SongDownload:FailedTitle");
MessageBox.Show($"{line1}\n{line2}", title);
}
throw new Exception("Zip file not found.");
}
return mapName;
}
public static BeatSaver.BeatSaverRatelimit GetRatelimit(HttpResponseHeaders headers)
{
BeatSaver.BeatSaverRatelimit ratelimit = new BeatSaver.BeatSaverRatelimit();
if (headers.TryGetValues("Rate-Limit-Remaining", out IEnumerable<string> _remaining))
@ -138,58 +237,30 @@ namespace ModAssistant.API
return new DateTime(unixStart.Ticks + unixTimeStampInTicks, System.DateTimeKind.Utc);
}
public static async Task<string> InstallMap(BeatSaverApiResponseMap Map, bool showNotification = true)
public static async Task Download(string url, string output, int retries = 3)
{
string zip = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, Map.hash) + ".zip";
string mapName = string.Concat(($"{Map.key} ({Map.metadata.songName} - {Map.metadata.levelAuthorName})")
.Split(ModAssistant.Utils.Constants.IllegalCharacters));
string directory = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, mapName);
#pragma warning disable CS0162 // Unreachable code detected
if (BypassDownloadCounter)
if (retries == 0)
{
await Utils.DownloadAsset(BeatSaverURLPrefix + Map.directDownload, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification);
ModAssistant.Utils.Log($"Max tries reached: Couldn't download {url}", "ERROR");
throw new Exception("Max retries allowed");
}
else
{
await Utils.DownloadAsset(BeatSaverURLPrefix + Map.downloadURL, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification);
}
#pragma warning restore CS0162 // Unreachable code detected
if (File.Exists(zip))
{
using (FileStream stream = new FileStream(zip, FileMode.Open))
using (ZipArchive archive = new ZipArchive(stream))
{
foreach (ZipArchiveEntry file in archive.Entries)
{
string fileDirectory = Path.GetDirectoryName(Path.Combine(directory, file.FullName));
if (!Directory.Exists(fileDirectory))
{
Directory.CreateDirectory(fileDirectory);
}
var resp = await HttpClient.GetAsync(url);
if (!string.IsNullOrEmpty(file.Name))
{
file.ExtractToFile(Path.Combine(directory, file.FullName), true);
}
}
}
File.Delete(zip);
}
else
if ((int)resp.StatusCode == 429)
{
if (showNotification)
{
string line1 = (string)Application.Current.FindResource("OneClick:SongDownload:Failed");
string line2 = (string)Application.Current.FindResource("OneClick:SongDownload:NetworkIssues");
string title = (string)Application.Current.FindResource("OneClick:SongDownload:FailedTitle");
MessageBox.Show($"{line1}\n{line2}", title);
}
return null;
var ratelimit = new BeatSaver.BeatSaverRatelimit();
ratelimit = GetRatelimit(resp.Headers);
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), ratelimit.ResetTime)}");
await ratelimit.Wait();
await Download(url, output, retries - 1);
}
using (var stream = await resp.Content.ReadAsStreamAsync())
using (var fs = new FileStream(output, FileMode.OpenOrCreate, FileAccess.Write))
{
await stream.CopyToAsync(fs);
}
return mapName;
}
public class BeatSaverMap
@ -212,15 +283,6 @@ namespace ModAssistant.API
public int? Total { get; set; }
public int? Reset { get; set; }
public DateTime ResetTime { get; set; }
public bool IsSafe
{
get
{
if (Remaining > 3) return true;
else return false;
}
}
public async Task Wait()
{
await Task.Delay(new TimeSpan(ResetTime.Ticks - DateTime.Now.Ticks));

View file

@ -41,7 +41,7 @@ namespace ModAssistant.API
}
}
public static async Task DownloadFrom(string file, bool gui = false, System.Windows.Controls.ProgressBar progress = null)
public static async Task DownloadFrom(string file)
{
if (Path.Combine(BeatSaberPath, PlaylistsFolder) != Path.GetDirectoryName(file))
{
@ -52,15 +52,9 @@ namespace ModAssistant.API
int Errors = 0;
int Minimum = 0;
int Value = 0;
if (progress != null)
{
progress.Minimum = 0;
progress.Maximum = 1;
progress.Value = 0;
}
Playlist playlist = JsonSerializer.Deserialize<Playlist>(File.ReadAllText(file));
int Maximum = playlist.songs.Length;
if (progress != null) progress.Maximum = playlist.songs.Length;
foreach (Playlist.Song song in playlist.songs)
{
@ -74,27 +68,20 @@ namespace ModAssistant.API
response = await BeatSaver.GetFromKey(song.key, false);
}
Value++;
if (progress != null) progress.Value++;
if (gui)
if (response.Success)
{
if (response.Success)
{
MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))}";
}
else
{
MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}";
ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash}");
await Task.Delay(3 * 1000);
Errors++;
}
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))} {response.Name}");
}
else
{
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}");
ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash} | ({response?.response?.ratelimit?.Remaining})");
await Task.Delay(3 * 1000);
Errors++;
}
}
if (gui)
{
MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}";
}
Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}");
}
private static string TextProgress(int min, int max, int value)

View file

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Windows;
using static ModAssistant.Http;
namespace ModAssistant.API
{
@ -11,6 +14,18 @@ namespace ModAssistant.API
{
public static readonly string BeatSaberPath = App.BeatSaberInstallDirectory;
public static void SetMessage(string message)
{
if (App.window == null)
{
OneClickStatus.Instance.MainText = message;
}
else
{
MainWindow.Instance.MainText = message;
}
}
public static async Task DownloadAsset(string link, string folder, bool showNotifcation, string fileName = null)
{
await DownloadAsset(link, folder, fileName, null, showNotifcation);
@ -21,7 +36,7 @@ namespace ModAssistant.API
await DownloadAsset(link, folder, fileName, displayName, true);
}
public static async Task DownloadAsset(string link, string folder, string fileName, string displayName, bool showNotification)
public static async Task DownloadAsset(string link, string folder, string fileName, string displayName, bool showNotification, bool beatsaver = false)
{
if (string.IsNullOrEmpty(BeatSaberPath))
{
@ -43,17 +58,18 @@ namespace ModAssistant.API
displayName = Path.GetFileNameWithoutExtension(fileName);
}
await ModAssistant.Utils.Download(link, fileName);
if (beatsaver) await BeatSaver.Download(link, fileName);
else await ModAssistant.Utils.Download(link, fileName);
if (showNotification)
{
ModAssistant.Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName));
SetMessage(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName));
}
}
catch
{
ModAssistant.Utils.SendNotify((string)Application.Current.FindResource("OneClick:AssetInstallFailed"));
SetMessage((string)Application.Current.FindResource("OneClick:AssetInstallFailed"));
}
}
}
}

View file

@ -9,6 +9,7 @@ namespace ModAssistant
class OneClickInstaller
{
private static readonly string[] Protocols = new[] { "modelsaber", "beatsaver", "bsplaylist" };
public static OneClickStatus Status = new OneClickStatus();
public static async Task InstallAsset(string link)
{
@ -37,11 +38,14 @@ namespace ModAssistant
private static async Task ModelSaber(Uri uri)
{
Status.Show();
API.Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), System.Web.HttpUtility.UrlDecode(uri.Segments.Last()))}");
await API.ModelSaber.GetModel(uri);
}
private static async Task Playlist(Uri uri)
{
Status.Show();
await API.Playlists.DownloadAll(uri);
}

View file

@ -158,6 +158,10 @@
<sys:String x:Key="OneClick:AssetInstallFailed">OneClick:AssetInstallFailed</sys:String>
<sys:String x:Key="OneClick:ProtocolHandler:Registered">{0} OneClick:ProtocolHandler:Registered</sys:String>
<sys:String x:Key="OneClick:ProtocolHandler:Unregistered">{0} OneClick:ProtocolHandler:Unregistered</sys:String>
<sys:String x:Key="OneClick:Installing">{0} OneClick:Installing</sys:String>
<sys:String x:Key="OneClick:RatelimitSkip">{0} OneClick:RatelimitSkip</sys:String>
<sys:String x:Key="OneClick:RatelimitHit">{0} OneClick:RatelimitHit</sys:String>
<sys:String x:Key="OneClick:Failed">{0} OneClick:Failed</sys:String>
<!-- Themes Class -->
<sys:String x:Key="Themes:ThemeNotFound">Themes:ThemeNotFound</sys:String>

View file

@ -217,6 +217,10 @@
<sys:String x:Key="OneClick:AssetInstallFailed">Failed to install.</sys:String>
<sys:String x:Key="OneClick:ProtocolHandler:Registered">{0} OneClick™ Install handlers registered!</sys:String>
<sys:String x:Key="OneClick:ProtocolHandler:Unregistered">{0} OneClick™ Install handlers unregistered!</sys:String>
<sys:String x:Key="OneClick:Installing">Installing: {0}</sys:String>
<sys:String x:Key="OneClick:RatelimitSkip">Max tries reached: Skipping {0}</sys:String>
<sys:String x:Key="OneClick:RatelimitHit">Ratelimit hit. Resuming in {0}</sys:String>
<sys:String x:Key="OneClick:Failed">Download failed: {0}</sys:String>
<!-- Themes Class -->
<sys:String x:Key="Themes:ThemeNotFound">Theme not found, reverting to default theme...</sys:String>

View file

@ -78,6 +78,9 @@
<Compile Include="Classes\Updater.cs" />
<Compile Include="Libs\semver\SemVersion.cs" />
<Compile Include="Libs\semver\IntExtensions.cs" />
<Compile Include="OneClickStatus.xaml.cs">
<DependentUpon>OneClickStatus.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\Intro.xaml.cs">
<DependentUpon>Intro.xaml</DependentUpon>
</Compile>
@ -131,6 +134,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="OneClickStatus.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Pages\Intro.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View file

@ -0,0 +1,98 @@
<Window x:Class="ModAssistant.OneClickStatus"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ModAssistant"
mc:Ignorable="d"
Title="OneClick Installer" Height="800" Width="600" WindowStyle="ToolWindow" ResizeMode="NoResize">
<Window.Resources>
<local:DivideDoubleByTwoConverter x:Key="DivideDoubleByTwoConverter" />
<Style x:Key="Spin" TargetType="{x:Type Image}">
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="0" CenterX="{Binding Path=ActualWidth, Converter={StaticResource DivideDoubleByTwoConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Image}}" CenterY="{Binding Path=ActualHeight, Converter={StaticResource DivideDoubleByTwoConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Image}}" />
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard x:Name="RotateStarCompass">
<DoubleAnimation
AutoReverse="False"
RepeatBehavior="Forever"
Storyboard.TargetProperty="RenderTransform.Angle"
From="0"
To="360"
Duration="0:0:3" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Fill="{DynamicResource ModAssistantBackground}" Grid.RowSpan="2"/>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Margin="60,0"
VerticalAlignment="Center"
Source="{DynamicResource loadingInnerDrawingImage}"
Stretch="Uniform" />
<Image
Grid.Row="0"
Margin="60,0"
VerticalAlignment="Center"
Source="{DynamicResource loadingMiddleDrawingImage}"
Stretch="Uniform" />
<Image
Grid.Row="0"
Margin="60,0"
VerticalAlignment="Center"
Source="{DynamicResource loadingOuterDrawingImage}"
Stretch="Uniform"
Style="{StaticResource Spin}" />
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="10,0,10,10"
BorderBrush="{DynamicResource BottomStatusBarOutline}"
BorderThickness="1">
<ScrollViewer
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
Background="{DynamicResource BottomStatusBarBackground}"
Margin="0">
<TextBox
Name="HistoryTextBlock"
Margin="0"
Padding="5"
Background="{DynamicResource BottomStatusBarBackground}"
BorderThickness="0"
Foreground="{DynamicResource TextColor}" />
</ScrollViewer>
</Border>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,64 @@
using System;
using System.Windows;
using System.Windows.Data;
namespace ModAssistant
{
/// <summary>
/// Interaction logic for OneClickStatus.xaml
/// </summary>
public partial class OneClickStatus : Window
{
public static OneClickStatus Instance;
public string HistoryText
{
get
{
return HistoryTextBlock.Text;
}
set
{
Dispatcher.Invoke(new Action(() => { OneClickStatus.Instance.HistoryTextBlock.Text = value; }));
}
}
public string MainText
{
get
{
return HistoryTextBlock.Text;
}
set
{
Dispatcher.Invoke(new Action(() => {
OneClickStatus.Instance.HistoryTextBlock.Text = string.IsNullOrEmpty(MainText) ? $"{value}" : $"{value}\n{MainText}";
}));
}
}
public OneClickStatus()
{
InitializeComponent();
Instance = this;
}
}
[ValueConversion(typeof(double), typeof(double))]
public class DivideDoubleByTwoConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(double))
{
throw new InvalidOperationException("The target must be a double");
}
double d = (double)value;
return ((double)d) / 2;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View file

@ -325,7 +325,7 @@ namespace ModAssistant.Pages
string playlistFile = Utils.GetManualFile();
if (File.Exists(playlistFile))
{
Task.Run(() => { API.Playlists.DownloadFrom(playlistFile, true).Wait(); });
Task.Run(() => { API.Playlists.DownloadFrom(playlistFile).Wait(); });
}
}
}