diff --git a/CHANGELOG.md b/CHANGELOG.md index cd15a2d9..b4a969dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.3.5 + +## Bug fixes + +* Fixed HTML characters not decoded in metadata. [#157](https://github.com/Otiel/BandcampDownloader/issues/157) + +## Improvements + +* Updated the following languages thanks to [contributors](https://github.com/Otiel/BandcampDownloader/pull/160): Italian. + # 1.3.4 ## Bug fixes diff --git a/src/BandcampDownloader/Helpers/BandcampHelper.cs b/src/BandcampDownloader/Helpers/BandcampHelper.cs index a59f3260..fdce6209 100644 --- a/src/BandcampDownloader/Helpers/BandcampHelper.cs +++ b/src/BandcampDownloader/Helpers/BandcampHelper.cs @@ -1,107 +1,106 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using HtmlAgilityPack; -using Newtonsoft.Json; - -namespace BandcampDownloader { - - internal static class BandcampHelper { - - /// - /// Retrieves the data on the album of the specified Bandcamp page. - /// - /// The HTML source code of a Bandcamp album page. - /// The data on the album of the specified Bandcamp page. - public static Album GetAlbum(string htmlCode) { - // Keep the interesting part of htmlCode only - string albumData; - try { - albumData = GetAlbumData(htmlCode); - } catch (Exception e) { - throw new Exception("Could not retrieve album data in HTML code.", e); - } - - // Fix some wrongly formatted JSON in source code - albumData = FixJson(albumData); - - // Deserialize JSON - Album album; - try { - var settings = new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore, - MissingMemberHandling = MissingMemberHandling.Ignore - }; - album = JsonConvert.DeserializeObject(albumData, settings).ToAlbum(); - } catch (Exception e) { - throw new Exception("Could not deserialize JSON data.", e); - } - - // Extract lyrics from album page - var htmlDoc = new HtmlDocument(); - htmlDoc.LoadHtml(htmlCode); - foreach (Track track in album.Tracks) { - HtmlNode lyricsElement = htmlDoc.GetElementbyId("_lyrics_" + track.Number); - if (lyricsElement != null) { - track.Lyrics = lyricsElement.InnerText.Trim(); - } - } - - return album; - } - - /// - /// Retrieves all the albums URL existing on the specified Bandcamp page. - /// - /// The HTML source code of a Bandcamp page. - /// The albums URL existing on the specified Bandcamp page. - public static List GetAlbumsUrl(string htmlCode, string artistPage) { - - // Get albums ("real" albums or track-only pages) relative urls - var regex = new Regex("href=\"(?/(album|track)/.*)\""); - if (!regex.IsMatch(htmlCode)) { - throw new NoAlbumFoundException(); - } - - var albumsUrl = new List(); - foreach (Match m in regex.Matches(htmlCode)) { - albumsUrl.Add(artistPage + m.Groups["url"].Value); - } - - // Remove duplicates - albumsUrl = albumsUrl.Distinct().ToList(); - return albumsUrl; - } - - private static string FixJson(string albumData) { - // Some JSON is not correctly formatted in bandcamp pages, so it needs to be fixed before we can deserialize it - - // In trackinfo property, we have for instance: - // url: "http://verbalclick.bandcamp.com" + "/album/404" - // -> Remove the " + " - var regex = new Regex("(?url: \".+)\" \\+ \"(?.+\",)"); - string fixedData = regex.Replace(albumData, "${root}${album}"); - - return fixedData; - } - - private static string GetAlbumData(string htmlCode) { - string startString = "data-tralbum=\"{"; - string stopString = "}\""; - - if (htmlCode.IndexOf(startString) == -1) { - // Could not find startString - throw new Exception($"Could not find the following string in HTML code: {startString}"); - } - - string albumDataTemp = htmlCode.Substring(htmlCode.IndexOf(startString) + startString.Length - 1); - string albumData = albumDataTemp.Substring(0, albumDataTemp.IndexOf(stopString) + 1); - - // Replace " by " - albumData = albumData.Replace(""", "\""); - - return albumData; - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Newtonsoft.Json; + +namespace BandcampDownloader { + + internal static class BandcampHelper { + + /// + /// Retrieves the data on the album of the specified Bandcamp page. + /// + /// The HTML source code of a Bandcamp album page. + /// The data on the album of the specified Bandcamp page. + public static Album GetAlbum(string htmlCode) { + // Keep the interesting part of htmlCode only + string albumData; + try { + albumData = GetAlbumData(htmlCode); + } catch (Exception e) { + throw new Exception("Could not retrieve album data in HTML code.", e); + } + + // Fix some wrongly formatted JSON in source code + albumData = FixJson(albumData); + + // Deserialize JSON + Album album; + try { + var settings = new JsonSerializerSettings { + NullValueHandling = NullValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore + }; + album = JsonConvert.DeserializeObject(albumData, settings).ToAlbum(); + } catch (Exception e) { + throw new Exception("Could not deserialize JSON data.", e); + } + + // Extract lyrics from album page + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(htmlCode); + foreach (Track track in album.Tracks) { + HtmlNode lyricsElement = htmlDoc.GetElementbyId("_lyrics_" + track.Number); + if (lyricsElement != null) { + track.Lyrics = lyricsElement.InnerText.Trim(); + } + } + + return album; + } + + /// + /// Retrieves all the albums URL existing on the specified Bandcamp page. + /// + /// The HTML source code of a Bandcamp page. + /// The albums URL existing on the specified Bandcamp page. + public static List GetAlbumsUrl(string htmlCode, string artistPage) { + // Get albums ("real" albums or track-only pages) relative urls + var regex = new Regex("href=\"(?/(album|track)/.*)\""); + if (!regex.IsMatch(htmlCode)) { + throw new NoAlbumFoundException(); + } + + var albumsUrl = new List(); + foreach (Match m in regex.Matches(htmlCode)) { + albumsUrl.Add(artistPage + m.Groups["url"].Value); + } + + // Remove duplicates + albumsUrl = albumsUrl.Distinct().ToList(); + return albumsUrl; + } + + private static string FixJson(string albumData) { + // Some JSON is not correctly formatted in bandcamp pages, so it needs to be fixed before we can deserialize it + + // In trackinfo property, we have for instance: + // url: "http://verbalclick.bandcamp.com" + "/album/404" + // -> Remove the " + " + var regex = new Regex("(?url: \".+)\" \\+ \"(?.+\",)"); + string fixedData = regex.Replace(albumData, "${root}${album}"); + + return fixedData; + } + + private static string GetAlbumData(string htmlCode) { + string startString = "data-tralbum=\"{"; + string stopString = "}\""; + + if (htmlCode.IndexOf(startString) == -1) { + // Could not find startString + throw new Exception($"Could not find the following string in HTML code: {startString}"); + } + + string albumDataTemp = htmlCode.Substring(htmlCode.IndexOf(startString) + startString.Length - 1); + string albumData = albumDataTemp.Substring(0, albumDataTemp.IndexOf(stopString) + 1); + + albumData = WebUtility.HtmlDecode(albumData); + + return albumData; + } + } } \ No newline at end of file diff --git a/src/BandcampDownloader/Properties/AssemblyInfo.cs b/src/BandcampDownloader/Properties/AssemblyInfo.cs index 6284ae08..9cf30915 100644 --- a/src/BandcampDownloader/Properties/AssemblyInfo.cs +++ b/src/BandcampDownloader/Properties/AssemblyInfo.cs @@ -47,6 +47,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.3.4")] -[assembly: AssemblyFileVersion("1.3.4")] +[assembly: AssemblyVersion("1.3.5")] +[assembly: AssemblyFileVersion("1.3.5")] [assembly: GuidAttribute("8C171C7F-9BAC-4EC0-A287-59908B48953F")] \ No newline at end of file diff --git a/src/BandcampDownloader/Properties/Resources.it.resx b/src/BandcampDownloader/Properties/Resources.it.resx index f77a33aa..c95ac882 100644 --- a/src/BandcampDownloader/Properties/Resources.it.resx +++ b/src/BandcampDownloader/Properties/Resources.it.resx @@ -448,6 +448,7 @@ Disabilitare questa opzione per risparmiare banda/tempo. Scaricando una traccia, se esiste già un file con lo stesso nome, si comparerà alla traccia da scaricare. Se la dimensione dei file differisce meno di questo valore (in percentuale), la traccia non sarà scaricata. +Imposta questo valore a 0 per scaricare sempre le tracce, anche se sono già presenti sul disco. Valore consigliato = 5 diff --git a/src/BandcampDownloader/UI/Dialogs/WindowMain.xaml.cs b/src/BandcampDownloader/UI/Dialogs/WindowMain.xaml.cs index cd8a4fd8..d7f20cfd 100644 --- a/src/BandcampDownloader/UI/Dialogs/WindowMain.xaml.cs +++ b/src/BandcampDownloader/UI/Dialogs/WindowMain.xaml.cs @@ -1,375 +1,375 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Media; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Shell; -using System.Windows.Threading; -using NLog; -using WpfMessageBoxLibrary; - -namespace BandcampDownloader { - - public partial class WindowMain: Window { - /// - /// True if there are active downloads; false otherwise. - /// - private bool _activeDownloads = false; - /// - /// The DownloadManager used to download albums. - /// - private DownloadManager _downloadManager; - /// - /// Used to compute and display the download speed. - /// - private DateTime _lastDownloadSpeedUpdate; - /// - /// Used to compute and display the download speed. - /// - private long _lastTotalReceivedBytes = 0; - /// - /// Used when user clicks on 'Cancel' to manage the cancellation (UI...). - /// - private bool _userCancelled; - - public WindowMain() { - // Save DataContext for bindings (must be called before initializing UI) - DataContext = App.UserSettings; - - InitializeComponent(); - -#if DEBUG - textBoxUrls.Text = "" - //+ "https://projectmooncircle.bandcamp.com" /* Lots of albums (124) */ + Environment.NewLine - //+ "https://goataholicskjald.bandcamp.com/album/dogma" /* #65 Downloaded size ≠ predicted */ + Environment.NewLine - //+ "https://mstrvlk.bandcamp.com/album/-" /* #64 Album with big cover */ + Environment.NewLine - //+ "https://mstrvlk.bandcamp.com/track/-" /* #64 Track with big cover */ + Environment.NewLine - //+ "https://weneverlearnedtolive.bandcamp.com/album/silently-i-threw-them-skyward" /* #42 Album with lyrics */ + Environment.NewLine - //+ "https://weneverlearnedtolive.bandcamp.com/track/shadows-in-hibernation-2" /* #42 Track with lyrics */ + Environment.NewLine - //+ "https://goataholicskjald.bandcamp.com/track/europa" + Environment.NewLine - //+ "https://goataholicskjald.bandcamp.com/track/epilogue" + Environment.NewLine - //+ "https://afterdarkrecordings.bandcamp.com/album/adr-unreleased-tracks" /* #69 Album without cover */ + Environment.NewLine - //+ "https://liluglymane.bandcamp.com/album/study-of-the-hypothesized-removable-and-or-expandable-nature-of-human-capability-and-limitations-primarily-regarding-introductory-experiences-with-new-and-exciting-technologies-by-way-of-motivati-2" /* #54 Long path */ + Environment.NewLine - //+ "https://brzoskamarciniakmarkiewicz.bandcamp.com/album/wp-aw" /* #82 Tracks with diacritics */ + Environment.NewLine - //+ "https://empyrium.bandcamp.com/album/der-wie-ein-blitz-vom-himmel-fiel" /* #102 Album ending with '...' */ + Environment.NewLine - + "https://tympanikaudio.bandcamp.com" /* #118 Different discography page */ + Environment.NewLine - ; -#endif - } - - private void ButtonBrowse_Click(object sender, RoutedEventArgs e) { - using (var dialog = new System.Windows.Forms.FolderBrowserDialog { - Description = Properties.Resources.folderBrowserDialogDescription - }) { - if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { - textBoxDownloadsPath.Text = dialog.SelectedPath + "\\{artist}\\{album}"; - // Force update of the settings file (it's not done unless the user gives then loses focus on the textbox) - textBoxDownloadsPath.GetBindingExpression(TextBox.TextProperty).UpdateSource(); - } - } - } - - private void ButtonOpenSettingsWindow_Click(object sender, RoutedEventArgs e) { - var windowSettings = new WindowSettings(_activeDownloads) { - Owner = this, - ShowInTaskbar = false, - }; - windowSettings.ShowDialog(); - } - - private async void ButtonStart_Click(object sender, RoutedEventArgs e) { - if (textBoxUrls.Text == "") { - // No URL to look - Log("Paste some albums URLs to be downloaded", LogType.Error); - return; - } - - // Set controls to "downloading..." state - _activeDownloads = true; - UpdateControlsState(true); - - Log("Starting download...", LogType.Info); - - await StartDownloadAsync(); - - if (_userCancelled) { - // Display message if user cancelled - Log("Downloads cancelled by user", LogType.Info); - } - - // Reset controls to "ready" state - _activeDownloads = false; - _lastTotalReceivedBytes = 0; - UpdateControlsState(false); - Mouse.OverrideCursor = null; - - if (App.UserSettings.EnableApplicationSounds) { - // Play a sound - try { - using (var soundPlayer = new SoundPlayer(@"C:\Windows\Media\Windows Ding.wav")) { - soundPlayer.Play(); - } - } catch { - } - } - } - - private void ButtonStop_Click(object sender, RoutedEventArgs e) { - var msgProperties = new WpfMessageBoxProperties() { - Button = MessageBoxButton.YesNo, - ButtonCancelText = Properties.Resources.messageBoxButtonCancel, - ButtonOkText = Properties.Resources.messageBoxButtonOK, - Image = MessageBoxImage.Question, - Text = Properties.Resources.messageBoxCancelDownloadsText, - Title = "Bandcamp Downloader", - }; - - if (WpfMessageBox.Show(this, ref msgProperties) != MessageBoxResult.Yes || !_activeDownloads) { - // If user cancelled the cancellation or if downloads finished while he choosed to cancel - return; - } - - Mouse.OverrideCursor = Cursors.Wait; - _userCancelled = true; - buttonStop.IsEnabled = false; - - _downloadManager.CancelDownloads(); - } - - /// - /// Displays a message if a new version is available. - /// - private async Task CheckForUpdates() { - Version latestVersion; - try { - latestVersion = await UpdatesHelper.GetLatestVersionAsync(); - } catch (CouldNotCheckForUpdatesException) { - labelNewVersion.Content = Properties.Resources.labelVersionError; - labelNewVersion.Visibility = Visibility.Visible; - return; - } - - Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; - if (currentVersion.CompareTo(latestVersion) < 0) { - // The latest version is newer than the current one - labelNewVersion.Content = Properties.Resources.labelVersionNewUpdateAvailable; - labelNewVersion.Visibility = Visibility.Visible; - } - } - - private void DownloadManager_LogAdded(object sender, LogArgs eventArgs) { - Log(eventArgs.Message, eventArgs.LogType); - } - - private void LabelNewVersion_MouseDown(object sender, MouseButtonEventArgs e) { - var windowUpdate = new WindowUpdate() { - Owner = this, - ShowInTaskbar = true, - WindowStartupLocation = WindowStartupLocation.CenterScreen, - }; - windowUpdate.Show(); - } - - /// - /// Logs to file and displays the specified message in the log textbox. - /// - /// The message. - /// The log type. - private void Log(string message, LogType logType) { - // Log to file - Logger logger = LogManager.GetCurrentClassLogger(); - logger.Log(logType.ToNLogLevel(), message); - - // Log to window - if (App.UserSettings.ShowVerboseLog || logType == LogType.Error || logType == LogType.Info || logType == LogType.IntermediateSuccess || logType == LogType.Success) { - // Time - var textRange = new TextRange(richTextBoxLog.Document.ContentEnd, richTextBoxLog.Document.ContentEnd) { - Text = DateTime.Now.ToString("HH:mm:ss") + " " - }; - textRange.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Gray); - // Message - textRange = new TextRange(richTextBoxLog.Document.ContentEnd, richTextBoxLog.Document.ContentEnd) { - Text = message - }; - textRange.ApplyPropertyValue(TextElement.ForegroundProperty, LogHelper.GetColor(logType)); - // Line break - richTextBoxLog.AppendText(Environment.NewLine); - - if (richTextBoxLog.IsScrolledToEnd()) { - richTextBoxLog.ScrollToEnd(); - } - } - } - - /// - /// Starts downloads. - /// - private async Task StartDownloadAsync() { - _userCancelled = false; - - // Initializes the DownloadManager - _downloadManager = new DownloadManager(textBoxUrls.Text); - _downloadManager.LogAdded += DownloadManager_LogAdded; - - // Fetch URL to get the files size - await _downloadManager.FetchUrlsAsync(); - - // Set progressBar max value - long maxProgressBarValue; - if (App.UserSettings.RetrieveFilesSize) { - maxProgressBarValue = _downloadManager.DownloadingFiles.Sum(f => f.Size); // Bytes to download - } else { - maxProgressBarValue = _downloadManager.DownloadingFiles.Count; // Number of files to download - } - if (maxProgressBarValue > 0) { - progressBar.IsIndeterminate = false; - progressBar.Maximum = maxProgressBarValue; - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal; - } - - // Start timer to update progress on UI - var updateProgressTimer = new DispatcherTimer() { - Interval = TimeSpan.FromMilliseconds(100) - }; - updateProgressTimer.Tick += UpdateProgressTimer_Tick; - updateProgressTimer.Start(); - - // Start timer to update download speed on UI - var updateDownloadSpeedTimer = new DispatcherTimer() { - Interval = TimeSpan.FromMilliseconds(1000) - }; - updateDownloadSpeedTimer.Tick += UpdateDownloadSpeedTimer_Tick; - updateDownloadSpeedTimer.Start(); - - // Start downloading albums - await _downloadManager.StartDownloadsAsync(); - - // Stop timers - updateProgressTimer.Stop(); - updateDownloadSpeedTimer.Stop(); - - // Update progress one last time to make sure the downloaded bytes displayed on UI is up-to-date - UpdateProgress(); - } - - /// - /// Updates the state of the controls. - /// - /// True if the download just started; false if it just stopped. - private void UpdateControlsState(bool downloadStarted) { - if (downloadStarted) { - // We just started the download - buttonBrowse.IsEnabled = false; - buttonStart.IsEnabled = false; - buttonStop.IsEnabled = true; - checkBoxDownloadDiscography.IsEnabled = false; - labelProgress.Content = ""; - progressBar.IsIndeterminate = true; - progressBar.Value = progressBar.Minimum; - richTextBoxLog.Document.Blocks.Clear(); - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate; - TaskbarItemInfo.ProgressValue = 0; - textBoxDownloadsPath.IsReadOnly = true; - textBoxUrls.IsReadOnly = true; - } else { - // We just finished the download (or user has cancelled) - buttonBrowse.IsEnabled = true; - buttonStart.IsEnabled = true; - buttonStop.IsEnabled = false; - checkBoxDownloadDiscography.IsEnabled = true; - labelDownloadSpeed.Content = ""; - progressBar.IsIndeterminate = false; - progressBar.Value = progressBar.Minimum; - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; - TaskbarItemInfo.ProgressValue = 0; - textBoxDownloadsPath.IsReadOnly = false; - textBoxUrls.IsReadOnly = false; - } - } - - /// - /// Updates the download speed on UI. - /// - private void UpdateDownloadSpeed() { - DateTime now = DateTime.Now; - - // Compute new progress values - long totalReceivedBytes = _downloadManager.DownloadingFiles.Sum(f => f.BytesReceived); - - double bytesPerSecond = - ((double) (totalReceivedBytes - _lastTotalReceivedBytes)) / - (now - _lastDownloadSpeedUpdate).TotalSeconds; - _lastTotalReceivedBytes = totalReceivedBytes; - _lastDownloadSpeedUpdate = now; - - // Update download speed on UI - labelDownloadSpeed.Content = (bytesPerSecond / 1024).ToString("0.0") + " kB/s"; - } - - private void UpdateDownloadSpeedTimer_Tick(object sender, EventArgs e) { - UpdateDownloadSpeed(); - } - - /// - /// Updates the progress label on UI. - /// - private void UpdateProgress() { - // Compute new progress values - long totalReceivedBytes = _downloadManager.DownloadingFiles.Sum(f => f.BytesReceived); - long bytesToDownload = _downloadManager.DownloadingFiles.Sum(f => f.Size); - - // Update progress label - labelProgress.Content = - ((double) totalReceivedBytes / (1024 * 1024)).ToString("0.00") + " MB" + - (App.UserSettings.RetrieveFilesSize ? (" / " + ((double) bytesToDownload / (1024 * 1024)).ToString("0.00") + " MB") : ""); - - if (App.UserSettings.RetrieveFilesSize) { - // Update progress bar based on bytes received - progressBar.Value = totalReceivedBytes; - // Taskbar progress is between 0 and 1 - TaskbarItemInfo.ProgressValue = totalReceivedBytes / progressBar.Maximum; - } else { - double downloadedFilesCount = _downloadManager.DownloadingFiles.Count(f => f.Downloaded); - // Update progress bar based on downloaded files - progressBar.Value = downloadedFilesCount; - // Taskbar progress is between 0 and count of files to download - TaskbarItemInfo.ProgressValue = downloadedFilesCount / progressBar.Maximum; - } - } - - private void UpdateProgressTimer_Tick(object sender, EventArgs e) { - UpdateProgress(); - } - - private void WindowMain_Closing(object sender, CancelEventArgs e) { - if (_activeDownloads) { - // There are active downloads, ask for confirmation - var msgProperties = new WpfMessageBoxProperties() { - Button = MessageBoxButton.OKCancel, - ButtonCancelText = Properties.Resources.messageBoxButtonCancel, - ButtonOkText = Properties.Resources.messageBoxCloseWindowWhenDownloadingButtonOk, - Image = MessageBoxImage.Warning, - Text = Properties.Resources.messageBoxCloseWindowWhenDownloadingText, - Title = "Bandcamp Downloader", - }; - - if (WpfMessageBox.Show(this, ref msgProperties) == MessageBoxResult.Cancel) { - // Cancel closing the window - e.Cancel = true; - } - } - } - - private async void WindowMain_Loaded(object sender, RoutedEventArgs e) { - if (App.UserSettings.CheckForUpdates) { - await CheckForUpdates(); - } - } - } +using System; +using System.ComponentModel; +using System.Linq; +using System.Media; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shell; +using System.Windows.Threading; +using NLog; +using WpfMessageBoxLibrary; + +namespace BandcampDownloader { + + public partial class WindowMain: Window { + /// + /// True if there are active downloads; false otherwise. + /// + private bool _activeDownloads = false; + /// + /// The DownloadManager used to download albums. + /// + private DownloadManager _downloadManager; + /// + /// Used to compute and display the download speed. + /// + private DateTime _lastDownloadSpeedUpdate; + /// + /// Used to compute and display the download speed. + /// + private long _lastTotalReceivedBytes = 0; + /// + /// Used when user clicks on 'Cancel' to manage the cancellation (UI...). + /// + private bool _userCancelled; + + public WindowMain() { + // Save DataContext for bindings (must be called before initializing UI) + DataContext = App.UserSettings; + + InitializeComponent(); + +#if DEBUG + textBoxUrls.Text = "" + //+ "https://projectmooncircle.bandcamp.com" /* Lots of albums (124) */ + Environment.NewLine + //+ "https://goataholicskjald.bandcamp.com/album/dogma" /* #65 Downloaded size ≠ predicted */ + Environment.NewLine + //+ "https://mstrvlk.bandcamp.com/album/-" /* #64 Album with big cover */ + Environment.NewLine + //+ "https://mstrvlk.bandcamp.com/track/-" /* #64 Track with big cover */ + Environment.NewLine + //+ "https://weneverlearnedtolive.bandcamp.com/album/silently-i-threw-them-skyward" /* #42 Album with lyrics */ + Environment.NewLine + //+ "https://weneverlearnedtolive.bandcamp.com/track/shadows-in-hibernation-2" /* #42 Track with lyrics */ + Environment.NewLine + //+ "https://goataholicskjald.bandcamp.com/track/europa" + Environment.NewLine + //+ "https://goataholicskjald.bandcamp.com/track/epilogue" + Environment.NewLine + //+ "https://afterdarkrecordings.bandcamp.com/album/adr-unreleased-tracks" /* #69 Album without cover */ + Environment.NewLine + //+ "https://liluglymane.bandcamp.com/album/study-of-the-hypothesized-removable-and-or-expandable-nature-of-human-capability-and-limitations-primarily-regarding-introductory-experiences-with-new-and-exciting-technologies-by-way-of-motivati-2" /* #54 Long path */ + Environment.NewLine + //+ "https://brzoskamarciniakmarkiewicz.bandcamp.com/album/wp-aw" /* #82 Tracks with diacritics */ + Environment.NewLine + //+ "https://empyrium.bandcamp.com/album/der-wie-ein-blitz-vom-himmel-fiel" /* #102 Album ending with '...' */ + Environment.NewLine + + "https://tympanikaudio.bandcamp.com" /* #118 Different discography page */ + Environment.NewLine + ; +#endif + } + + private void ButtonBrowse_Click(object sender, RoutedEventArgs e) { + using (var dialog = new System.Windows.Forms.FolderBrowserDialog { + Description = Properties.Resources.folderBrowserDialogDescription + }) { + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { + textBoxDownloadsPath.Text = dialog.SelectedPath + "\\{artist}\\{album}"; + // Force update of the settings file (it's not done unless the user gives then loses focus on the textbox) + textBoxDownloadsPath.GetBindingExpression(TextBox.TextProperty).UpdateSource(); + } + } + } + + private void ButtonOpenSettingsWindow_Click(object sender, RoutedEventArgs e) { + var windowSettings = new WindowSettings(_activeDownloads) { + Owner = this, + ShowInTaskbar = false, + }; + windowSettings.ShowDialog(); + } + + private async void ButtonStart_Click(object sender, RoutedEventArgs e) { + if (textBoxUrls.Text == "") { + // No URL to look + Log("Paste some albums URLs to be downloaded", LogType.Error); + return; + } + + // Set controls to "downloading..." state + _activeDownloads = true; + UpdateControlsState(true); + + Log("Starting download...", LogType.Info); + + await StartDownloadAsync(); + + if (_userCancelled) { + // Display message if user cancelled + Log("Downloads cancelled by user", LogType.Info); + } + + // Reset controls to "ready" state + _activeDownloads = false; + _lastTotalReceivedBytes = 0; + UpdateControlsState(false); + Mouse.OverrideCursor = null; + + if (App.UserSettings.EnableApplicationSounds) { + // Play a sound + try { + using (var soundPlayer = new SoundPlayer(@"C:\Windows\Media\Windows Ding.wav")) { + soundPlayer.Play(); + } + } catch { + } + } + } + + private void ButtonStop_Click(object sender, RoutedEventArgs e) { + var msgProperties = new WpfMessageBoxProperties() { + Button = MessageBoxButton.YesNo, + ButtonCancelText = Properties.Resources.messageBoxButtonCancel, + ButtonOkText = Properties.Resources.messageBoxButtonOK, + Image = MessageBoxImage.Question, + Text = Properties.Resources.messageBoxCancelDownloadsText, + Title = "Bandcamp Downloader", + }; + + if (WpfMessageBox.Show(this, ref msgProperties) != MessageBoxResult.Yes || !_activeDownloads) { + // If user cancelled the cancellation or if downloads finished while he choosed to cancel + return; + } + + Mouse.OverrideCursor = Cursors.Wait; + _userCancelled = true; + buttonStop.IsEnabled = false; + + _downloadManager.CancelDownloads(); + } + + /// + /// Displays a message if a new version is available. + /// + private async Task CheckForUpdates() { + Version latestVersion; + try { + latestVersion = await UpdatesHelper.GetLatestVersionAsync(); + } catch (CouldNotCheckForUpdatesException) { + labelNewVersion.Content = Properties.Resources.labelVersionError; + labelNewVersion.Visibility = Visibility.Visible; + return; + } + + Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (currentVersion.CompareTo(latestVersion) < 0) { + // The latest version is newer than the current one + labelNewVersion.Content = Properties.Resources.labelVersionNewUpdateAvailable; + labelNewVersion.Visibility = Visibility.Visible; + } + } + + private void DownloadManager_LogAdded(object sender, LogArgs eventArgs) { + Log(eventArgs.Message, eventArgs.LogType); + } + + private void LabelNewVersion_MouseDown(object sender, MouseButtonEventArgs e) { + var windowUpdate = new WindowUpdate() { + Owner = this, + ShowInTaskbar = true, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + }; + windowUpdate.Show(); + } + + /// + /// Logs to file and displays the specified message in the log textbox. + /// + /// The message. + /// The log type. + private void Log(string message, LogType logType) { + // Log to file + Logger logger = LogManager.GetCurrentClassLogger(); + logger.Log(logType.ToNLogLevel(), message); + + // Log to window + if (App.UserSettings.ShowVerboseLog || logType == LogType.Error || logType == LogType.Info || logType == LogType.IntermediateSuccess || logType == LogType.Success) { + // Time + var textRange = new TextRange(richTextBoxLog.Document.ContentEnd, richTextBoxLog.Document.ContentEnd) { + Text = DateTime.Now.ToString("HH:mm:ss") + " " + }; + textRange.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Gray); + // Message + textRange = new TextRange(richTextBoxLog.Document.ContentEnd, richTextBoxLog.Document.ContentEnd) { + Text = message + }; + textRange.ApplyPropertyValue(TextElement.ForegroundProperty, LogHelper.GetColor(logType)); + // Line break + richTextBoxLog.AppendText(Environment.NewLine); + + if (richTextBoxLog.IsScrolledToEnd()) { + richTextBoxLog.ScrollToEnd(); + } + } + } + + /// + /// Starts downloads. + /// + private async Task StartDownloadAsync() { + _userCancelled = false; + + // Initializes the DownloadManager + _downloadManager = new DownloadManager(textBoxUrls.Text); + _downloadManager.LogAdded += DownloadManager_LogAdded; + + // Fetch URL to get the files size + await _downloadManager.FetchUrlsAsync(); + + // Set progressBar max value + long maxProgressBarValue; + if (App.UserSettings.RetrieveFilesSize) { + maxProgressBarValue = _downloadManager.DownloadingFiles.Sum(f => f.Size); // Bytes to download + } else { + maxProgressBarValue = _downloadManager.DownloadingFiles.Count; // Number of files to download + } + if (maxProgressBarValue > 0) { + progressBar.IsIndeterminate = false; + progressBar.Maximum = maxProgressBarValue; + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal; + } + + // Start timer to update progress on UI + var updateProgressTimer = new DispatcherTimer() { + Interval = TimeSpan.FromMilliseconds(100) + }; + updateProgressTimer.Tick += UpdateProgressTimer_Tick; + updateProgressTimer.Start(); + + // Start timer to update download speed on UI + var updateDownloadSpeedTimer = new DispatcherTimer() { + Interval = TimeSpan.FromMilliseconds(1000) + }; + updateDownloadSpeedTimer.Tick += UpdateDownloadSpeedTimer_Tick; + updateDownloadSpeedTimer.Start(); + + // Start downloading albums + await _downloadManager.StartDownloadsAsync(); + + // Stop timers + updateProgressTimer.Stop(); + updateDownloadSpeedTimer.Stop(); + + // Update progress one last time to make sure the downloaded bytes displayed on UI is up-to-date + UpdateProgress(); + } + + /// + /// Updates the state of the controls. + /// + /// True if the download just started; false if it just stopped. + private void UpdateControlsState(bool downloadStarted) { + if (downloadStarted) { + // We just started the download + buttonBrowse.IsEnabled = false; + buttonStart.IsEnabled = false; + buttonStop.IsEnabled = true; + checkBoxDownloadDiscography.IsEnabled = false; + labelProgress.Content = ""; + progressBar.IsIndeterminate = true; + progressBar.Value = progressBar.Minimum; + richTextBoxLog.Document.Blocks.Clear(); + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate; + TaskbarItemInfo.ProgressValue = 0; + textBoxDownloadsPath.IsReadOnly = true; + textBoxUrls.IsReadOnly = true; + } else { + // We just finished the download (or user has cancelled) + buttonBrowse.IsEnabled = true; + buttonStart.IsEnabled = true; + buttonStop.IsEnabled = false; + checkBoxDownloadDiscography.IsEnabled = true; + labelDownloadSpeed.Content = ""; + progressBar.IsIndeterminate = false; + progressBar.Value = progressBar.Minimum; + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; + TaskbarItemInfo.ProgressValue = 0; + textBoxDownloadsPath.IsReadOnly = false; + textBoxUrls.IsReadOnly = false; + } + } + + /// + /// Updates the download speed on UI. + /// + private void UpdateDownloadSpeed() { + DateTime now = DateTime.Now; + + // Compute new progress values + long totalReceivedBytes = _downloadManager.DownloadingFiles.Sum(f => f.BytesReceived); + + double bytesPerSecond = + ((double) (totalReceivedBytes - _lastTotalReceivedBytes)) / + (now - _lastDownloadSpeedUpdate).TotalSeconds; + _lastTotalReceivedBytes = totalReceivedBytes; + _lastDownloadSpeedUpdate = now; + + // Update download speed on UI + labelDownloadSpeed.Content = (bytesPerSecond / 1024).ToString("0.0") + " kB/s"; + } + + private void UpdateDownloadSpeedTimer_Tick(object sender, EventArgs e) { + UpdateDownloadSpeed(); + } + + /// + /// Updates the progress label on UI. + /// + private void UpdateProgress() { + // Compute new progress values + long totalReceivedBytes = _downloadManager.DownloadingFiles.Sum(f => f.BytesReceived); + long bytesToDownload = _downloadManager.DownloadingFiles.Sum(f => f.Size); + + // Update progress label + labelProgress.Content = + ((double) totalReceivedBytes / (1024 * 1024)).ToString("0.00") + " MB" + + (App.UserSettings.RetrieveFilesSize ? (" / " + ((double) bytesToDownload / (1024 * 1024)).ToString("0.00") + " MB") : ""); + + if (App.UserSettings.RetrieveFilesSize) { + // Update progress bar based on bytes received + progressBar.Value = totalReceivedBytes; + // Taskbar progress is between 0 and 1 + TaskbarItemInfo.ProgressValue = totalReceivedBytes / progressBar.Maximum; + } else { + double downloadedFilesCount = _downloadManager.DownloadingFiles.Count(f => f.Downloaded); + // Update progress bar based on downloaded files + progressBar.Value = downloadedFilesCount; + // Taskbar progress is between 0 and count of files to download + TaskbarItemInfo.ProgressValue = downloadedFilesCount / progressBar.Maximum; + } + } + + private void UpdateProgressTimer_Tick(object sender, EventArgs e) { + UpdateProgress(); + } + + private void WindowMain_Closing(object sender, CancelEventArgs e) { + if (_activeDownloads) { + // There are active downloads, ask for confirmation + var msgProperties = new WpfMessageBoxProperties() { + Button = MessageBoxButton.OKCancel, + ButtonCancelText = Properties.Resources.messageBoxButtonCancel, + ButtonOkText = Properties.Resources.messageBoxCloseWindowWhenDownloadingButtonOk, + Image = MessageBoxImage.Warning, + Text = Properties.Resources.messageBoxCloseWindowWhenDownloadingText, + Title = "Bandcamp Downloader", + }; + + if (WpfMessageBox.Show(this, ref msgProperties) == MessageBoxResult.Cancel) { + // Cancel closing the window + e.Cancel = true; + } + } + } + + private async void WindowMain_Loaded(object sender, RoutedEventArgs e) { + if (App.UserSettings.CheckForUpdates) { + await CheckForUpdates(); + } + } + } } \ No newline at end of file