private void WebClientOnDownloadFileCompleted(object sender, AsyncCompletedEventArgs asyncCompletedEventArgs)
        {
            if (asyncCompletedEventArgs.Cancelled)
            {
                if (Exists())
                {
                    Delete();
                }
                Cancelled.Handle(h => h(this));
                return;
            }

            if (asyncCompletedEventArgs.Error != null)
            {
                DownloadFailed.Handle(h => h(this, asyncCompletedEventArgs.Error));

                Trace.Write(asyncCompletedEventArgs.Error);
                return;
            }


            if (Exists())
            {
                Downloaded.Handle(h => h(this));
            }
        }
Ejemplo n.º 2
0
        private void WebClientOnDownloadFileCompleted(object sender, AsyncCompletedEventArgs asyncCompletedEventArgs)
        {
            if (asyncCompletedEventArgs.Cancelled)
            {
                if (Exists())
                {
                    Delete();
                }
                Cancelled?.Invoke(this, new EventArgs());
                return;
            }

            if (asyncCompletedEventArgs.Error != null)
            {
                DownloadFailed?.Invoke(this, new DownloadFailEvent(asyncCompletedEventArgs.Error));

                AppLogger.Log.Error("Problem downloading file ", asyncCompletedEventArgs.Error);
                return;
            }


            if (Exists())
            {
                Downloaded?.Invoke(this, new EventArgs());
            }
        }
Ejemplo n.º 3
0
        public async Task <string[]> Download(params string[] uris)
        {
            if (Downloading)
            {
                throw new NotSupportedException("This downloader is already downloading!");
            }

            Downloading = true;
            var files = new List <string>();

            try {
                DownloadStarted?.Invoke(this, EventArgs.Empty);
                foreach (var uri in uris)
                {
                    files.Add(await DownloadFile(uri));
                }
                DownloadCompleted?.Invoke(this, EventArgs.Empty);
            } catch {
                files.Clear();
                DownloadFailed?.Invoke(this, EventArgs.Empty);
            }
            Downloading = false;

            return(files.ToArray());
        }
        public override void ResponseReceived(byte[] parameter)
        {
            switch ((FileExplorerCommunication)parameter[0])
            {
            case FileExplorerCommunication.ResponseDtpPackage:
                _dtpFactory.Receive(parameter, 1);
                break;

            case FileExplorerCommunication.ResponseDownloadPackage:
                DownloadPackageReceived?.Invoke(this, parameter);
                break;

            case FileExplorerCommunication.ResponsePackagingDirectory:
                break;

            case FileExplorerCommunication.ResponseCopyingFile:
                break;

            case FileExplorerCommunication.ResponseProcessingEntryChanged:
                ProcessingEntryUpdateReceived?.Invoke(this,
                                                      _processingEntryUpdateSerializer.Value.Deserialize <ProcessingEntryUpdate>(parameter, 1));
                break;

            case FileExplorerCommunication.ResponseDownloadFailed:
                DownloadFailed?.Invoke(this, new Guid(parameter.Skip(1).ToArray()));
                break;

            case FileExplorerCommunication.ResponseProcessingEntryAdded:
                ProcessingEntryAdded?.Invoke(this,
                                             _processingEntrySerializer.Value.Deserialize <ProcessingEntry>(parameter, 1).Unpack(null));
                break;
            }
        }
Ejemplo n.º 5
0
 internal void StartDownload(IEnumerable <DependencyObject> download, DownloadStart downloadStart = null,
                             DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null,
                             InstallFailed installFail         = null, InstallFinish installFinish = null)
 {
     foreach (var item in download)
     {
         StartCoroutine(UpdateModCoroutine(item, downloadStart, downloadProgress, downloadFail, downloadFinish, installFail, installFinish));
     }
 }
