diff --git a/CHANGELOG.md b/CHANGELOG.md index de7b8255..261d389e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 0.2.9.0 + +## New features + +* Starting next version, the changelog will be displayed when a new version is available. +* Starting next version, an option will allow to directly download the new version from the application. + +## Improvements + +* Minor UI improvements. + +## Bug fixes + +* Fixed playlist creation for tracks with foreign characters. [#82](https://github.com/Otiel/BandcampDownloader/issues/82) +* Fixed application crash when saving tracks to a path with more than 260 characters. + # 0.2.8.2 ## Bug fixes @@ -8,7 +24,7 @@ ## Bug fixes -* Fixed a bug preventing the "Playlist file name format" setting to be saved in the _ini_ file. +* Fixed a bug preventing the "Playlist file name format" setting to be saved in the _ini_ file. # 0.2.8.0 diff --git a/docs/dependencies.md b/docs/dependencies.md index 450cd96a..9e0f9d1c 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -9,6 +9,8 @@ BandcampDownloader relies on a number of open-source libraries, all listed below * [`HtmlAgilityPack`](https://github.com/zzzprojects/html-agility-pack): used to parse Html from bandcamp.com pages. * [`ImageResizer`](https://github.com/imazen/resizer): used to resize/compress the album covers. * [`Json.NET`](https://github.com/JamesNK/Newtonsoft.Json): used to parse Json from bandcamp.com pages. +* [`Markdig`](https://github.com/lunet-io/markdig): required by `Markdig.WPF`. +* [`Markdig.WPF`](https://github.com/Kryptos-FR/markdig.wpf): used to display Markdown. * [`MessageBoxManager`](https://www.codeproject.com/Articles/18399/Localizing-System-MessageBox): used to localize the buttons of the Windows system message box. * [`NLog`](https://github.com/NLog/NLog): used to log events. * [`PlaylistsNET`](https://github.com/tmk907/PlaylistsNET): used to create playlists. diff --git a/src/BandcampDownloader/BandcampDownloader.csproj b/src/BandcampDownloader/BandcampDownloader.csproj index d82479f7..263d3866 100644 --- a/src/BandcampDownloader/BandcampDownloader.csproj +++ b/src/BandcampDownloader/BandcampDownloader.csproj @@ -94,6 +94,12 @@ ..\packages\ImageResizer.4.2.5\lib\net45\ImageResizer.dll + + ..\packages\Markdig.0.16.0\lib\net40\Markdig.dll + + + ..\packages\Markdig.Wpf.0.2.8\lib\net452\Markdig.Wpf.dll + @@ -142,18 +148,27 @@ MSBuild:Compile Designer + + + - + + + + UserControlChangelog.xaml + + + WindowUpdate.xaml + - @@ -229,6 +244,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -237,6 +256,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + @@ -252,11 +275,15 @@ Settings.settings True - + + Designer + - Resources.fr.Designer.cs + Designer + + + Designer - ResXFileCodeGenerator Resources.Designer.cs diff --git a/src/BandcampDownloader/Core/Constants.cs b/src/BandcampDownloader/Core/Constants.cs index c12208e0..3354dba2 100644 --- a/src/BandcampDownloader/Core/Constants.cs +++ b/src/BandcampDownloader/Core/Constants.cs @@ -10,6 +10,10 @@ internal static class Constants { /// public static readonly String AppVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(); /// + /// The URL redirecting to the changelog file on GitHub. + /// + public static readonly String ChangelogUrl = "https://raw.githubusercontent.com/Otiel/BandcampDownloader/master/CHANGELOG.md"; + /// /// The URL redirecting to the help page on translating the app on GitHub. /// public static readonly String HelpTranslateWebsite = "https://github.com/Otiel/BandcampDownloader/blob/master/docs/help-translate.md"; @@ -33,5 +37,9 @@ internal static class Constants { /// The absolute path to the settings file. /// public static readonly String UserSettingsFilePath = Directory.GetParent(Assembly.GetExecutingAssembly().Location) + @"\BandcampDownloader.ini"; + /// + /// The URL redirecting to the zip file. Must be formatted with a version. + /// + public static readonly String ZipUrl = "https://github.com/Otiel/BandcampDownloader/releases/download/v{0}/BandcampDownloader.zip"; } } \ No newline at end of file diff --git a/src/BandcampDownloader/Core/DownloadManager.cs b/src/BandcampDownloader/Core/DownloadManager.cs new file mode 100644 index 00000000..d0bbcdac --- /dev/null +++ b/src/BandcampDownloader/Core/DownloadManager.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using ImageResizer; + +namespace BandcampDownloader { + + internal class DownloadManager { + /// + /// The URLs to download. + /// + private readonly String _urls; + /// + /// The albums to download. + /// + private List _albums; + /// + /// True if we received the order to cancel downloads; false otherwise. + /// + private Boolean _cancelDownloads; + /// + /// The list of WebClients currently used to download files. Used when downloads must be cancelled. + /// + private List _pendingDownloads = new List(); + + /// + /// The files to download, or being downloaded, or already downloaded. Used to compute the current received bytes and the total bytes to download. + /// + public List DownloadingFiles { get; set; } + + public delegate void LogAddedEventHandler(object sender, LogArgs eventArgs); + + /// + /// Initializes a new instance of DownloadManager. + /// + /// The URLs we'll download from. + public DownloadManager(String urls) { + _urls = urls; + } + + /// + /// Cancels all downloads. + /// + public void CancelDownloads() { + _cancelDownloads = true; + + lock (_pendingDownloads) { + // Stop current downloads + foreach (WebClient webClient in _pendingDownloads) { + webClient.CancelAsync(); + } + } + } + + /// + /// Fetch albums data from the URLs specified when creating this DownloadManager. + /// + public async Task FetchUrlsAsync() { + var urls = _urls.Split(new String[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).ToList(); + urls = urls.Distinct().ToList(); + + // Get URLs of albums to download + if (App.UserSettings.DownloadArtistDiscography) { + urls = await GetArtistDiscographyAsync(urls); + } + + // Get info on albums + _albums = await GetAlbumsAsync(urls); + + // Save the files to download and get their size + DownloadingFiles = await GetFilesToDownloadAsync(_albums); + } + + /// + /// Starts downloads. + /// + public async Task StartDownloadsAsync() { + if (_albums == null) { + throw new Exception("Must call FetchUrls before calling StartDownloadsAsync"); + } + + _pendingDownloads = new List(); + + // Start downloading albums + if (App.UserSettings.DownloadOneAlbumAtATime) { + // Download one album at a time + foreach (Album album in _albums) { + await DownloadAlbumAsync(album); + } + } else { + // Parallel download + Int32[] albumsIndexes = Enumerable.Range(0, _albums.Count).ToArray(); + await Task.WhenAll(albumsIndexes.Select(i => DownloadAlbumAsync(_albums[i]))); + } + } + + /// + /// Downloads an album. + /// + /// The album to download. + private async Task DownloadAlbumAsync(Album album) { + if (_cancelDownloads) { + // Abort + return; + } + + // Create directory to place track files + try { + Directory.CreateDirectory(album.Path); + } catch { + LogAdded(this, new LogArgs("An error occured when creating the album folder. Make sure you have the rights to write files in the folder you chose", LogType.Error)); + return; + } + + TagLib.Picture artwork = null; + + // Download artwork + if ((App.UserSettings.SaveCoverArtInTags || App.UserSettings.SaveCoverArtInFolder) && album.HasArtwork) { + artwork = await DownloadCoverArtAsync(album); + } + + // Download & tag tracks + Boolean[] tracksDownloaded = new Boolean[album.Tracks.Count]; + Int32[] indexes = Enumerable.Range(0, album.Tracks.Count).ToArray(); + await Task.WhenAll(indexes.Select(async i => tracksDownloaded[i] = await DownloadAndTagTrackAsync(album, album.Tracks[i], artwork))); + + // Create playlist file + if (App.UserSettings.CreatePlaylist && !_cancelDownloads) { + new PlaylistCreator(album).SavePlaylistToFile(); + LogAdded(this, new LogArgs($"Saved playlist for album \"{album.Title}\"", LogType.IntermediateSuccess)); + } + + if (!_cancelDownloads) { + // Tasks have not been aborted + if (tracksDownloaded.All(x => x == true)) { + LogAdded(this, new LogArgs($"Successfully downloaded album \"{album.Title}\"", LogType.Success)); + } else { + LogAdded(this, new LogArgs($"Finished downloading album \"{album.Title}\". Some tracks were not downloaded", LogType.Success)); + } + } + } + + /// + /// Downloads and tags a track. Returns true if the track has been correctly downloaded; false otherwise. + /// + /// The album of the track to download. + /// The track to download. + /// The cover art. + private async Task DownloadAndTagTrackAsync(Album album, Track track, TagLib.Picture artwork) { + LogAdded(this, new LogArgs($"Downloading track \"{track.Title}\" from url: {track.Mp3Url}", LogType.VerboseInfo)); + + int tries = 0; + Boolean trackDownloaded = false; + TrackFile currentFile = DownloadingFiles.Where(f => f.Url == track.Mp3Url).First(); + + if (File.Exists(track.Path)) { + long length = new FileInfo(track.Path).Length; + if (currentFile.Size > length - (currentFile.Size * App.UserSettings.AllowedFileSizeDifference) && + currentFile.Size < length + (currentFile.Size * App.UserSettings.AllowedFileSizeDifference)) { + LogAdded(this, new LogArgs($"Track already exists within allowed file size range: track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\" - Skipping download!", LogType.IntermediateSuccess)); + return false; + } + } + + do { + using (var webClient = new WebClient()) { + ProxyHelper.SetProxy(webClient); + + // Update progress bar when downloading + webClient.DownloadProgressChanged += (s, e) => { + currentFile.BytesReceived = e.BytesReceived; + }; + + // Warn & tag when downloaded + webClient.DownloadFileCompleted += async (s, e) => { + if (!e.Cancelled && e.Error == null) { + trackDownloaded = true; + + if (App.UserSettings.ModifyTags) { + // Tag (ID3) the file when downloaded + var tagFile = TagLib.File.Create(track.Path); + tagFile = TagHelper.UpdateArtist(tagFile, album.Artist, App.UserSettings.TagArtist); + tagFile = TagHelper.UpdateAlbumArtist(tagFile, album.Artist, App.UserSettings.TagAlbumArtist); + tagFile = TagHelper.UpdateAlbumTitle(tagFile, album.Title, App.UserSettings.TagAlbumTitle); + tagFile = TagHelper.UpdateAlbumYear(tagFile, (uint) album.ReleaseDate.Year, App.UserSettings.TagYear); + tagFile = TagHelper.UpdateTrackNumber(tagFile, (uint) track.Number, App.UserSettings.TagTrackNumber); + tagFile = TagHelper.UpdateTrackTitle(tagFile, track.Title, App.UserSettings.TagTrackTitle); + tagFile = TagHelper.UpdateTrackLyrics(tagFile, track.Lyrics, App.UserSettings.TagLyrics); + tagFile = TagHelper.UpdateComments(tagFile, App.UserSettings.TagComments); + tagFile.Save(); + } + + if (App.UserSettings.SaveCoverArtInTags && artwork != null) { + // Save cover in tags when downloaded + var tagFile = TagLib.File.Create(track.Path); + tagFile.Tag.Pictures = new TagLib.IPicture[1] { artwork }; + tagFile.Save(); + } + + // Note the file as downloaded + currentFile.Downloaded = true; + LogAdded(this, new LogArgs($"Downloaded track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\"", LogType.IntermediateSuccess)); + } else if (!e.Cancelled && e.Error != null) { + if (tries + 1 < App.UserSettings.DownloadMaxTries) { + LogAdded(this, new LogArgs($"Unable to download track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning)); + } else { + LogAdded(this, new LogArgs($"Unable to download track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}", LogType.Error)); + } + } // Else the download has been cancelled (by the user) + + tries++; + if (!trackDownloaded && tries < App.UserSettings.DownloadMaxTries) { + await WaitForCooldownAsync(tries); + } + }; + + lock (_pendingDownloads) { + if (_cancelDownloads) { + // Abort + return false; + } + // Register current download + _pendingDownloads.Add(webClient); + } + + // Start download + try { + await webClient.DownloadFileTaskAsync(track.Mp3Url, track.Path); + } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { + // Downloads cancelled by the user + // Do nothing + } + + lock (_pendingDownloads) { + _pendingDownloads.Remove(webClient); + } + } + } while (!trackDownloaded && tries < App.UserSettings.DownloadMaxTries); + + return trackDownloaded; + } + + /// + /// Downloads and returns the cover art of the specified album. + /// Depending on UserSettings, save the cover art in the album folder. + /// + /// The album. + private async Task DownloadCoverArtAsync(Album album) { + TagLib.Picture artworkInTags = null; + + int tries = 0; + Boolean artworkDownloaded = false; + TrackFile currentFile = DownloadingFiles.Where(f => f.Url == album.ArtworkUrl).First(); + + do { + using (var webClient = new WebClient()) { + ProxyHelper.SetProxy(webClient); + + // Update progress bar when downloading + webClient.DownloadProgressChanged += (s, e) => { + currentFile.BytesReceived = e.BytesReceived; + }; + + // Warn when downloaded + webClient.DownloadFileCompleted += async (s, e) => { + if (!e.Cancelled && e.Error == null) { + artworkDownloaded = true; + + // Convert/resize artwork to be saved in album folder + if (App.UserSettings.SaveCoverArtInFolder && (App.UserSettings.CoverArtInFolderConvertToJpg || App.UserSettings.CoverArtInFolderResize)) { + var settings = new ResizeSettings(); + if (App.UserSettings.CoverArtInFolderConvertToJpg) { + settings.Format = "jpg"; + settings.Quality = 90; + } + if (App.UserSettings.CoverArtInFolderResize) { + settings.MaxHeight = App.UserSettings.CoverArtInFolderMaxSize; + settings.MaxWidth = App.UserSettings.CoverArtInFolderMaxSize; + } + + await Task.Run(() => { + ImageBuilder.Current.Build(album.ArtworkTempPath, album.ArtworkPath, settings); // Save it to the album folder + }); + } else if (App.UserSettings.SaveCoverArtInFolder) { + File.Copy(album.ArtworkTempPath, album.ArtworkPath, true); + } + + // Convert/resize artwork to be saved in tags + if (App.UserSettings.SaveCoverArtInTags && (App.UserSettings.CoverArtInTagsConvertToJpg || App.UserSettings.CoverArtInTagsResize)) { + var settings = new ResizeSettings(); + if (App.UserSettings.CoverArtInTagsConvertToJpg) { + settings.Format = "jpg"; + settings.Quality = 90; + } + if (App.UserSettings.CoverArtInTagsResize) { + settings.MaxHeight = App.UserSettings.CoverArtInTagsMaxSize; + settings.MaxWidth = App.UserSettings.CoverArtInTagsMaxSize; + } + + await Task.Run(() => { + ImageBuilder.Current.Build(album.ArtworkTempPath, album.ArtworkTempPath, settings); // Save it to %Temp% + }); + } + artworkInTags = new TagLib.Picture(album.ArtworkTempPath) { + Description = "Picture" + }; + + try { + File.Delete(album.ArtworkTempPath); + } catch { + // Could not delete the file. Nevermind, it's in %Temp% folder... + } + + // Note the file as downloaded + currentFile.Downloaded = true; + LogAdded(this, new LogArgs($"Downloaded artwork for album \"{album.Title}\"", LogType.IntermediateSuccess)); + } else if (!e.Cancelled && e.Error != null) { + if (tries < App.UserSettings.DownloadMaxTries) { + LogAdded(this, new LogArgs($"Unable to download artwork for album \"{album.Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning)); + } else { + LogAdded(this, new LogArgs($"Unable to download artwork for album \"{album.Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}", LogType.Error)); + } + } // Else the download has been cancelled (by the user) + + tries++; + if (!artworkDownloaded && tries < App.UserSettings.DownloadMaxTries) { + await WaitForCooldownAsync(tries); + } + }; + + lock (_pendingDownloads) { + if (_cancelDownloads) { + // Abort + return null; + } + // Register current download + _pendingDownloads.Add(webClient); + } + + // Start download + try { + await webClient.DownloadFileTaskAsync(album.ArtworkUrl, album.ArtworkTempPath); + } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { + // Downloads cancelled by the user + // Do nothing + } + + lock (_pendingDownloads) { + _pendingDownloads.Remove(webClient); + } + } + } while (!artworkDownloaded && tries < App.UserSettings.DownloadMaxTries); + + return artworkInTags; + } + + /// + /// Returns the albums located at the specified URLs. + /// + /// The URLs. + private async Task> GetAlbumsAsync(List urls) { + var albums = new List(); + + foreach (String url in urls) { + LogAdded(this, new LogArgs($"Retrieving album data for {url}", LogType.Info)); + + // Retrieve URL HTML source code + String htmlCode = ""; + using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { + ProxyHelper.SetProxy(webClient); + + if (_cancelDownloads) { + // Abort + return new List(); + } + + try { + htmlCode = await webClient.DownloadStringTaskAsync(url); + } catch { + LogAdded(this, new LogArgs($"Could not retrieve data for {url}", LogType.Error)); + continue; + } + } + + // Get info on album + try { + albums.Add(BandcampHelper.GetAlbum(htmlCode)); + } catch { + LogAdded(this, new LogArgs($"Could not retrieve album info for {url}", LogType.Error)); + continue; + } + } + + return albums; + } + + /// + /// Returns the artists discography from any URL (artist, album, track). + /// + /// The URLs. + private async Task> GetArtistDiscographyAsync(List urls) { + var albumsUrls = new List(); + + foreach (String url in urls) { + LogAdded(this, new LogArgs($"Retrieving artist discography from {url}", LogType.Info)); + + // Retrieve URL HTML source code + String htmlCode = ""; + using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { + ProxyHelper.SetProxy(webClient); + + if (_cancelDownloads) { + // Abort + return new List(); + } + + try { + htmlCode = await webClient.DownloadStringTaskAsync(url); + } catch { + LogAdded(this, new LogArgs($"Could not retrieve data for {url}", LogType.Error)); + continue; + } + } + + // Get artist "music" bandcamp page (http://artist.bandcamp.com/music) + var regex = new Regex("band_url = \"(?.*)\""); + if (!regex.IsMatch(htmlCode)) { + LogAdded(this, new LogArgs($"No discography could be found on {url}. Try to uncheck the \"Download artist discography\" option", LogType.Error)); + continue; + } + String artistMusicPage = regex.Match(htmlCode).Groups["url"].Value + "/music"; + + // Retrieve artist "music" page HTML source code + using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { + ProxyHelper.SetProxy(webClient); + + if (_cancelDownloads) { + // Abort + return new List(); + } + + try { + htmlCode = await webClient.DownloadStringTaskAsync(artistMusicPage); + } catch { + LogAdded(this, new LogArgs($"Could not retrieve data for {artistMusicPage}", LogType.Error)); + continue; + } + } + + // Get albums referred on the page + regex = new Regex("TralbumData.*\n.*url:.*'/music'\n"); + if (!regex.IsMatch(htmlCode)) { + // This seem to be a one-album artist with no "music" page => URL redirects to the unique album URL + albumsUrls.Add(url); + } else { + // We are on a real "music" page + try { + albumsUrls.AddRange(BandcampHelper.GetAlbumsUrl(htmlCode)); + } catch (NoAlbumFoundException) { + LogAdded(this, new LogArgs($"No referred album could be found on {artistMusicPage}. Try to uncheck the \"Download artist discography\" option", LogType.Error)); + continue; + } + } + } + + return albumsUrls.Distinct().ToList(); + } + + /// + /// Returns the size of the file located at the specified URL. + /// + /// The URL. + /// The title of the file to be displayed in the log. + /// The type of the file. + private async Task GetFileSizeAsync(String url, String titleForLog, FileType fileType) { + long size = 0; + Boolean sizeRetrieved; + int tries = 0; + String fileTypeForLog; + String protocolMethod; + + switch (fileType) { + case FileType.Artwork: + fileTypeForLog = "cover art file for album"; + protocolMethod = "HEAD"; + break; + case FileType.Track: + fileTypeForLog = "MP3 file for the track"; + // Using the HEAD method on tracks urls does not work (Error 405: Method not allowed) + // Surprisingly, using the GET method does not seem to download the whole file, so we will use it to retrieve the mp3 sizes + protocolMethod = "GET"; + break; + default: + throw new NotImplementedException(); + } + + do { + if (_cancelDownloads) { + // Abort + return 0; + } + + try { + size = await FileHelper.GetFileSizeAsync(url, protocolMethod); + sizeRetrieved = true; + LogAdded(this, new LogArgs($"Retrieved the size of the {fileTypeForLog} \"{titleForLog}\"", LogType.VerboseInfo)); + } catch { + sizeRetrieved = false; + if (tries + 1 < App.UserSettings.DownloadMaxTries) { + LogAdded(this, new LogArgs($"Failed to retrieve the size of the {fileTypeForLog} \"{titleForLog}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning)); + } else { + LogAdded(this, new LogArgs($"Failed to retrieve the size of the {fileTypeForLog} \"{titleForLog}\". Hit max retries of {App.UserSettings.DownloadMaxTries}. Progress update may be wrong.", LogType.Error)); + } + } + + tries++; + if (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries) { + await WaitForCooldownAsync(tries); + } + } while (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries); + + return size; + } + + /// + /// Returns the files to download from a list of albums. + /// + /// The albums. + private async Task> GetFilesToDownloadAsync(List albums) { + var files = new List(); + foreach (Album album in albums) { + LogAdded(this, new LogArgs($"Computing size for album \"{album.Title}\"...", LogType.Info)); + + // Artwork + if ((App.UserSettings.SaveCoverArtInTags || App.UserSettings.SaveCoverArtInFolder) && album.HasArtwork) { + if (App.UserSettings.RetrieveFilesSize) { + long size = await GetFileSizeAsync(album.ArtworkUrl, album.Title, FileType.Artwork); + files.Add(new TrackFile(album.ArtworkUrl, 0, size)); + } else { + files.Add(new TrackFile(album.ArtworkUrl, 0, 0)); + } + } + + // Tracks + if (App.UserSettings.RetrieveFilesSize) { + Int32[] tracksIndexes = Enumerable.Range(0, album.Tracks.Count).ToArray(); + await Task.WhenAll(tracksIndexes.Select(async i => { + long size = await GetFileSizeAsync(album.Tracks[i].Mp3Url, album.Tracks[i].Title, FileType.Track); + files.Add(new TrackFile(album.Tracks[i].Mp3Url, 0, size)); + })); + } else { + foreach (Track track in album.Tracks) { + files.Add(new TrackFile(track.Mp3Url, 0, 0)); + } + } + } + + return files; + } + + /// + /// Waits for a "cooldown" time, computed from the specified number of download tries. + /// + /// The times count we tried to download the same file. + /// + private async Task WaitForCooldownAsync(int triesNumber) { + if (App.UserSettings.DownloadRetryCooldown != 0) { + await Task.Delay((int) (Math.Pow(App.UserSettings.DownloadRetryExponent, triesNumber) * App.UserSettings.DownloadRetryCooldown * 1000)); + } + } + + public event LogAddedEventHandler LogAdded; + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/Core/LogArgs.cs b/src/BandcampDownloader/Core/LogArgs.cs new file mode 100644 index 00000000..e4e4955c --- /dev/null +++ b/src/BandcampDownloader/Core/LogArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace BandcampDownloader { + + internal class LogArgs: EventArgs { + public LogType LogType { get; private set; } + public String Message { get; private set; } + + public LogArgs(String message, LogType logType) { + Message = message; + LogType = logType; + } + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/Core/PlaylistCreator.cs b/src/BandcampDownloader/Core/PlaylistCreator.cs new file mode 100644 index 00000000..771745df --- /dev/null +++ b/src/BandcampDownloader/Core/PlaylistCreator.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Text; +using PlaylistsNET.Content; +using PlaylistsNET.Models; + +namespace BandcampDownloader { + + internal class PlaylistCreator { + /// + /// The album. + /// + private readonly Album _album; + + /// + /// Initializes a new instance of PlaylistCreator. + /// + /// + public PlaylistCreator(Album album) { + _album = album; + } + + /// + /// Saves the playlist to a file. + /// + public void SavePlaylistToFile() { + String fileContent; + + switch (App.UserSettings.PlaylistFormat) { + case PlaylistFormat.m3u: + fileContent = CreateM3uPlaylist(); + break; + case PlaylistFormat.pls: + fileContent = CreatePlsPlaylist(); + break; + case PlaylistFormat.wpl: + fileContent = CreateWplPlaylist(); + break; + case PlaylistFormat.zpl: + fileContent = CreateZplPlaylist(); + break; + default: + throw new NotImplementedException(); + } + + File.WriteAllText(_album.PlaylistPath, fileContent, Encoding.UTF8); + } + + /// + /// Returns the playlist in m3u format. + /// + private String CreateM3uPlaylist() { + var playlist = new M3uPlaylist() { + IsExtended = App.UserSettings.M3uExtended, + }; + + foreach (Track track in _album.Tracks) { + playlist.PlaylistEntries.Add(new M3uPlaylistEntry() { + Album = _album.Title, + AlbumArtist = _album.Artist, + Duration = TimeSpan.FromSeconds(track.Duration), + Path = Path.GetFileName(track.Path), + Title = track.Title, + }); + } + + return new M3uContent().ToText(playlist); + } + + /// + /// Returns the playlist in pls format. + /// + private String CreatePlsPlaylist() { + var playlist = new PlsPlaylist(); + + foreach (Track track in _album.Tracks) { + playlist.PlaylistEntries.Add(new PlsPlaylistEntry() { + Length = TimeSpan.FromSeconds(track.Duration), + Path = Path.GetFileName(track.Path), + Title = track.Title, + }); + } + + return new PlsContent().ToText(playlist); + } + + /// + /// Returns the playlist in wpl format. + /// + private String CreateWplPlaylist() { + var playlist = new WplPlaylist() { + Title = _album.Title, + }; + + foreach (Track track in _album.Tracks) { + playlist.PlaylistEntries.Add(new WplPlaylistEntry() { + AlbumArtist = _album.Artist, + AlbumTitle = _album.Title, + Duration = TimeSpan.FromSeconds(track.Duration), + Path = Path.GetFileName(track.Path), + TrackArtist = _album.Artist, + TrackTitle = track.Title, + }); + } + + return new WplContent().ToText(playlist); + } + + /// + /// Returns the playlist in zpl format. + /// + private String CreateZplPlaylist() { + var playlist = new ZplPlaylist() { + Title = _album.Title, + }; + + foreach (Track track in _album.Tracks) { + playlist.PlaylistEntries.Add(new ZplPlaylistEntry() { + AlbumArtist = _album.Artist, + AlbumTitle = _album.Title, + Duration = TimeSpan.FromSeconds(track.Duration), + Path = Path.GetFileName(track.Path), + TrackArtist = _album.Artist, + TrackTitle = track.Title, + }); + } + + return new ZplContent().ToText(playlist); + } + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/Helpers/FileHelper.cs b/src/BandcampDownloader/Helpers/FileHelper.cs index d0c3fcde..03479d4e 100644 --- a/src/BandcampDownloader/Helpers/FileHelper.cs +++ b/src/BandcampDownloader/Helpers/FileHelper.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Threading.Tasks; namespace BandcampDownloader { @@ -12,12 +13,12 @@ internal static class FileHelper { /// The protocol method to use in order to retrieve the file /// size. /// The size of the file located at the provided URL. - public static long GetFileSize(String url, String protocolMethod) { + public static async Task GetFileSizeAsync(String url, String protocolMethod) { WebRequest webRequest = HttpWebRequest.Create(url); webRequest.Method = protocolMethod; long fileSize; try { - using (WebResponse webResponse = webRequest.GetResponse()) { + using (WebResponse webResponse = await webRequest.GetResponseAsync()) { fileSize = webResponse.ContentLength; } } catch (Exception e) { @@ -26,4 +27,4 @@ public static long GetFileSize(String url, String protocolMethod) { return fileSize; } } -} \ No newline at end of file +} diff --git a/src/BandcampDownloader/Helpers/PlaylistHelper.cs b/src/BandcampDownloader/Helpers/PlaylistHelper.cs deleted file mode 100644 index 84614abc..00000000 --- a/src/BandcampDownloader/Helpers/PlaylistHelper.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; -using PlaylistsNET.Content; -using PlaylistsNET.Models; - -namespace BandcampDownloader { - - internal static class PlaylistHelper { - - /// - /// Saves the playlist file for the specified album in the specified folder. - /// - /// The album relative of the playlist. - /// The folder where the playlist should be stored. - public static void SavePlaylistForAlbum(Album album, String folderPath) { - String fileContent; - - switch (App.UserSettings.PlaylistFormat) { - case PlaylistFormat.m3u: - fileContent = CreateM3uPlaylist(album); - break; - case PlaylistFormat.pls: - fileContent = CreatePlsPlaylist(album); - break; - case PlaylistFormat.wpl: - fileContent = CreateWplPlaylist(album); - break; - case PlaylistFormat.zpl: - fileContent = CreateZplPlaylist(album); - break; - default: - throw new NotImplementedException(); - } - - File.WriteAllText(ComputeFilePath(album, folderPath), fileContent); - } - - /// - /// Returns the full path where the playlist file should be saved. - /// - /// The album relative of the playlist. - /// The folder where the playlist should be stored. - private static String ComputeFilePath(Album album, String folderPath) { - String fileExt = GetFileExtension(); - - // Compute paths where to save artwork - String filePath = folderPath + "\\" + ParseFileName(album) + fileExt; - - if (filePath.Length >= 260) { - // Windows doesn't do well with path + filename >= 260 characters (and path >= 248 characters) - // Path has been shorten to 247 characters before, so we have 12 characters max left for filename.ext - int fileNameMaxLength = 12 - fileExt.Length; - filePath = folderPath + "\\" + ParseFileName(album).Substring(0, fileNameMaxLength) + fileExt; - } - - return filePath; - } - - /// - /// Returns the playlist in m3u format for the specified album. - /// - /// The album relative of the playlist. - private static String CreateM3uPlaylist(Album album) { - var playlist = new M3uPlaylist() { - IsExtended = App.UserSettings.M3uExtended, - }; - - foreach (Track track in album.Tracks) { - playlist.PlaylistEntries.Add(new M3uPlaylistEntry() { - Album = album.Title, - AlbumArtist = album.Artist, - Duration = TimeSpan.FromSeconds(track.Duration), - Path = Path.GetFileName(track.Path), - Title = track.Title, - }); - } - - return new M3uContent().ToText(playlist); - } - - /// - /// Returns the playlist in pls format for the specified album. - /// - /// The album relative of the playlist. - private static String CreatePlsPlaylist(Album album) { - var playlist = new PlsPlaylist(); - - foreach (Track track in album.Tracks) { - playlist.PlaylistEntries.Add(new PlsPlaylistEntry() { - Length = TimeSpan.FromSeconds(track.Duration), - Path = Path.GetFileName(track.Path), - Title = track.Title, - }); - } - - return new PlsContent().ToText(playlist); - } - - /// - /// Returns the playlist in wpl format for the specified album. - /// - /// The album relative of the playlist. - private static String CreateWplPlaylist(Album album) { - var playlist = new WplPlaylist() { - Title = album.Title, - }; - - foreach (Track track in album.Tracks) { - playlist.PlaylistEntries.Add(new WplPlaylistEntry() { - AlbumArtist = album.Artist, - AlbumTitle = album.Title, - Duration = TimeSpan.FromSeconds(track.Duration), - Path = Path.GetFileName(track.Path), - TrackArtist = album.Artist, - TrackTitle = track.Title, - }); - } - - return new WplContent().ToText(playlist); - } - - /// - /// Returns the playlist in zpl format for the specified album. - /// - /// The album relative of the playlist. - private static String CreateZplPlaylist(Album album) { - var playlist = new ZplPlaylist() { - Title = album.Title, - }; - - foreach (Track track in album.Tracks) { - playlist.PlaylistEntries.Add(new ZplPlaylistEntry() { - AlbumArtist = album.Artist, - AlbumTitle = album.Title, - Duration = TimeSpan.FromSeconds(track.Duration), - Path = Path.GetFileName(track.Path), - TrackArtist = album.Artist, - TrackTitle = track.Title, - }); - } - - return new ZplContent().ToText(playlist); - } - - /// - /// Returns the file extension to be used for the playlist, depending of the type of playlist defined in UserSettings. - /// - private static String GetFileExtension() { - switch (App.UserSettings.PlaylistFormat) { - case PlaylistFormat.m3u: - return ".m3u"; - case PlaylistFormat.pls: - return ".pls"; - case PlaylistFormat.wpl: - return ".wpl"; - case PlaylistFormat.zpl: - return ".zpl"; - default: - throw new NotImplementedException(); - } - } - - /// - /// Returns the file name to be used for the playlist file of the specified album from the file name format saved - /// in the UserSettings, by replacing the placeholders strings with their corresponding values. - /// - /// The album relative of the playlist. - private static String ParseFileName(Album album) { - String fileName = App.UserSettings.PlaylistFileNameFormat - .Replace("{year}", album.ReleaseDate.Year.ToString()) - .Replace("{month}", album.ReleaseDate.Month.ToString("00")) - .Replace("{day}", album.ReleaseDate.Day.ToString("00")) - .Replace("{album}", album.Title) - .Replace("{artist}", album.Artist); - return fileName.ToAllowedFileName(); - } - } -} \ No newline at end of file diff --git a/src/BandcampDownloader/Helpers/ProxyHelper.cs b/src/BandcampDownloader/Helpers/ProxyHelper.cs new file mode 100644 index 00000000..26f96c8f --- /dev/null +++ b/src/BandcampDownloader/Helpers/ProxyHelper.cs @@ -0,0 +1,30 @@ +using System; +using System.Net; + +namespace BandcampDownloader { + + internal class ProxyHelper { + + /// + /// Sets the proxy of the specified WebClient according to the UserSettings. + /// + /// The WebClient to modify. + public static void SetProxy(WebClient webClient) { + switch (App.UserSettings.Proxy) { + case ProxyType.None: + webClient.Proxy = null; + break; + case ProxyType.System: + if (webClient.Proxy != null) { + webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; + } + break; + case ProxyType.Manual: + webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); + break; + default: + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/Model/Album.cs b/src/BandcampDownloader/Model/Album.cs index 76d86edf..faa57ecb 100644 --- a/src/BandcampDownloader/Model/Album.cs +++ b/src/BandcampDownloader/Model/Album.cs @@ -39,6 +39,11 @@ public Boolean HasArtwork { /// public String Path { get; private set; } + /// + /// The local path (full path with file name) where the playlist file should be saved. + /// + public String PlaylistPath { get; private set; } + /// /// The release date of the album. /// @@ -63,10 +68,30 @@ public Album(String artist, String artworkUrl, DateTime releaseDate, String titl ReleaseDate = releaseDate; Title = title; // Must be done after other properties are filled! - Path = ParseFolderPath(App.UserSettings.DownloadsPath); + Path = ParseFolderPath(); + // Must be done after Path is set! + PlaylistPath = ParsePlaylistPath(); SetArtworkPaths(); } + /// + /// Returns the file extension to be used for the playlist, depending of the type of playlist defined in UserSettings. + /// + private static String GetPlaylistFileExtension() { + switch (App.UserSettings.PlaylistFormat) { + case PlaylistFormat.m3u: + return ".m3u"; + case PlaylistFormat.pls: + return ".pls"; + case PlaylistFormat.wpl: + return ".wpl"; + case PlaylistFormat.zpl: + return ".zpl"; + default: + throw new NotImplementedException(); + } + } + /// /// Returns the file name to be used for the cover art of the specified album from the file name format saved in /// the UserSettings, by replacing the placeholders strings with their corresponding values. @@ -86,20 +111,55 @@ private String ParseCoverArtFileName() { /// Returns the folder path from the specified path format, by replacing the placeholders strings with their /// corresponding values. If the path is too long (> 247 characters), it will be stripped. /// - /// The download path to parse. - private String ParseFolderPath(String downloadPath) { - downloadPath = downloadPath.Replace("{year}", ReleaseDate.Year.ToString().ToAllowedFileName()); - downloadPath = downloadPath.Replace("{month}", ReleaseDate.Month.ToString("00").ToAllowedFileName()); - downloadPath = downloadPath.Replace("{day}", ReleaseDate.Day.ToString("00").ToAllowedFileName()); - downloadPath = downloadPath.Replace("{artist}", Artist.ToAllowedFileName()); - downloadPath = downloadPath.Replace("{album}", Title.ToAllowedFileName()); - - if (downloadPath.Length >= 248) { + private String ParseFolderPath() { + String path = App.UserSettings.DownloadsPath; + path = path.Replace("{year}", ReleaseDate.Year.ToString().ToAllowedFileName()); + path = path.Replace("{month}", ReleaseDate.Month.ToString("00").ToAllowedFileName()); + path = path.Replace("{day}", ReleaseDate.Day.ToString("00").ToAllowedFileName()); + path = path.Replace("{artist}", Artist.ToAllowedFileName()); + path = path.Replace("{album}", Title.ToAllowedFileName()); + + if (path.Length >= 248) { // Windows doesn't do well with path >= 248 characters (and path + filename >= 260 characters) - downloadPath = downloadPath.Substring(0, 247); + path = path.Substring(0, 247); + } + + return path; + } + + /// + /// Returns the file name to be used for the playlist file of the specified album from the file name format saved + /// in the UserSettings, by replacing the placeholders strings with their corresponding values. + /// + private String ParsePlaylistFileName() { + String fileName = App.UserSettings.PlaylistFileNameFormat + .Replace("{year}", ReleaseDate.Year.ToString()) + .Replace("{month}", ReleaseDate.Month.ToString("00")) + .Replace("{day}", ReleaseDate.Day.ToString("00")) + .Replace("{album}", Title) + .Replace("{artist}", Artist); + return fileName.ToAllowedFileName(); + } + + /// + /// Returns the path to be used for the playlist file from the file name format saved in the UserSettings, by + /// replacing the placeholders strings with their corresponding values. If the path is too long (> 259 + /// characters), it will be stripped. + /// + private String ParsePlaylistPath() { + String fileExt = GetPlaylistFileExtension(); + + // Compute paths where to save artwork + String filePath = Path + "\\" + ParsePlaylistFileName() + fileExt; + + if (filePath.Length >= 260) { + // Windows doesn't do well with path + filename >= 260 characters (and path >= 248 characters) + // Path has been shorten to 247 characters before, so we have 12 characters max left for "\filename.ext", so 11 character max for "filename.ext" + int fileNameMaxLength = 11 - fileExt.Length; + filePath = Path + "\\" + ParsePlaylistFileName().Substring(0, fileNameMaxLength) + fileExt; } - return downloadPath; + return filePath; } /// @@ -118,10 +178,10 @@ private void SetArtworkPaths() { if (ArtworkTempPath.Length >= 260 || ArtworkPath.Length >= 260) { // Windows doesn't do well with path + filename >= 260 characters (and path >= 248 characters) - // Path has been shorten to 247 characters before, so we have 12 characters max left for filename.ext + // Path has been shorten to 247 characters before, so we have 12 characters max left for "\filename.ext", so 11 character max for "filename.ext" // There may be only one path needed to shorten, but it's better to use the same file name in both places - int fileNameInTempMaxLength = 12 - randomNumber.Length - artworkFileExt.Length; - int fileNameInFolderMaxLength = 12 - artworkFileExt.Length; + int fileNameInTempMaxLength = 11 - randomNumber.Length - artworkFileExt.Length; + int fileNameInFolderMaxLength = 11 - artworkFileExt.Length; ArtworkTempPath = System.IO.Path.GetTempPath() + "\\" + ParseCoverArtFileName().Substring(0, fileNameInTempMaxLength) + randomNumber + artworkFileExt; ArtworkPath = Path + "\\" + ParseCoverArtFileName().Substring(0, fileNameInFolderMaxLength) + artworkFileExt; } diff --git a/src/BandcampDownloader/Model/DownloadProgress.cs b/src/BandcampDownloader/Model/DownloadProgress.cs deleted file mode 100644 index b990a221..00000000 --- a/src/BandcampDownloader/Model/DownloadProgress.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace BandcampDownloader { - - internal class DownloadProgress { - public long BytesReceived { get; set; } - public String FileUrl { get; set; } - - public DownloadProgress(String fileUrl, long bytesReceived) { - FileUrl = fileUrl; - BytesReceived = bytesReceived; - } - } -} \ No newline at end of file diff --git a/src/BandcampDownloader/Model/FileType.cs b/src/BandcampDownloader/Model/FileType.cs new file mode 100644 index 00000000..a5a708f0 --- /dev/null +++ b/src/BandcampDownloader/Model/FileType.cs @@ -0,0 +1,7 @@ +namespace BandcampDownloader { + + internal enum FileType { + Artwork, + Track + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/Model/Track.cs b/src/BandcampDownloader/Model/Track.cs index c5c68689..8ec9fee7 100644 --- a/src/BandcampDownloader/Model/Track.cs +++ b/src/BandcampDownloader/Model/Track.cs @@ -79,8 +79,8 @@ private String ParseTrackFilePath() { String path = Album.Path + "\\" + fileName; if (path.Length >= 260) { // Windows doesn't do well with path + filename >= 260 characters (and path >= 248 characters) - // album.Path has been shorten to 247 characters before, so we have 12 characters max left for filename.ext - int fileNameMaxLength = 12 - System.IO.Path.GetExtension(path).Length; + // album.Path has been shorten to 247 characters before, so we have 12 characters max left for "\filename.ext", so 11 character max for "filename.ext" + int fileNameMaxLength = 11 - System.IO.Path.GetExtension(path).Length; path = Album.Path + "\\" + fileName.Substring(0, fileNameMaxLength) + System.IO.Path.GetExtension(path); } diff --git a/src/BandcampDownloader/Properties/AssemblyInfo.cs b/src/BandcampDownloader/Properties/AssemblyInfo.cs index f47b2df1..8c91246c 100644 --- a/src/BandcampDownloader/Properties/AssemblyInfo.cs +++ b/src/BandcampDownloader/Properties/AssemblyInfo.cs @@ -51,6 +51,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("0.2.8.2")] -[assembly: AssemblyFileVersion("0.2.8.2")] +[assembly: AssemblyVersion("0.2.9.0")] +[assembly: AssemblyFileVersion("0.2.9.0")] [assembly: GuidAttribute("8C171C7F-9BAC-4EC0-A287-59908B48953F")] diff --git a/src/BandcampDownloader/Properties/Resources.Designer.cs b/src/BandcampDownloader/Properties/Resources.Designer.cs index 8e761dc0..45084ba2 100644 --- a/src/BandcampDownloader/Properties/Resources.Designer.cs +++ b/src/BandcampDownloader/Properties/Resources.Designer.cs @@ -106,6 +106,15 @@ internal static string buttonCheckForUpdates { } } + /// + /// Looks up a localized string similar to _Download update. + /// + internal static string buttonDownloadUpdate { + get { + return ResourceManager.GetString("buttonDownloadUpdate", resourceCulture); + } + } + /// /// Looks up a localized string similar to _Settings. /// @@ -160,6 +169,15 @@ internal static string buttonStop { } } + /// + /// Looks up a localized string similar to Could not download changelog from {0}. + /// + internal static string changelogDownloadError { + get { + return ResourceManager.GetString("changelogDownloadError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Check for _updates at startup. /// @@ -538,6 +556,15 @@ internal static string labelButtonResetSettings { } } + /// + /// Looks up a localized string similar to Changelog. + /// + internal static string labelChangelogTitle { + get { + return ResourceManager.GetString("labelChangelogTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Comm_ents. /// @@ -574,6 +601,15 @@ internal static string labelCoverArtInTagsMaxSize { } } + /// + /// Looks up a localized string similar to Current version:. + /// + internal static string labelCurrentVersion { + get { + return ResourceManager.GetString("labelCurrentVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Download _max tries. /// @@ -709,15 +745,6 @@ internal static string labelVerboseLogInfo { } } - /// - /// Looks up a localized string similar to Click to go to official project website:. - /// - internal static string labelVersion_ToolTip_Part { - get { - return ResourceManager.GetString("labelVersion_ToolTip_Part", resourceCulture); - } - } - /// /// Looks up a localized string similar to Could not check for updates. /// @@ -728,7 +755,7 @@ internal static string labelVersionError { } /// - /// Looks up a localized string similar to A new version ({0}) is available. + /// Looks up a localized string similar to A new version is available. /// internal static string labelVersionNewUpdateAvailable { get { @@ -974,6 +1001,15 @@ internal static string TagRemoveAction_Empty { } } + /// + /// Looks up a localized string similar to Go to website. + /// + internal static string textBlockGoToWebsite { + get { + return ResourceManager.GetString("textBlockGoToWebsite", resourceCulture); + } + } + /// /// Looks up a localized string similar to Help translate. /// @@ -1108,5 +1144,14 @@ internal static string windowSettings_Title { return ResourceManager.GetString("windowSettings_Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Update. + /// + internal static string windowUpdate_Title { + get { + return ResourceManager.GetString("windowUpdate_Title", resourceCulture); + } + } } } diff --git a/src/BandcampDownloader/Properties/Resources.de.resx b/src/BandcampDownloader/Properties/Resources.de.resx index 040d35be..7133696e 100644 --- a/src/BandcampDownloader/Properties/Resources.de.resx +++ b/src/BandcampDownloader/Properties/Resources.de.resx @@ -117,7 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ... @@ -130,6 +129,9 @@ _Jetzt suchen + + _Download update + _Einstellungen @@ -148,6 +150,9 @@ _Abbrechen + + Could not download changelog from {0} + Beim start auf _updates prüfen @@ -197,7 +202,7 @@ Wenn ausgewählt wird ein Benachrichtigungston nach erfolgreichem Download abgespielt. - Use _extended format (for M3U only) + Use _extended format (for M3U only) If checked, the extended format (directives prefaced by the # character) will be used when creating M3U playlists. @@ -264,6 +269,9 @@ Das Herunterladen geht schneller wenn diese Option ausgeschaltet ist. _Zurücksetzen + + Changelog + Komm_entare @@ -276,6 +284,9 @@ Das Herunterladen geht schneller wenn diese Option ausgeschaltet ist. Maximale Größe (p_x) + + Current version: + _Maximale Downloadversuche @@ -321,14 +332,11 @@ Das Herunterladen geht schneller wenn diese Option ausgeschaltet ist. (Die Logdatei ist immer ausführlich) - - Klicken um zur Homepage diese Projektes zu gelangen: - Konnte nicht auf Updates prüfen - Eine neue Version ({0}) ist verfügbar + Eine neue Version ist verfügbar Album _Veröffentlichungsjahr @@ -466,6 +474,9 @@ Empfohlender Wert = 4 - {album} Wird mit dem albumnamen ersetzt - {year}, {month} und {day} werden mit dem Erscheinungsdatum ersetzt + + Go to website + You can use placeholders to customize the file name: - {artist} will be replaced by the album artist @@ -482,4 +493,7 @@ seine alben herunterzuladen. Einstellungen + + Update + \ No newline at end of file diff --git a/src/BandcampDownloader/Properties/Resources.fr.resx b/src/BandcampDownloader/Properties/Resources.fr.resx index be31ad32..c9d1a08b 100644 --- a/src/BandcampDownloader/Properties/Resources.fr.resx +++ b/src/BandcampDownloader/Properties/Resources.fr.resx @@ -117,7 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ... @@ -130,6 +129,9 @@ Vérifier _maintenant + + _Télécharger la mise à jour + _Paramètres @@ -148,6 +150,9 @@ _Annuler + + Impossible de télécharger les notes de version depuis {0} + Vérifier les mises à jour au _démarrage @@ -264,6 +269,9 @@ Décocher cette option permet de gagner du temps. _Réinitialiser + + Notes de version + C_ommentaires @@ -276,6 +284,9 @@ Décocher cette option permet de gagner du temps. Taille maximale (p_x) + + Version actuelle : + Nombre de téléchargements _maximum @@ -321,14 +332,11 @@ Décocher cette option permet de gagner du temps. (le fichier log est toujours verbeux) - - Cliquer pour se rendre sur le site web du projet : - Impossible de vérifier les mises à jour - Une nouvelle version ({0}) est disponible + Une nouvelle version est disponible Année _de parution de l'album @@ -466,6 +474,9 @@ Valeur conseillée = 4 - {album} sera remplacé par le nom de l'album - {year} (année), {month} (mois) et {day} (jour) seront remplacés par la date de parution de l'album + + Site web + Vous pouvez utiliser des paramètres de substitution pour personnaliser le nom des fichiers : - {artist} sera remplacé par l'interprète de l'album @@ -482,4 +493,7 @@ de leurs albums. Paramètres + + Mise à jour + \ No newline at end of file diff --git a/src/BandcampDownloader/Properties/Resources.it.resx b/src/BandcampDownloader/Properties/Resources.it.resx index a615fc3c..585d54af 100644 --- a/src/BandcampDownloader/Properties/Resources.it.resx +++ b/src/BandcampDownloader/Properties/Resources.it.resx @@ -117,7 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ... @@ -130,6 +129,9 @@ Controlla _ora + + _Download update + _Impostazioni @@ -148,6 +150,9 @@ _Annulla + + Could not download changelog from {0} + _Cerca aggiornamenti all'avvio @@ -264,6 +269,9 @@ Togli la spunta da quest'impostazione per risparmiare tempo. _Ripristina impostazioni + + Changelog + Comm_enti @@ -276,6 +284,9 @@ Togli la spunta da quest'impostazione per risparmiare tempo. Dimensione massima (p_x) + + Current version: + _Massimo numero di tentativi @@ -321,14 +332,11 @@ Togli la spunta da quest'impostazione per risparmiare tempo. (il file registro è sempre esteso) - - Fai click qui per andare sul sito ufficiale del progetto: - Non è possibile trovare aggiornamenti. - È disponibile una nuova versione ({0}) + È disponibile una nuova versione Anno _di rilascio album @@ -465,6 +473,9 @@ Valore consigliato = 4 - {album} è sostituito col nome dell'album; - {year}, {month} e {day} sono sostituiti con la data di rilascio. + + Go to website + You can use placeholders to customize the file name: - {artist} will be replaced by the album artist @@ -480,4 +491,7 @@ Incolla l'URL di una pagina artista http://[artista].bandcamp.com e spunta “ Impostazioni + + Update + \ No newline at end of file diff --git a/src/BandcampDownloader/Properties/Resources.resx b/src/BandcampDownloader/Properties/Resources.resx index 9c14d547..13085d27 100644 --- a/src/BandcampDownloader/Properties/Resources.resx +++ b/src/BandcampDownloader/Properties/Resources.resx @@ -145,6 +145,9 @@ Check no_w + + _Download update + _Settings @@ -163,6 +166,9 @@ _Cancel + + Could not download changelog from {0} + Check for _updates at startup @@ -279,6 +285,9 @@ Uncheck this option to save some time. _Reset settings + + Changelog + Comm_ents @@ -291,6 +300,9 @@ Uncheck this option to save some time. Max size (p_x) + + Current version: + Download _max tries @@ -336,14 +348,11 @@ Uncheck this option to save some time. (the log file is always verbose) - - Click to go to official project website: - Could not check for updates - A new version ({0}) is available + A new version is available Album release _year @@ -481,6 +490,9 @@ Recommended value = 4 - {album} will be replaced by the album name - {year}, {month} and {day} will be replaced by the album release date + + Go to website + You can use placeholders to customize the file name: - {artist} will be replaced by the album artist @@ -496,4 +508,7 @@ Paste artist pages: http://[artist].bandcamp.com and check "☑ Download artist Settings + + Update + \ No newline at end of file diff --git a/src/BandcampDownloader/UI/Dialogs/Settings/UserControlSettingsGeneral.xaml.cs b/src/BandcampDownloader/UI/Dialogs/Settings/UserControlSettingsGeneral.xaml.cs index 0d0586c6..dbf237da 100644 --- a/src/BandcampDownloader/UI/Dialogs/Settings/UserControlSettingsGeneral.xaml.cs +++ b/src/BandcampDownloader/UI/Dialogs/Settings/UserControlSettingsGeneral.xaml.cs @@ -57,9 +57,11 @@ private void ButtonCheckForUpdates_Click(object sender, RoutedEventArgs e) { Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; if (currentVersion.CompareTo(latestVersion) < 0) { // The latest version is newer than the current one - if (MessageBox.Show(String.Format(Properties.Resources.messageBoxUpdateAvailable, currentVersion, latestVersion), "Bandcamp Downloader", MessageBoxButton.YesNo, MessageBoxImage.Information, MessageBoxResult.Yes) == MessageBoxResult.Yes) { - Process.Start(Constants.ProjectWebsite); - } + var windowUpdate = new WindowUpdate() { + ShowInTaskbar = true, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + }; + windowUpdate.Show(); } else { MessageBox.Show(String.Format(Properties.Resources.messageBoxNoUpdateAvailable, currentVersion), "Bandcamp Downloader", MessageBoxButton.OK, MessageBoxImage.Information); } diff --git a/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml b/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml new file mode 100644 index 00000000..51e42155 --- /dev/null +++ b/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml.cs b/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml.cs new file mode 100644 index 00000000..3eaaf709 --- /dev/null +++ b/src/BandcampDownloader/UI/Dialogs/Update/UserControlChangelog.xaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace BandcampDownloader { + + public partial class UserControlChangelog: UserControl { + + public UserControlChangelog() { + InitializeComponent(); + Loaded += OnLoaded; + } + + /// + /// Downloads the changelog file and returns its content. + /// + private async Task DownloadChangelogAsync() { + String changelog; + using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { + ProxyHelper.SetProxy(webClient); + changelog = await webClient.DownloadStringTaskAsync(Constants.ChangelogUrl); + } + + return changelog; + } + + private async void OnLoaded(object sender, RoutedEventArgs e) { + String changelog; + try { + changelog = await DownloadChangelogAsync(); + } catch { + changelog = String.Format(Properties.Resources.changelogDownloadError, Constants.ChangelogUrl); + } + + markdownViewer.Markdown = changelog; + } + + private void OpenHyperlink(object sender, ExecutedRoutedEventArgs e) { + Process.Start(e.Parameter.ToString()); + } + } +} \ No newline at end of file diff --git a/src/BandcampDownloader/UI/Dialogs/Update/WindowUpdate.xaml b/src/BandcampDownloader/UI/Dialogs/Update/WindowUpdate.xaml new file mode 100644 index 00000000..e691cee6 --- /dev/null +++ b/src/BandcampDownloader/UI/Dialogs/Update/WindowUpdate.xaml @@ -0,0 +1,78 @@ + + + + + + + + private Boolean _activeDownloads = false; /// - /// Used to update the downloaded bytes / files count on the UI. - /// - private ConcurrentQueue _downloadProgresses; - /// - /// The files to download, or being downloaded, or downloaded. Used to compute the current received bytes and the total bytes to download. + /// The DownloadManager used to download albums. /// - private List _filesDownload; + private DownloadManager _downloadManager; /// /// Used to compute and display the download speed. /// @@ -50,18 +36,10 @@ public partial class WindowMain: Window { /// private long _lastTotalReceivedBytes = 0; /// - /// Used when user clicks on 'Cancel' to abort all current downloads. - /// - private List _pendingDownloads; - /// - /// Used when user clicks on 'Cancel' to manage the cancelation (UI...). + /// Used when user clicks on 'Cancel' to manage the cancellation (UI...). /// private Boolean _userCancelled; - #endregion - - #region Constructor - public WindowMain() { // Save DataContext for bindings (must be called before initializing UI) DataContext = App.UserSettings; @@ -89,584 +67,100 @@ public WindowMain() { //+ "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 ; #endif } - #endregion - - #region Methods - - /// - /// Displays a message if a new version is available. - /// - private void CheckForUpdates() { - Version latestVersion = null; - try { - latestVersion = UpdatesHelper.GetLatestVersion(); - } catch (CouldNotCheckForUpdatesException) { - Dispatcher.BeginInvoke(new Action(() => { - labelVersion.Content += " - " + Properties.Resources.labelVersionError; - })); - return; + private void ButtonBrowse_Click(object sender, RoutedEventArgs e) { + 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(); } + } - Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; - if (currentVersion.CompareTo(latestVersion) < 0) { - // The latest version is newer than the current one - Dispatcher.BeginInvoke(new Action(() => { - labelVersion.Content = String.Format(Properties.Resources.labelVersionNewUpdateAvailable, latestVersion); - })); - } + private void ButtonOpenSettingsWindow_Click(object sender, RoutedEventArgs e) { + var windowSettings = new WindowSettings(_activeDownloads) { + Owner = this, + ShowInTaskbar = false, + }; + windowSettings.ShowDialog(); } - /// - /// Downloads an album. - /// - /// The album to download. - private void DownloadAlbum(Album album) { - if (_userCancelled) { - // Abort + 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; } - // Create directory to place track files - try { - Directory.CreateDirectory(album.Path); - } catch { - Log("An error occured when creating the album folder. Make sure you have the rights to write files in the folder you chose", LogType.Error); - return; - } + // Set controls to "downloading..." state + _activeDownloads = true; + UpdateControlsState(true); - TagLib.Picture artwork = null; + Log("Starting download...", LogType.Info); - // Download artwork - if ((App.UserSettings.SaveCoverArtInTags || App.UserSettings.SaveCoverArtInFolder) && album.HasArtwork) { - artwork = DownloadCoverArt(album); - } + await StartDownloadAsync(); - // Download & tag tracks - var tasks = new Task[album.Tracks.Count]; - Boolean[] tracksDownloaded = new Boolean[album.Tracks.Count]; - for (int i = 0; i < album.Tracks.Count; i++) { - // Temporarily save the index or we will have a race condition exception when i hits its maximum value - int currentIndex = i; - tasks[currentIndex] = Task.Factory.StartNew(() => tracksDownloaded[currentIndex] = DownloadAndTagTrack(album, album.Tracks[currentIndex], artwork)); + if (_userCancelled) { + // Display message if user cancelled + Log("Downloads cancelled by user", LogType.Info); } - // Wait for all tracks to be downloaded before saying the album is downloaded - Task.WaitAll(tasks); - - // Create playlist file - if (App.UserSettings.CreatePlaylist) { - PlaylistHelper.SavePlaylistForAlbum(album, album.Path); - Log($"Saved playlist for album \"{album.Title}\"", LogType.IntermediateSuccess); - } + // Reset controls to "ready" state + _activeDownloads = false; + _lastTotalReceivedBytes = 0; + UpdateControlsState(false); - if (!_userCancelled) { - // Tasks have not been aborted - if (tracksDownloaded.All(x => x == true)) { - Log($"Successfully downloaded album \"{album.Title}\"", LogType.Success); - } else { - Log($"Finished downloading album \"{album.Title}\". Some tracks were not downloaded", LogType.Success); + if (App.UserSettings.EnableApplicationSounds) { + // Play a sound + try { + new SoundPlayer(@"C:\Windows\Media\Windows Ding.wav").Play(); + } catch { } } } - /// - /// Downloads and tags a track. Returns true if the track has been correctly downloaded; false otherwise. - /// - /// The album of the track to download. - /// The track to download. - /// The cover art. - private Boolean DownloadAndTagTrack(Album album, Track track, TagLib.Picture artwork) { - Log($"Downloading track \"{track.Title}\" from url: {track.Mp3Url}", LogType.VerboseInfo); - - int tries = 0; - Boolean trackDownloaded = false; - - if (File.Exists(track.Path)) { - long length = new FileInfo(track.Path).Length; - foreach (TrackFile trackFile in _filesDownload) { - if (track.Mp3Url == trackFile.Url && - trackFile.Size > length - (trackFile.Size * App.UserSettings.AllowedFileSizeDifference) && - trackFile.Size < length + (trackFile.Size * App.UserSettings.AllowedFileSizeDifference)) { - Log($"Track already exists within allowed file size range: track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\" - Skipping download!", LogType.IntermediateSuccess); - return false; - } - } + private void ButtonStop_Click(object sender, RoutedEventArgs e) { + if (MessageBox.Show(Properties.Resources.messageBoxCancelDownloads, "Bandcamp Downloader", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) != MessageBoxResult.Yes) { + return; } - do { - var doneEvent = new AutoResetEvent(false); - - using (var webClient = new WebClient()) { - switch (App.UserSettings.Proxy) { - case ProxyType.None: - webClient.Proxy = null; - break; - case ProxyType.System: - if (webClient.Proxy != null) { - webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; - } - break; - case ProxyType.Manual: - webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); - break; - default: - throw new NotImplementedException(); // Shouldn't happen - } - - // Update progress bar when downloading - webClient.DownloadProgressChanged += (s, e) => { - _downloadProgresses.Enqueue(new DownloadProgress(track.Mp3Url, e.BytesReceived)); - UpdateProgress(); - }; - - // Warn & tag when downloaded - webClient.DownloadFileCompleted += (s, e) => { - if (!e.Cancelled && e.Error == null) { - trackDownloaded = true; - - if (App.UserSettings.ModifyTags) { - // Tag (ID3) the file when downloaded - var tagFile = TagLib.File.Create(track.Path); - tagFile = TagHelper.UpdateArtist(tagFile, album.Artist, App.UserSettings.TagArtist); - tagFile = TagHelper.UpdateAlbumArtist(tagFile, album.Artist, App.UserSettings.TagAlbumArtist); - tagFile = TagHelper.UpdateAlbumTitle(tagFile, album.Title, App.UserSettings.TagAlbumTitle); - tagFile = TagHelper.UpdateAlbumYear(tagFile, (uint) album.ReleaseDate.Year, App.UserSettings.TagYear); - tagFile = TagHelper.UpdateTrackNumber(tagFile, (uint) track.Number, App.UserSettings.TagTrackNumber); - tagFile = TagHelper.UpdateTrackTitle(tagFile, track.Title, App.UserSettings.TagTrackTitle); - tagFile = TagHelper.UpdateTrackLyrics(tagFile, track.Lyrics, App.UserSettings.TagLyrics); - tagFile = TagHelper.UpdateComments(tagFile, App.UserSettings.TagComments); - tagFile.Save(); - } - - if (App.UserSettings.SaveCoverArtInTags && artwork != null) { - // Save cover in tags when downloaded - var tagFile = TagLib.File.Create(track.Path); - tagFile.Tag.Pictures = new TagLib.IPicture[1] { artwork }; - tagFile.Save(); - } - - // Note the file as downloaded - TrackFile currentFile = _filesDownload.Where(f => f.Url == track.Mp3Url).First(); - currentFile.Downloaded = true; - Log($"Downloaded track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\"", LogType.IntermediateSuccess); - } else if (!e.Cancelled && e.Error != null) { - if (tries + 1 < App.UserSettings.DownloadMaxTries) { - Log($"Unable to download track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning); - } else { - Log($"Unable to download track \"{Path.GetFileName(track.Path)}\" from album \"{album.Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}", LogType.Error); - } - } // Else the download has been cancelled (by the user) - - tries++; - if (!trackDownloaded && tries < App.UserSettings.DownloadMaxTries) { - WaitForCooldown(tries); - } - - doneEvent.Set(); - }; - - lock (_pendingDownloads) { - if (_userCancelled) { - // Abort - return false; - } - // Register current download - _pendingDownloads.Add(webClient); - // Start download - webClient.DownloadFileAsync(new Uri(track.Mp3Url), track.Path); - } - // Wait for download to be finished - doneEvent.WaitOne(); - lock (_pendingDownloads) { - _pendingDownloads.Remove(webClient); - } - } - } while (!trackDownloaded && tries < App.UserSettings.DownloadMaxTries); - - return trackDownloaded; - } - - /// - /// Downloads the cover art and returns the one to save in tags. - /// - /// The album to download. - private TagLib.Picture DownloadCoverArt(Album album) { - TagLib.Picture artworkInTags = null; - - int tries = 0; - Boolean artworkDownloaded = false; - - do { - var doneEvent = new AutoResetEvent(false); - - using (var webClient = new WebClient()) { - switch (App.UserSettings.Proxy) { - case ProxyType.None: - webClient.Proxy = null; - break; - case ProxyType.System: - if (webClient.Proxy != null) { - webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; - } - break; - case ProxyType.Manual: - webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); - break; - default: - throw new NotImplementedException(); // Shouldn't happen - } - - // Update progress bar when downloading - webClient.DownloadProgressChanged += (s, e) => { - _downloadProgresses.Enqueue(new DownloadProgress(album.ArtworkUrl, e.BytesReceived)); - UpdateProgress(); - }; - - // Warn when downloaded - webClient.DownloadFileCompleted += (s, e) => { - if (!e.Cancelled && e.Error == null) { - artworkDownloaded = true; - - // Convert/resize artwork to be saved in album folder - if (App.UserSettings.SaveCoverArtInFolder && (App.UserSettings.CoverArtInFolderConvertToJpg || App.UserSettings.CoverArtInFolderResize)) { - var settings = new ResizeSettings(); - if (App.UserSettings.CoverArtInFolderConvertToJpg) { - settings.Format = "jpg"; - settings.Quality = 90; - } - if (App.UserSettings.CoverArtInFolderResize) { - settings.MaxHeight = App.UserSettings.CoverArtInFolderMaxSize; - settings.MaxWidth = App.UserSettings.CoverArtInFolderMaxSize; - } - ImageBuilder.Current.Build(album.ArtworkTempPath, album.ArtworkPath, settings); // Save it to the album folder - } else if (App.UserSettings.SaveCoverArtInFolder) { - File.Copy(album.ArtworkTempPath, album.ArtworkPath, true); - } - - // Convert/resize artwork to be saved in tags - if (App.UserSettings.SaveCoverArtInTags && (App.UserSettings.CoverArtInTagsConvertToJpg || App.UserSettings.CoverArtInTagsResize)) { - var settings = new ResizeSettings(); - if (App.UserSettings.CoverArtInTagsConvertToJpg) { - settings.Format = "jpg"; - settings.Quality = 90; - } - if (App.UserSettings.CoverArtInTagsResize) { - settings.MaxHeight = App.UserSettings.CoverArtInTagsMaxSize; - settings.MaxWidth = App.UserSettings.CoverArtInTagsMaxSize; - } - ImageBuilder.Current.Build(album.ArtworkTempPath, album.ArtworkTempPath, settings); // Save it to %Temp% - } - artworkInTags = new TagLib.Picture(album.ArtworkTempPath) { Description = "Picture" }; - - try { - File.Delete(album.ArtworkTempPath); - } catch { - // Could not delete the file. Nevermind, it's in %Temp% folder... - } - - // Note the file as downloaded - TrackFile currentFile = _filesDownload.Where(f => f.Url == album.ArtworkUrl).First(); - currentFile.Downloaded = true; - Log($"Downloaded artwork for album \"{album.Title}\"", LogType.IntermediateSuccess); - } else if (!e.Cancelled && e.Error != null) { - if (tries < App.UserSettings.DownloadMaxTries) { - Log($"Unable to download artwork for album \"{album.Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning); - } else { - Log($"Unable to download artwork for album \"{album.Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}", LogType.Error); - } - } // Else the download has been cancelled (by the user) - - tries++; - if (!artworkDownloaded && tries < App.UserSettings.DownloadMaxTries) { - WaitForCooldown(tries); - } - - doneEvent.Set(); - }; - - lock (_pendingDownloads) { - if (_userCancelled) { - // Abort - return null; - } - // Register current download - _pendingDownloads.Add(webClient); - // Start download - webClient.DownloadFileAsync(new Uri(album.ArtworkUrl), album.ArtworkTempPath); - } - - // Wait for download to be finished - doneEvent.WaitOne(); - lock (_pendingDownloads) { - _pendingDownloads.Remove(webClient); - } - } - } while (!artworkDownloaded && tries < App.UserSettings.DownloadMaxTries); + _userCancelled = true; + buttonStop.IsEnabled = false; - return artworkInTags; + _downloadManager.CancelDownloads(); } /// - /// Returns the albums located at the specified URLs. + /// Displays a message if a new version is available. /// - /// The URLs. - private List GetAlbums(List urls) { - var albums = new List(); - - foreach (String url in urls) { - Log($"Retrieving album data for {url}", LogType.Info); - - // Retrieve URL HTML source code - String htmlCode = ""; - using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { - switch (App.UserSettings.Proxy) { - case ProxyType.None: - webClient.Proxy = null; - break; - case ProxyType.System: - if (webClient.Proxy != null) { - webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; - } - break; - case ProxyType.Manual: - webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); - break; - default: - throw new NotImplementedException(); // Shouldn't happen - } - - if (_userCancelled) { - // Abort - return new List(); - } - - try { - htmlCode = webClient.DownloadString(url); - } catch { - Log($"Could not retrieve data for {url}", LogType.Error); - continue; - } - } - - // Get info on album - try { - albums.Add(BandcampHelper.GetAlbum(htmlCode)); - } catch { - Log($"Could not retrieve album info for {url}", LogType.Error); - continue; - } + private void CheckForUpdates() { + Version latestVersion; + try { + latestVersion = UpdatesHelper.GetLatestVersion(); + } catch (CouldNotCheckForUpdatesException) { + Dispatcher.BeginInvoke(new Action(() => { + labelNewVersion.Content = Properties.Resources.labelVersionError; + })); + return; } - return albums; - } - - /// - /// Returns the artists discography from any URL (artist, album, track). - /// - /// The URLs. - private List GetArtistDiscography(List urls) { - var albumsUrls = new List(); - - foreach (String url in urls) { - Log($"Retrieving artist discography from {url}", LogType.Info); - - // Retrieve URL HTML source code - String htmlCode = ""; - using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { - switch (App.UserSettings.Proxy) { - case ProxyType.None: - webClient.Proxy = null; - break; - case ProxyType.System: - if (webClient.Proxy != null) { - webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; - } - break; - case ProxyType.Manual: - webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); - break; - default: - throw new NotImplementedException(); // Shouldn't happen - } - - if (_userCancelled) { - // Abort - return new List(); - } - - try { - htmlCode = webClient.DownloadString(url); - } catch { - Log($"Could not retrieve data for {url}", LogType.Error); - continue; - } - } - - // Get artist "music" bandcamp page (http://artist.bandcamp.com/music) - var regex = new Regex("band_url = \"(?.*)\""); - if (!regex.IsMatch(htmlCode)) { - Log($"No discography could be found on {url}. Try to uncheck the \"Download artist discography\" option", LogType.Error); - continue; - } - String artistMusicPage = regex.Match(htmlCode).Groups["url"].Value + "/music"; - - // Retrieve artist "music" page HTML source code - using (var webClient = new WebClient() { Encoding = Encoding.UTF8 }) { - switch (App.UserSettings.Proxy) { - case ProxyType.None: - webClient.Proxy = null; - break; - case ProxyType.System: - if (webClient.Proxy != null) { - webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; - } - break; - case ProxyType.Manual: - webClient.Proxy = new WebProxy(App.UserSettings.ProxyHttpAddress, App.UserSettings.ProxyHttpPort); - break; - default: - throw new NotImplementedException(); // Shouldn't happen - } - - if (_userCancelled) { - // Abort - return new List(); - } - - try { - htmlCode = webClient.DownloadString(artistMusicPage); - } catch { - Log($"Could not retrieve data for {artistMusicPage}", LogType.Error); - continue; - } - } - - // Get albums referred on the page - regex = new Regex("TralbumData.*\n.*url:.*'/music'\n"); - if (!regex.IsMatch(htmlCode)) { - // This seem to be a one-album artist with no "music" page => URL redirects to the unique album URL - albumsUrls.Add(url); - } else { - // We are on a real "music" page - try { - albumsUrls.AddRange(BandcampHelper.GetAlbumsUrl(htmlCode)); - } catch (NoAlbumFoundException) { - Log($"No referred album could be found on {artistMusicPage}. Try to uncheck the \"Download artist discography\" option", LogType.Error); - continue; - } - } + Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (currentVersion.CompareTo(latestVersion) < 0) { + // The latest version is newer than the current one + Dispatcher.BeginInvoke(new Action(() => { + labelNewVersion.Content = Properties.Resources.labelVersionNewUpdateAvailable; + })); } - - return albumsUrls; } - /// - /// Returns the files to download from a list of albums. - /// - /// The albums. - /// True if the cover arts must be downloaded, false otherwise. - private List GetFilesToDownload(List albums, Boolean downloadCoverArt) { - var files = new List(); - foreach (Album album in albums) { - Log($"Computing size for album \"{album.Title}\"...", LogType.Info); - - // Artwork - if (downloadCoverArt && album.HasArtwork) { - if (App.UserSettings.RetrieveFilesSize) { - long size = 0; - Boolean sizeRetrieved = false; - int tries = 0; - do { - if (_userCancelled) { - // Abort - return new List(); - } - - try { - size = FileHelper.GetFileSize(album.ArtworkUrl, "HEAD"); - sizeRetrieved = true; - Log($"Retrieved the size of the cover art file for album \"{album.Title}\"", LogType.VerboseInfo); - } catch { - sizeRetrieved = false; - if (tries + 1 < App.UserSettings.DownloadMaxTries) { - Log($"Failed to retrieve the size of the cover art file for album \"{album.Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning); - } else { - Log($"Failed to retrieve the size of the cover art file for album \"{album.Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}. Progress update may be wrong.", LogType.Error); - } - } - - tries++; - if (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries) { - WaitForCooldown(tries); - } - } while (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries); - files.Add(new TrackFile(album.ArtworkUrl, 0, size)); - } else { - files.Add(new TrackFile(album.ArtworkUrl, 0, 0)); - } - } - - // Tracks - if (App.UserSettings.RetrieveFilesSize) { - var tasks = new Task[album.Tracks.Count]; - for (int i = 0; i < album.Tracks.Count; i++) { - // Temporarily save the index or we will have a race condition exception when i hits its maximum value - int trackIndex = i; - - if (_userCancelled) { - // Abort - return new List(); - } - - tasks[trackIndex] = Task.Factory.StartNew(() => { - long size = 0; - Boolean sizeRetrieved = false; - int tries = 0; - do { - if (_userCancelled) { - // Abort - break; - } - - try { - // Using the HEAD method on tracks urls does not work (Error 405: Method not allowed) - // Surprisingly, using the GET method does not seem to download the whole file, so we will use it to retrieve - // the mp3 sizes - size = FileHelper.GetFileSize(album.Tracks[trackIndex].Mp3Url, "GET"); - sizeRetrieved = true; - Log($"Retrieved the size of the MP3 file for the track \"{album.Tracks[trackIndex].Title}\"", LogType.VerboseInfo); - } catch { - sizeRetrieved = false; - if (tries + 1 < App.UserSettings.DownloadMaxTries) { - Log($"Failed to retrieve the size of the MP3 file for the track \"{album.Tracks[trackIndex].Title}\". Try {tries + 1} of {App.UserSettings.DownloadMaxTries}", LogType.Warning); - } else { - Log($"Failed to retrieve the size of the MP3 file for the track \"{album.Tracks[trackIndex].Title}\". Hit max retries of {App.UserSettings.DownloadMaxTries}. Progress update may be wrong.", LogType.Error); - } - } - - tries++; - if (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries) { - WaitForCooldown(tries); - } - } while (!sizeRetrieved && tries < App.UserSettings.DownloadMaxTries); - files.Add(new TrackFile(album.Tracks[trackIndex].Mp3Url, 0, size)); - }); - } - - // Wait for all tracks size to be retrieved - Task.WaitAll(tasks); - } else { - foreach (Track track in album.Tracks) { - files.Add(new TrackFile(track.Mp3Url, 0, 0)); - } - } - } - - return files; + private void DownloadManager_LogAdded(object sender, LogArgs eventArgs) { + Log(eventArgs.Message, eventArgs.LogType); } /// @@ -686,11 +180,20 @@ private void InitializeLogger() { LogManager.Configuration = config; } + 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 with the specified color. + /// Logs to file and displays the specified message in the log textbox. /// /// The message. - /// The color. + /// The log type. private void Log(String message, LogType logType) { // Log to file Logger logger = LogManager.GetCurrentClassLogger(); @@ -698,278 +201,165 @@ private void Log(String message, LogType logType) { // Log to window if (App.UserSettings.ShowVerboseLog || logType == LogType.Error || logType == LogType.Info || logType == LogType.IntermediateSuccess || logType == LogType.Success) { - Dispatcher.Invoke(new Action(() => { - // 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(); - } - })); + // 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(); + } } } /// - /// Updates the state of the controls. + /// Starts downloads. /// - /// True if the download just started, false if it just stopped. - private void UpdateControlsState(Boolean downloadStarted) { - Dispatcher.Invoke(new Action(() => { - 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.Foreground = new SolidColorBrush((Color) ColorConverter.ConvertFromString("#FF01D328")); // Green - progressBar.IsIndeterminate = false; - progressBar.Value = progressBar.Minimum; - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; - TaskbarItemInfo.ProgressValue = 0; - textBoxDownloadsPath.IsReadOnly = false; - textBoxUrls.IsReadOnly = false; - } - })); - } + private async Task StartDownloadAsync() { + _userCancelled = false; - /// - /// Updates the progress messages and the progressbar. - /// - private void UpdateProgress() { - DateTime now = DateTime.Now; + // Initializes the DownloadManager + _downloadManager = new DownloadManager(textBoxUrls.Text); + _downloadManager.LogAdded += DownloadManager_LogAdded; - _downloadProgresses.TryDequeue(out DownloadProgress downloadProgress); + // Fetch URL to get the files size + await _downloadManager.FetchUrlsAsync(); - // Compute new progress values - TrackFile currentFile = _filesDownload.Where(f => f.Url == downloadProgress.FileUrl).First(); - currentFile.BytesReceived = downloadProgress.BytesReceived; - long totalReceivedBytes = _filesDownload.Sum(f => f.BytesReceived); - long bytesToDownload = _filesDownload.Sum(f => f.Size); - Double downloadedFilesCount = _filesDownload.Count(f => f.Downloaded); - - Double bytesPerSecond; - if (_lastTotalReceivedBytes == 0) { - // First time we update the progress - bytesPerSecond = 0; - _lastTotalReceivedBytes = totalReceivedBytes; - _lastDownloadSpeedUpdate = now; - } else if ((now - _lastDownloadSpeedUpdate).TotalMilliseconds > 500) { - // Last update of progress happened more than 500 milliseconds ago - // We only update the download speed every 500+ milliseconds - bytesPerSecond = - ((Double) (totalReceivedBytes - _lastTotalReceivedBytes)) / - (now - _lastDownloadSpeedUpdate).TotalSeconds; - _lastTotalReceivedBytes = totalReceivedBytes; - _lastDownloadSpeedUpdate = now; - - // Update UI - Dispatcher.Invoke(new Action(() => { - // Update download speed - labelDownloadSpeed.Content = (bytesPerSecond / 1024).ToString("0.0") + " kB/s"; - })); + // 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 } - - // Update UI - Dispatcher.Invoke(new Action(() => { - if (!_userCancelled) { - // 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 { - // 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 WaitForCooldown(int triesNumber) { - if (App.UserSettings.DownloadRetryCooldown != 0) { - Thread.Sleep((int) (Math.Pow(App.UserSettings.DownloadRetryExponent, triesNumber) * App.UserSettings.DownloadRetryCooldown * 1000)); + if (maxProgressBarValue > 0) { + progressBar.IsIndeterminate = false; + progressBar.Maximum = maxProgressBarValue; + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal; } - } - #endregion - - #region Events - - private void ButtonBrowse_Click(object sender, RoutedEventArgs e) { - var dialog = new System.Windows.Forms.FolderBrowserDialog { - Description = Properties.Resources.folderBrowserDialogDescription + // Start timer to update progress on UI + var updateProgressTimer = new DispatcherTimer() { + Interval = TimeSpan.FromMilliseconds(100) }; - 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 give then lose focus on the textbox) - textBoxDownloadsPath.GetBindingExpression(TextBox.TextProperty).UpdateSource(); - } - } + updateProgressTimer.Tick += UpdateProgressTimer_Tick; + updateProgressTimer.Start(); - private void ButtonOpenSettingsWindow_Click(object sender, RoutedEventArgs e) { - var windowSettings = new WindowSettings(_activeDownloads) { - Owner = this, - ShowInTaskbar = false, + // Start timer to update download speed on UI + var updateDownloadSpeedTimer = new DispatcherTimer() { + Interval = TimeSpan.FromMilliseconds(1000) }; - windowSettings.ShowDialog(); - } + updateDownloadSpeedTimer.Tick += UpdateDownloadSpeedTimer_Tick; + updateDownloadSpeedTimer.Start(); - private 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; - } + // Start downloading albums + await _downloadManager.StartDownloadsAsync(); - _userCancelled = false; + // Stop timers + updateProgressTimer.Stop(); + updateDownloadSpeedTimer.Stop(); - _pendingDownloads = new List(); + // Update progress one last time to make sure the downloaded bytes displayed on UI is up-to-date + UpdateProgress(); + } - // Set controls to "downloading..." state - _activeDownloads = true; - UpdateControlsState(true); + /// + /// Updates the state of the controls. + /// + /// True if the download just started; false if it just stopped. + private void UpdateControlsState(Boolean downloadStarted) { + if (downloadStarted) { + // We just started the download + buttonBrowse.IsEnabled = false; + buttonStart.IsEnabled = false; + buttonStop.IsEnabled = true; + checkBoxDownloadDiscography.IsEnabled = false; + labelProgress.Content = ""; + progressBar.Foreground = new SolidColorBrush((Color) ColorConverter.ConvertFromString("#FF01D328")); // Green + 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.Foreground = new SolidColorBrush((Color) ColorConverter.ConvertFromString("#FF01D328")); // Green + progressBar.IsIndeterminate = false; + progressBar.Value = progressBar.Minimum; + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; + TaskbarItemInfo.ProgressValue = 0; + textBoxDownloadsPath.IsReadOnly = false; + textBoxUrls.IsReadOnly = false; + } + } - Log("Starting download...", LogType.Info); + /// + /// Updates the download speed on UI. + /// + private void UpdateDownloadSpeed() { + DateTime now = DateTime.Now; - // Get user inputs - var userUrls = textBoxUrls.Text.Split(new String[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).ToList(); - userUrls = userUrls.Distinct().ToList(); + // Compute new progress values + long totalReceivedBytes = _downloadManager.DownloadingFiles.Sum(f => f.BytesReceived); - var urls = new List(); - var albums = new List(); - _downloadProgresses = new ConcurrentQueue(); + Double bytesPerSecond = + ((Double) (totalReceivedBytes - _lastTotalReceivedBytes)) / + (now - _lastDownloadSpeedUpdate).TotalSeconds; + _lastTotalReceivedBytes = totalReceivedBytes; + _lastDownloadSpeedUpdate = now; - Task.Factory.StartNew(() => { - // Get URLs of albums to download - if (App.UserSettings.DownloadArtistDiscography) { - urls = GetArtistDiscography(userUrls); - } else { - urls = userUrls; - } - urls = urls.Distinct().ToList(); - }).ContinueWith(x => { - // Get info on albums - albums = GetAlbums(urls); - }).ContinueWith(x => { - // Save files to download (we'll need the list to update the progressBar) - _filesDownload = GetFilesToDownload(albums, App.UserSettings.SaveCoverArtInTags || App.UserSettings.SaveCoverArtInFolder); - }).ContinueWith(x => { - // Set progressBar max value - long maxProgressBarValue; - if (App.UserSettings.RetrieveFilesSize) { - maxProgressBarValue = _filesDownload.Sum(f => f.Size); // Bytes to download - } else { - maxProgressBarValue = _filesDownload.Count; // Number of files to download - } - if (maxProgressBarValue > 0) { - Dispatcher.Invoke(new Action(() => { - progressBar.IsIndeterminate = false; - progressBar.Maximum = maxProgressBarValue; - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal; - })); - } - }).ContinueWith(x => { - // Start downloading albums - if (App.UserSettings.DownloadOneAlbumAtATime) { - // Download one album at a time - foreach (Album album in albums) { - DownloadAlbum(album); - } - } else { - // Parallel download - var tasks = new Task[albums.Count]; - for (int i = 0; i < albums.Count; i++) { - Album album = albums[i]; // Mandatory or else => race condition - tasks[i] = Task.Factory.StartNew(() => - DownloadAlbum(album)); - } - // Wait for all albums to be downloaded - Task.WaitAll(tasks); - } - }).ContinueWith(x => { - if (_userCancelled) { - // Display message if user cancelled - Log("Downloads cancelled by user", LogType.Info); - } - // Set controls to "ready" state - _activeDownloads = false; - UpdateControlsState(false); - if (App.UserSettings.EnableApplicationSounds) { - // Play a sound - try { - new SoundPlayer(@"C:\Windows\Media\Windows Ding.wav").Play(); - } catch { - } - } - }); + // Update download speed on UI + labelDownloadSpeed.Content = (bytesPerSecond / 1024).ToString("0.0") + " kB/s"; } - private void ButtonStop_Click(object sender, RoutedEventArgs e) { - if (MessageBox.Show(Properties.Resources.messageBoxCancelDownloads, "Bandcamp Downloader", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) != MessageBoxResult.Yes) { - return; - } - - _userCancelled = true; - Cursor = Cursors.Wait; - Log("Cancelling downloads. Please wait...", LogType.Info); - - lock (_pendingDownloads) { - if (_pendingDownloads.Count == 0) { - // Nothing to cancel - Cursor = Cursors.Arrow; - return; - } - } + private void UpdateDownloadSpeedTimer_Tick(object sender, EventArgs e) { + UpdateDownloadSpeed(); + } - buttonStop.IsEnabled = false; - progressBar.Foreground = Brushes.Red; - progressBar.IsIndeterminate = true; - TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; - TaskbarItemInfo.ProgressValue = 0; - - lock (_pendingDownloads) { - // Stop current downloads - foreach (WebClient webClient in _pendingDownloads) { - webClient.CancelAsync(); - } + /// + /// 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; } - - Cursor = Cursors.Arrow; } - private void LabelVersion_MouseDown(object sender, MouseButtonEventArgs e) { - Process.Start(Constants.ProjectWebsite); + private void UpdateProgressTimer_Tick(object sender, EventArgs e) { + UpdateProgress(); } private void WindowMain_Closing(object sender, CancelEventArgs e) { @@ -981,7 +371,5 @@ private void WindowMain_Closing(object sender, CancelEventArgs e) { } } } - - #endregion } } \ No newline at end of file diff --git a/src/BandcampDownloader/packages.config b/src/BandcampDownloader/packages.config index 4729d0a1..a636b8fb 100644 --- a/src/BandcampDownloader/packages.config +++ b/src/BandcampDownloader/packages.config @@ -6,6 +6,8 @@ + +