Ejemplo n.º 6
0
        private void RunTask(BeatmapDownloadTask task)
        {
            Log.Verbose("Detect {name} {id} downloading",
                        task.IsBeatmapSet ? "beatmapset" : "beatmap",
                        task.Id);

            IBeatmapInfo beatmapInfo = null;

            try
            {
                Task <IBeatmapInfo> infoTask = task.IsBeatmapSet ?
                                               _beatmapProvider.LookupBySetIdAsync(task.Id) :
                                               _beatmapProvider.LookupByIdAsync(task.Id);

                beatmapInfo = infoTask.Result ?? throw new BeatmapNotFoundException(task.Id);

                DownloadStarted?.Invoke(this, new BeatmapDownloadEventArgs(beatmapInfo));

                var option = new BeatmapDownloadOption();

                if (DownloadProgressChanged != null)
                {
                    option.Progress = new PropagateHandler(this, beatmapInfo);
                }

                var result = _beatmapProvider.DownloadAsync(beatmapInfo, option).Result;

                if (result.Exception != null)
                {
                    throw result.Exception;
                }

                if (File.Exists(_process.MainModule?.FileName))
                {
                    Process.Start(_process.MainModule !.FileName, result.FilePath);
                }

                DownloadCompleted?.Invoke(this, new BeatmapDownloadEventArgs(beatmapInfo));
            }
            catch (Exception e)
            {
                DownloadFailed?.Invoke(this, new BeatmapDownloadFailedEventArgs(beatmapInfo, e));

                var fallbackUrl = task.IsBeatmapSet ?
                                  $"https://osu.ppy.sh/beatmapsets/{task.Id}" :
                                  $"https://osu.ppy.sh/b/{task.Id}";

                Process.Start(fallbackUrl);
            }
        }
Ejemplo n.º 7
0
        private void SetRenderImageState()
        {
            if (this.renderImageState == renderResource.State)
            {
                return;
            }

            RenderImageState oldRenderImageState = this.renderImageState;

            this.renderImageState = renderResource.State;

            if (oldRenderImageState != RenderImageState.Idle && oldRenderImageState != RenderImageState.DownloadProgress || renderImageState == RenderImageState.Idle)
            {
                throw new Granular.Exception("Can't change BitmapSource.RenderImageState from \"{0}\" to \"{1}\"", oldRenderImageState, renderImageState);
            }

            switch (renderImageState)
            {
            case RenderImageState.DownloadProgress:
                IsDownloading = true;
                DownloadProgress.Raise(this);
                break;

            case RenderImageState.DownloadCompleted:
                IsDownloading = false;
                DownloadCompleted.Raise(this);
                break;

            case RenderImageState.DownloadFailed:
                IsDownloading = false;
                DownloadFailed.Raise(this);
                break;

            default: throw new Granular.Exception("Unexpected DownloadState \"{0}\"", renderImageState);
            }
        }
        public async void ReceiveData(byte[] data)
        {
            _fileStream.Write(data, 0, data.Length);
            if (_fileStream.Length == TotalBytesToReceive)
            {
                if (_fileHash == null)
                {
                    await Task.Run(() => _autoResetEvent.WaitOne());
                }

                _fileStream.Position = 0;
                byte[] fileHash;
                using (var sha256 = new SHA256Managed())
                    fileHash = sha256.ComputeHash(_fileStream);

                _fileStream.Close();
                _fileStream = null;

                if (fileHash.SequenceEqual(_fileHash))
                {
                    ViewData.DataManagerType.ModifyDownloadedFile(FileName);
                    DownloadFinished?.Invoke(this, EventArgs.Empty);
                }
                else
                {
                    File.Delete(FileName);
                    DownloadFailed?.Invoke(this, null);
                }
                IsFinished = true;
                return;
            }

            BytesReceived = _fileStream.Length;
            Progress      = BytesReceived / TotalBytesToReceive;
            ProgressChanged?.Invoke(this, EventArgs.Empty);
        }
Ejemplo n.º 9
0
 /// <summary>
 /// Event invocator for the <see cref="DownloadFailed"/> event
 /// </summary>
 /// <param name="e">Event arguments for the <see cref="DownloadFailed"/> event</param>
 protected virtual void OnDownloadFailed(DownloadFailedEventArgs e)
 {
     DownloadFailed?.Invoke(this, e);
 }
Ejemplo n.º 10
0
        /// <summary>
        /// Begin a download for the requested <typeparamref name="TModel"/>.
        /// </summary>
        /// <param name="model">The <typeparamref name="TModel"/> to be downloaded.</param>
        /// <param name="minimiseDownloadSize">Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle.</param>
        /// <returns>Whether the download was started.</returns>
        public bool Download(TModel model, bool minimiseDownloadSize = false)
        {
            if (!canDownload(model))
            {
                return(false);
            }

            var request = CreateDownloadRequest(model, minimiseDownloadSize);

            DownloadNotification notification = new DownloadNotification
            {
                Text = $"Downloading {request.Model}",
            };

            request.DownloadProgressed += progress =>
            {
                notification.State    = ProgressNotificationState.Active;
                notification.Progress = progress;
            };

            request.Success += filename =>
            {
                Task.Factory.StartNew(async() =>
                {
                    // This gets scheduled back to the update thread, but we want the import to run in the background.
                    var imported = await Import(notification, filename);

                    // for now a failed import will be marked as a failed download for simplicity.
                    if (!imported.Any())
                    {
                        DownloadFailed?.Invoke(request);
                    }

                    currentDownloads.Remove(request);
                }, TaskCreationOptions.LongRunning);
            };

            request.Failure += triggerFailure;

            notification.CancelRequested += () =>
            {
                request.Cancel();
                currentDownloads.Remove(request);
                notification.State = ProgressNotificationState.Cancelled;
                return(true);
            };

            currentDownloads.Add(request);
            PostNotification?.Invoke(notification);

            api.PerformAsync(request);

            DownloadBegan?.Invoke(request);
            return(true);

            void triggerFailure(Exception error)
            {
                DownloadFailed?.Invoke(request);

                if (error is OperationCanceledException)
                {
                    return;
                }

                notification.State = ProgressNotificationState.Cancelled;
                Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
                currentDownloads.Remove(request);
            }
        }
Ejemplo n.º 11
0
        /// <summary>
        ///     Handles incoming messages.
        /// </summary>
        /// <param name="sender">The <see cref="IMessageConnection"/> instance from which the message originated.</param>
        /// <param name="message">The message.</param>
        public async void HandleMessageRead(object sender, byte[] message)
        {
            var connection = (IMessageConnection)sender;
            var code       = new MessageReader <MessageCode.Peer>(message).ReadCode();

            Diagnostic.Debug($"Peer message received: {code} from {connection.Username} ({connection.IPEndPoint}) (id: {connection.Id})");

            try
            {
                switch (code)
                {
                case MessageCode.Peer.SearchResponse:
                    var searchResponse = SearchResponseFactory.FromByteArray(message);

                    if (SoulseekClient.Searches.TryGetValue(searchResponse.Token, out var search))
                    {
                        search.TryAddResponse(searchResponse);
                    }

                    break;

                case MessageCode.Peer.BrowseResponse:
                    var browseWaitKey = new WaitKey(MessageCode.Peer.BrowseResponse, connection.Username);

                    try
                    {
                        SoulseekClient.Waiter.Complete(browseWaitKey, BrowseResponseFactory.FromByteArray(message));
                    }
                    catch (Exception ex)
                    {
                        SoulseekClient.Waiter.Throw(browseWaitKey, new MessageReadException("The peer returned an invalid browse response", ex));
                        throw;
                    }

                    break;

                case MessageCode.Peer.InfoRequest:
                    UserInfo outgoingInfo;

                    try
                    {
                        outgoingInfo = await SoulseekClient.Options
                                       .UserInfoResolver(connection.Username, connection.IPEndPoint).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        outgoingInfo = await new SoulseekClientOptions()
                                       .UserInfoResolver(connection.Username, connection.IPEndPoint).ConfigureAwait(false);

                        Diagnostic.Warning($"Failed to resolve user info response: {ex.Message}", ex);
                    }

                    await connection.WriteAsync(outgoingInfo.ToByteArray()).ConfigureAwait(false);

                    break;

                case MessageCode.Peer.SearchRequest:
                    var searchRequest = PeerSearchRequest.FromByteArray(message);

                    if (SoulseekClient.Options.SearchResponseResolver == default)
                    {
                        break;
                    }

                    try
                    {
                        var peerSearchResponse = await SoulseekClient.Options.SearchResponseResolver(connection.Username, searchRequest.Token, SearchQuery.FromText(searchRequest.Query)).ConfigureAwait(false);

                        if (peerSearchResponse != null && peerSearchResponse.FileCount + peerSearchResponse.LockedFileCount > 0)
                        {
                            await connection.WriteAsync(peerSearchResponse.ToByteArray()).ConfigureAwait(false);
                        }
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Warning($"Error resolving search response for query '{searchRequest.Query}' requested by {connection.Username} with token {searchRequest.Token}: {ex.Message}", ex);
                    }

                    break;

                case MessageCode.Peer.BrowseRequest:
                    BrowseResponse browseResponse;

                    try
                    {
                        browseResponse = await SoulseekClient.Options.BrowseResponseResolver(connection.Username, connection.IPEndPoint).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        browseResponse = await new SoulseekClientOptions()
                                         .BrowseResponseResolver(connection.Username, connection.IPEndPoint).ConfigureAwait(false);

                        Diagnostic.Warning($"Failed to resolve browse response: {ex.Message}", ex);
                    }

                    await connection.WriteAsync(browseResponse.ToByteArray()).ConfigureAwait(false);

                    break;

                case MessageCode.Peer.FolderContentsRequest:
                    var       folderContentsRequest  = FolderContentsRequest.FromByteArray(message);
                    Directory outgoingFolderContents = null;

                    try
                    {
                        outgoingFolderContents = await SoulseekClient.Options.DirectoryContentsResolver(
                            connection.Username,
                            connection.IPEndPoint,
                            folderContentsRequest.Token,
                            folderContentsRequest.DirectoryName).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Warning($"Failed to resolve directory contents response: {ex.Message}", ex);
                    }

                    if (outgoingFolderContents != null)
                    {
                        var folderContentsResponseMessage = new FolderContentsResponse(folderContentsRequest.Token, outgoingFolderContents);

                        await connection.WriteAsync(folderContentsResponseMessage).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Peer.FolderContentsResponse:
                    var folderContentsResponse = FolderContentsResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(MessageCode.Peer.FolderContentsResponse, connection.Username, folderContentsResponse.Token), folderContentsResponse.Directory);
                    break;

                case MessageCode.Peer.InfoResponse:
                    var incomingInfo = UserInfoResponseFactory.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(MessageCode.Peer.InfoResponse, connection.Username), incomingInfo);
                    break;

                case MessageCode.Peer.TransferResponse:
                    var transferResponse = TransferResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(MessageCode.Peer.TransferResponse, connection.Username, transferResponse.Token), transferResponse);
                    break;

                case MessageCode.Peer.QueueDownload:
                    var queueDownloadRequest = QueueDownloadRequest.FromByteArray(message);

                    var(queueRejected, queueRejectionMessage) =
                        await TryEnqueueDownloadAsync(connection.Username, connection.IPEndPoint, queueDownloadRequest.Filename).ConfigureAwait(false);

                    if (queueRejected)
                    {
                        await connection.WriteAsync(new UploadDenied(queueDownloadRequest.Filename, queueRejectionMessage)).ConfigureAwait(false);
                    }
                    else
                    {
                        await TrySendPlaceInQueueAsync(connection, queueDownloadRequest.Filename).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Peer.TransferRequest:
                    var transferRequest = TransferRequest.FromByteArray(message);

                    if (transferRequest.Direction == TransferDirection.Upload)
                    {
                        if (!SoulseekClient.DownloadDictionary.IsEmpty && SoulseekClient.DownloadDictionary.Values.Any(d => d.Username == connection.Username && d.Filename == transferRequest.Filename))
                        {
                            SoulseekClient.Waiter.Complete(new WaitKey(MessageCode.Peer.TransferRequest, connection.Username, transferRequest.Filename), transferRequest);
                        }
                        else
                        {
                            // reject the transfer with an empty reason.  it was probably cancelled, but we can't be sure.
                            Diagnostic.Debug($"Rejecting unknown upload from {connection.Username} for {transferRequest.Filename} with token {transferRequest.Token}");
                            await connection.WriteAsync(new TransferResponse(transferRequest.Token, "Cancelled")).ConfigureAwait(false);
                        }
                    }
                    else
                    {
                        var(transferRejected, transferRejectionMessage) = await TryEnqueueDownloadAsync(connection.Username, connection.IPEndPoint, transferRequest.Filename).ConfigureAwait(false);

                        if (transferRejected)
                        {
                            await connection.WriteAsync(new TransferResponse(transferRequest.Token, transferRejectionMessage)).ConfigureAwait(false);

                            await connection.WriteAsync(new UploadDenied(transferRequest.Filename, transferRejectionMessage)).ConfigureAwait(false);
                        }
                        else
                        {
                            await connection.WriteAsync(new TransferResponse(transferRequest.Token, "Queued")).ConfigureAwait(false);
                            await TrySendPlaceInQueueAsync(connection, transferRequest.Filename).ConfigureAwait(false);
                        }
                    }

                    break;

                case MessageCode.Peer.UploadDenied:
                    var uploadDeniedResponse = UploadDenied.FromByteArray(message);

                    Diagnostic.Debug($"Download of {uploadDeniedResponse.Filename} from {connection.Username} was denied: {uploadDeniedResponse.Message}");
                    SoulseekClient.Waiter.Throw(new WaitKey(MessageCode.Peer.TransferRequest, connection.Username, uploadDeniedResponse.Filename), new TransferRejectedException(uploadDeniedResponse.Message));

                    DownloadDenied?.Invoke(this, new DownloadDeniedEventArgs(connection.Username, uploadDeniedResponse.Filename, uploadDeniedResponse.Message));
                    break;

                case MessageCode.Peer.PlaceInQueueResponse:
                    var placeInQueueResponse = PlaceInQueueResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(MessageCode.Peer.PlaceInQueueResponse, connection.Username, placeInQueueResponse.Filename), placeInQueueResponse);
                    break;

                case MessageCode.Peer.PlaceInQueueRequest:
                    var placeInQueueRequest = PlaceInQueueRequest.FromByteArray(message);
                    await TrySendPlaceInQueueAsync(connection, placeInQueueRequest.Filename).ConfigureAwait(false);

                    break;

                case MessageCode.Peer.UploadFailed:
                    var uploadFailedResponse = UploadFailed.FromByteArray(message);
                    var msg = $"Download of {uploadFailedResponse.Filename} reported as failed by {connection.Username}";

                    var download = SoulseekClient.DownloadDictionary.Values.FirstOrDefault(d => d.Username == connection.Username && d.Filename == uploadFailedResponse.Filename);
                    if (download != null)
                    {
                        SoulseekClient.Waiter.Throw(new WaitKey(MessageCode.Peer.TransferRequest, download.Username, download.Filename), new TransferException(msg));
                    }

                    Diagnostic.Debug(msg);

                    DownloadFailed?.Invoke(this, new DownloadFailedEventArgs(connection.Username, uploadFailedResponse.Filename));
                    break;

                default:
                    Diagnostic.Debug($"Unhandled peer message: {code} from {connection.Username} ({connection.IPEndPoint}); {message.Length} bytes");
                    break;
                }
            }
            catch (Exception ex)
            {
                Diagnostic.Warning($"Error handling peer message: {code} from {connection.Username} ({connection.IPEndPoint}); {ex.Message}", ex);
            }
        }
Ejemplo n.º 12
0
 /// <summary>
 /// Passes the internal event in the protocol handler to the outward-facing
 /// event.
 /// </summary>
 /// <param name="sender">Sender.</param>
 /// <param name="e">E.</param>
 private void OnModuleInstallationFailed(object sender, ModuleInstallationFailedArgs e)
 {
     DownloadFailed?.Invoke(sender, e);
 }
Ejemplo n.º 13
0
        private static IEnumerator UpdateModCoroutine(DependencyObject item, DownloadStart downloadStart,
                                                      DownloadProgress progress, DownloadFailed dlFail, DownloadFinish finish,
                                                      InstallFailed installFail, InstallFinish installFinish)
        { // (3.2)
            Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}");

            var mod = new Ref <ApiEndpoint.Mod>(null);

            yield return(GetModInfo(item.Name, item.ResolvedVersion.ToString(), mod));

            try { mod.Verify(); }
            catch (Exception e)
            {
                Logger.updater.Error($"Error occurred while trying to get information for {item}");
                Logger.updater.Error(e);
                yield break;
            }

            var releaseName = BeatSaber.ReleaseType == BeatSaber.Release.Steam
                ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus;
            var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName);

            string url = ApiEndpoint.BeatModBase + platformFile.Path;

            Logger.updater.Debug($"URL = {url}");

            const int maxTries = 3;
            int       tries    = maxTries;

            while (tries > 0)
            {
                if (tries-- != maxTries)
                {
                    Logger.updater.Debug("Re-trying download...");
                }

                using (var stream = new MemoryStream())
                    using (var request = UnityWebRequest.Get(url))
                        using (var taskTokenSource = new CancellationTokenSource())
                        {
                            var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress?.Invoke(item, i1, i2, d));
                            request.downloadHandler = dlh;

                            downloadStart?.Invoke(item);

                            Logger.updater.Debug("Sending request");
                            //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
                            yield return(request.SendWebRequest());

                            Logger.updater.Debug("Download finished");

                            if (request.isNetworkError)
                            {
                                Logger.updater.Error("Network error while trying to update mod");
                                Logger.updater.Error(request.error);
                                dlFail?.Invoke(item, request.error);
                                taskTokenSource.Cancel();
                                continue;
                            }
                            if (request.isHttpError)
                            {
                                Logger.updater.Error("Server returned an error code while trying to update mod");
                                Logger.updater.Error(request.error);
                                dlFail?.Invoke(item, request.error);
                                taskTokenSource.Cancel();
                                continue;
                            }

                            finish?.Invoke(item);

                            stream.Seek(0, SeekOrigin.Begin); // reset to beginning

                            var downloadTask = Task.Run(() =>
                            { // use slightly more multi threaded approach than co-routines
                                // ReSharper disable once AccessToDisposedClosure
                                ExtractPluginAsync(stream, item, platformFile);
                            }, taskTokenSource.Token);

                            while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
                            {
                                yield return(null); // pause co-routine until task is done
                            }
                            if (downloadTask.IsFaulted)
                            {
                                if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException))
                                { // any exception is an intercept exception
                                    Logger.updater.Error($"BeatMods did not return expected data for {item.Name}");
                                }

                                Logger.updater.Error($"Error downloading mod {item.Name}");
                                Logger.updater.Error(downloadTask.Exception);

                                installFail?.Invoke(item, downloadTask.Exception);
                                continue;
                            }

                            break;
                        }
            }

            if (tries == 0)
            {
                Logger.updater.Warn($"Plugin download failed {maxTries} times, not re-trying");

                installFinish?.Invoke(item, true);
            }
            else
            {
                Logger.updater.Debug("Download complete");
                installFinish?.Invoke(item, false);
            }
        }
Ejemplo n.º 14
0
 private static void HandleMessage(DownloadFailed<V2FeedPackage> message)
 {
     System.Console.WriteLine("Failed downloading " + message.Item.Id +
                              " error: " + message.Exception + ". Try: " + message.NumberOfRetries);
 }
Ejemplo n.º 15
0
        public static void DownloadAsync(string page, Dictionary<string, string> parameters, DownloadComplete onComplete, DownloadFailed onError)
        {
            WebClient client = new WebClient();

            client.DownloadStringCompleted += (sender, args) =>
            {
                if (args.Error != null)
                    onError(args.Error);
                else
                    onComplete(args.Result);
            };

            // Generate GET variables
            StringBuilder vars = new StringBuilder();
            foreach (var param in parameters)
                vars.Append(string.Format("{0}={1}&", param.Key, param.Value));

            Uri url = new Uri(baseUrl + page + "?" + vars.ToString());

            client.DownloadStringAsync(url);
        }