public void Retry(string id) { if (this.paused) { return; } lock (this.changeDownloadLockObject) { DownloadTask downloadTask; if (!this.downloadTasks.TryGetValue(id, out downloadTask)) { TwitchVideoDownload download = this.Downloads.Where(d => d.Video.Id == id).FirstOrDefault(); if (download != null && (download.DownloadStatus == DownloadStatus.Canceled || download.DownloadStatus == DownloadStatus.Error)) { download.ResetLog(); download.Progress = 0; download.DownloadStatus = DownloadStatus.Queued; download.Status = "Initializing"; } } } }
private void ViewVideo(string id) { try { lock (_commandLockObject) { if (!string.IsNullOrWhiteSpace(id)) { TwitchVideoDownload download = Downloads.Where(d => d.Id == id).FirstOrDefault(); if (download != null) { string folder = download.DownloadParams.Folder; if (Directory.Exists(folder)) { Process.Start(folder); } } } } } catch (Exception ex) { _dialogService.ShowAndLogException(ex); } }
private void ViewVideo(string id) { try { lock (this.commandLockObject) { if (!string.IsNullOrWhiteSpace(id)) { TwitchVideoDownload download = this.Downloads.Where(d => d.Video.Id == id).FirstOrDefault(); if (download != null) { string folder = Path.GetDirectoryName(download.DownloadParams.Filename); if (Directory.Exists(folder)) { Process.Start(folder); } } } } } catch (Exception ex) { this.guiService.ShowAndLogException(ex); } }
public void ShowLog(TwitchVideoDownload download) { LogViewVM vm = this._kernel.Get <LogViewVM>(); vm.Download = download ?? throw new ArgumentNullException(nameof(download)); this.Navigate(vm); }
private void CleanUp(string directory, TwitchVideoDownload download) { try { download.AppendLog(Environment.NewLine + "Deleting directory '" + directory + "'..."); FileSystem.DeleteDirectory(directory); download.AppendLog(" done!"); } catch { } }
public void Remove(string id) { lock (_changeDownloadLockObject) { if (!_downloadTasks.TryGetValue(id, out DownloadTask downloadTask)) { TwitchVideoDownload download = _downloads.Where(d => d.Id == id).FirstOrDefault(); if (download != null) { _downloads.Remove(download); } } } }
public void ShowLog(TwitchVideoDownload download) { if (download == null) { throw new ArgumentNullException(nameof(download)); } LogWindowVM vm = this.kernel.Get <LogWindowVM>(); vm.Download = download; LogWindow window = this.kernel.Get <LogWindow>(); window.DataContext = vm; window.ShowDialog(); }
public void Remove(string id) { lock (this.changeDownloadLockObject) { DownloadTask downloadTask; if (!this.downloadTasks.TryGetValue(id, out downloadTask)) { TwitchVideoDownload download = this.Downloads.Where(d => d.Video.Id == id).FirstOrDefault(); if (download != null) { this.Downloads.Remove(download); } } } }
private void ShowLog(string id) { try { lock (this._commandLockObject) { if (!string.IsNullOrWhiteSpace(id)) { TwitchVideoDownload download = this.Downloads.Where(d => d.Id == id).FirstOrDefault(); if (download != null) { this._navigationService.ShowLog(download); } } } } catch (Exception ex) { this._dialogService.ShowAndLogException(ex); } }
public void Retry(string id) { if (_paused) { return; } lock (_changeDownloadLockObject) { if (!_downloadTasks.TryGetValue(id, out DownloadTask downloadTask)) { TwitchVideoDownload download = _downloads.Where(d => d.Id == id).FirstOrDefault(); if (download != null && (download.DownloadState == DownloadState.Canceled || download.DownloadState == DownloadState.Error)) { download.ResetLog(); download.SetProgress(0); download.SetDownloadState(DownloadState.Queued); download.SetStatus("Initializing"); } } } }
public FailedRecord(TwitchVideoDownload download) : base(download) { Retried = false; }
private void StartQueuedDownloadIfExists() { if (_paused) { return; } if (Monitor.TryEnter(_changeDownloadLockObject)) { try { if (!_downloads.Where(d => d.DownloadState == DownloadState.Downloading).Any()) { TwitchVideoDownload download = _downloads.Where(d => d.DownloadState == DownloadState.Queued).FirstOrDefault(); if (download == null) { return; } DownloadParameters downloadParams = download.DownloadParams; CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = cancellationTokenSource.Token; string downloadId = download.Id; string vodId = downloadParams.Video.Id; string tempDir = Path.Combine(_preferencesService.CurrentPreferences.DownloadTempFolder, downloadId); string ffmpegFile = _processingService.FFMPEGExe; string concatFile = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(downloadParams.FullPath) + ".ts"); string outputFile = downloadParams.FullPath; bool disableConversion = downloadParams.DisableConversion; bool cropStart = downloadParams.CropStart; bool cropEnd = downloadParams.CropEnd; TimeSpan cropStartTime = downloadParams.CropStartTime; TimeSpan cropEndTime = downloadParams.CropEndTime; TwitchVideoQuality quality = downloadParams.SelectedQuality; Action <DownloadState> setDownloadState = download.SetDownloadState; Action <string> log = download.AppendLog; Action <string> setStatus = download.SetStatus; Action <double> setProgress = download.SetProgress; Action <bool> setIsIndeterminate = download.SetIsIndeterminate; Task downloadVideoTask = new Task(() => { setStatus("Initializing"); log("Download task has been started!"); WriteDownloadInfo(log, downloadParams, ffmpegFile, tempDir); cancellationToken.ThrowIfCancellationRequested(); log(Environment.NewLine + Environment.NewLine + "Retrieving VOD access information..."); TwitchVideoAuthInfo vodAuthInfo = _apiService.GetVodAuthInfo(vodId); log(" done!"); cancellationToken.ThrowIfCancellationRequested(); WriteVodAuthInfo(log, vodAuthInfo); cancellationToken.ThrowIfCancellationRequested(); CheckTempDirectory(log, tempDir); cancellationToken.ThrowIfCancellationRequested(); log(Environment.NewLine + Environment.NewLine + "Retrieving playlist information for all VOD qualities..."); Dictionary <TwitchVideoQuality, string> playlistInfo = _apiService.GetPlaylistInfo(vodId, vodAuthInfo); log(" done!"); cancellationToken.ThrowIfCancellationRequested(); WritePlaylistInfo(log, playlistInfo); cancellationToken.ThrowIfCancellationRequested(); TwitchPlaylist vodPlaylist = GetVodPlaylist(log, tempDir, playlistInfo, quality); cancellationToken.ThrowIfCancellationRequested(); CropInfo cropInfo = CropVodPlaylist(vodPlaylist, cropStart, cropEnd, cropStartTime, cropEndTime); cancellationToken.ThrowIfCancellationRequested(); DownloadParts(log, setStatus, setProgress, vodPlaylist, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); _processingService.ConcatParts(log, setStatus, setProgress, vodPlaylist, disableConversion ? outputFile : concatFile); if (!disableConversion) { cancellationToken.ThrowIfCancellationRequested(); _processingService.ConvertVideo(log, setStatus, setProgress, setIsIndeterminate, concatFile, outputFile, cropInfo); } }, cancellationToken); Task continueTask = downloadVideoTask.ContinueWith(task => { log(Environment.NewLine + Environment.NewLine + "Starting temporary download folder cleanup!"); CleanUp(tempDir, log); setProgress(100); setIsIndeterminate(false); bool success = false; if (task.IsFaulted) { setDownloadState(DownloadState.Error); log(Environment.NewLine + Environment.NewLine + "Download task ended with an error!"); if (task.Exception != null) { log(Environment.NewLine + Environment.NewLine + task.Exception.ToString()); } } else if (task.IsCanceled) { setDownloadState(DownloadState.Canceled); log(Environment.NewLine + Environment.NewLine + "Download task was canceled!"); } else { success = true; setDownloadState(DownloadState.Done); log(Environment.NewLine + Environment.NewLine + "Download task ended successfully!"); } if (!_downloadTasks.TryRemove(downloadId, out DownloadTask downloadTask)) { throw new ApplicationException("Could not remove download task with ID '" + downloadId + "' from download task collection!"); } if (success && _preferencesService.CurrentPreferences.DownloadRemoveCompleted) { _eventAggregator.GetEvent <RemoveDownloadEvent>().Publish(downloadId); } }); if (_downloadTasks.TryAdd(downloadId, new DownloadTask(downloadVideoTask, continueTask, cancellationTokenSource))) { downloadVideoTask.Start(); setDownloadState(DownloadState.Downloading); } } } finally { Monitor.Exit(_changeDownloadLockObject); } } }
private void StartQueuedDownloadIfExists() { if (this.paused) { return; } if (Monitor.TryEnter(this.changeDownloadLockObject)) { try { if (!this.Downloads.Where(d => d.DownloadStatus == DownloadStatus.Active).Any()) { TwitchVideoDownload download = this.Downloads.Where(d => d.DownloadStatus == DownloadStatus.Queued).FirstOrDefault(); if (download != null) { TwitchVideo video = download.Video; DownloadParameters downloadParams = download.DownloadParams; CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = cancellationTokenSource.Token; long completedChunkDownloads = 0; int maxConnectionCount = ServicePointManager.DefaultConnectionLimit; string urlId = video.IdTrimmed; string quality = downloadParams.Resolution.VideoQuality.ToTwitchQuality(); string qualityFps = downloadParams.Resolution.ResolutionFps; string outputDir = Path.Combine(this.preferencesService.CurrentPreferences.DownloadTempFolder, TEMP_PREFIX + urlId); string playlistFile = Path.Combine(outputDir, "vod.m3u8"); string ffmpegFile = Path.Combine(appDir, Environment.Is64BitOperatingSystem ? FFMPEG_EXE_X64 : FFMPEG_EXE_X86); string outputFile = downloadParams.Filename; Task downloadVideoTask = new Task(() => { download.Status = "Initializing"; download.AppendLog("Download task has been started!"); download.AppendLog(Environment.NewLine + Environment.NewLine + "VOD ID: " + urlId); download.AppendLog(Environment.NewLine + "Selected Quality: " + qualityFps); download.AppendLog(Environment.NewLine + "Download Url: " + video.Url.ToString()); download.AppendLog(Environment.NewLine + "Output File: " + outputFile); download.AppendLog(Environment.NewLine + "FFMPEG Path: " + ffmpegFile); download.AppendLog(Environment.NewLine + "Temporary Download Folder: " + outputDir); if (!Directory.Exists(outputDir)) { download.AppendLog(Environment.NewLine + Environment.NewLine + "Creating directory '" + outputDir + "'..."); FileSystem.CreateDirectory(outputDir); download.AppendLog(" done!"); } if (Directory.EnumerateFileSystemEntries(outputDir).Any()) { throw new ApplicationException("Temporary download directory '" + outputDir + "' is not empty!"); } using (WebClient webClient = new WebClient()) { download.AppendLog(Environment.NewLine + Environment.NewLine + "Retrieving access token and signature..."); string accessTokenStr = webClient.DownloadString(string.Format(accessTokenUrl, urlId)); download.AppendLog(" done!"); dynamic accessTokenJson = JsonConvert.DeserializeObject(accessTokenStr); string token = Uri.EscapeDataString(accessTokenJson.token.ToString()); string sig = accessTokenJson.sig.ToString(); download.AppendLog(Environment.NewLine + "Token: " + token); download.AppendLog(Environment.NewLine + "Signature: " + sig); cancellationToken.ThrowIfCancellationRequested(); download.AppendLog(Environment.NewLine + Environment.NewLine + "Retrieving m3u8 playlist urls for all VOD qualities..."); string allPlaylistsStr = webClient.DownloadString(string.Format(allPlaylistsUrl, urlId, sig, token)); download.AppendLog(" done!"); List <string> allPlaylistsList = allPlaylistsStr.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Where(s => !s.StartsWith("#")).ToList(); allPlaylistsList.ForEach(url => { download.AppendLog(Environment.NewLine + url); }); string playlistUrl = allPlaylistsList.Where(s => s.ToLowerInvariant().Contains(quality)).First(); download.AppendLog(Environment.NewLine + Environment.NewLine + "Playlist url for selected quality " + qualityFps + " is " + playlistUrl); cancellationToken.ThrowIfCancellationRequested(); download.AppendLog(Environment.NewLine + Environment.NewLine + "Retrieving list of video chunks..."); string playlistStr = webClient.DownloadString(playlistUrl); download.AppendLog(" done!"); List <string> playlistLines = playlistStr.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); List <string> webChunkList = playlistLines.Where(s => !s.StartsWith("#")).ToList(); string webChunkUrlPrefix = playlistUrl.Substring(0, playlistUrl.LastIndexOf("/") + 1); long webChunkCount = webChunkList.Count; download.AppendLog(Environment.NewLine + "Number of video chunks to download: " + webChunkCount); download.AppendLog(Environment.NewLine + "Maximum connection count: " + maxConnectionCount); List <string> webChunkFilenames = new List <string>(); List <WebChunk> downloadQueue = new List <WebChunk>(); int counter = 0; download.AppendLog(Environment.NewLine + Environment.NewLine + "Initializing video chunk download queue..."); foreach (string webChunk in webChunkList) { string filename = Path.Combine(outputDir, counter.ToString("D8") + ".ts"); webChunkFilenames.Add(filename); downloadQueue.Add(new WebChunk(filename, webChunkUrlPrefix + webChunk)); counter++; cancellationToken.ThrowIfCancellationRequested(); } download.AppendLog(" done!"); download.AppendLog(Environment.NewLine + "Starting parallel video chunk download..."); download.Status = "Downloading"; download.AppendLog(Environment.NewLine + Environment.NewLine + "Parallel video chunk download is running..."); object percentageLock = new object(); Parallel.ForEach(downloadQueue, new ParallelOptions() { MaxDegreeOfParallelism = maxConnectionCount - 1 }, (webChunk, loopState) => { using (WebClient downloadClient = new WebClient()) { byte[] bytes = downloadClient.DownloadData(webChunk.Url); Interlocked.Increment(ref completedChunkDownloads); FileSystem.DeleteFile(webChunk.Filename); File.WriteAllBytes(webChunk.Filename, bytes); long completed = Interlocked.Read(ref completedChunkDownloads); lock (percentageLock) { download.Progress = (int)(completedChunkDownloads * 100 / webChunkCount); } } if (cancellationToken.IsCancellationRequested) { loopState.Stop(); } }); download.Progress = 100; cancellationToken.ThrowIfCancellationRequested(); download.AppendLog(" done!"); download.AppendLog(Environment.NewLine + Environment.NewLine + "Download of all video chunks complete!"); download.AppendLog(Environment.NewLine + Environment.NewLine + "Creating local m3u8 playlist for FFMPEG"); int chunkIndex = 0; for (int i = 0; i < playlistLines.Count; i++) { if (!playlistLines[i].StartsWith("#")) { playlistLines[i] = webChunkFilenames[chunkIndex]; chunkIndex++; } } string newPlaylistStr = string.Join("\n", playlistLines); FileSystem.DeleteFile(playlistFile); download.AppendLog(Environment.NewLine + "Writing playlist to '" + playlistFile + "'"); File.WriteAllText(playlistFile, newPlaylistStr); download.Status = "Encoding"; download.Progress = 0; download.AppendLog(Environment.NewLine + Environment.NewLine + "Executing '" + ffmpegFile + "' on local playlist..."); cancellationToken.ThrowIfCancellationRequested(); ProcessStartInfo psi = new ProcessStartInfo(ffmpegFile); psi.Arguments = "-y -i \"" + playlistFile + "\" -c:v copy -c:a copy -bsf:a aac_adtstoasc \"" + outputFile + "\""; psi.RedirectStandardError = true; psi.RedirectStandardOutput = true; psi.StandardErrorEncoding = Encoding.UTF8; psi.StandardOutputEncoding = Encoding.UTF8; psi.UseShellExecute = false; psi.CreateNoWindow = true; download.AppendLog(Environment.NewLine + "Command line arguments: " + psi.Arguments + Environment.NewLine); using (Process p = new Process()) { TimeSpan duration = new TimeSpan(); DataReceivedEventHandler outputDataReceived = new DataReceivedEventHandler((s, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) { string dataTrimmed = e.Data.Trim(); if (dataTrimmed.StartsWith("Duration")) { string durationStr = dataTrimmed.Substring(dataTrimmed.IndexOf(":") + 1).Trim(); durationStr = durationStr.Substring(0, durationStr.IndexOf(",")).Trim(); duration = TimeSpan.Parse(durationStr); } if (dataTrimmed.StartsWith("frame")) { string timeStr = dataTrimmed.Substring(dataTrimmed.IndexOf("time") + 4).Trim(); timeStr = timeStr.Substring(timeStr.IndexOf("=") + 1).Trim(); timeStr = timeStr.Substring(0, timeStr.IndexOf(" ")).Trim(); TimeSpan current = TimeSpan.Parse(timeStr); lock (percentageLock) { download.Progress = (int)(current.TotalMilliseconds * 100 / duration.TotalMilliseconds); } } download.AppendLog(Environment.NewLine + e.Data); } }); p.OutputDataReceived += outputDataReceived; p.ErrorDataReceived += outputDataReceived; p.StartInfo = psi; p.Start(); p.BeginErrorReadLine(); p.BeginOutputReadLine(); p.WaitForExit(); } download.AppendLog(Environment.NewLine + Environment.NewLine + "Encoding complete!"); } }, cancellationTokenSource.Token); Task continueTask = downloadVideoTask.ContinueWith(task => { download.AppendLog(Environment.NewLine + Environment.NewLine + "Starting temporary download folder cleanup!"); this.CleanUp(outputDir, download); download.Progress = 100; if (task.IsFaulted) { download.DownloadStatus = DownloadStatus.Error; download.AppendLog(Environment.NewLine + Environment.NewLine + "Download task ended with an error!"); if (task.Exception != null) { download.AppendLog(Environment.NewLine + Environment.NewLine + task.Exception.ToString()); } } else if (task.IsCanceled) { download.DownloadStatus = DownloadStatus.Canceled; download.AppendLog(Environment.NewLine + Environment.NewLine + "Download task was canceled!"); } else { download.DownloadStatus = DownloadStatus.Finished; download.AppendLog(Environment.NewLine + Environment.NewLine + "Download task ended successfully!"); } DownloadTask downloadTask; if (!this.downloadTasks.TryRemove(video.Id, out downloadTask)) { throw new ApplicationException("Could not remove download task with ID '" + video.Id + "' from download task collection!"); } }); if (this.downloadTasks.TryAdd(video.Id, new DownloadTask(downloadVideoTask, continueTask, cancellationTokenSource))) { downloadVideoTask.Start(); download.DownloadStatus = DownloadStatus.Active; } } } } finally { Monitor.Exit(this.changeDownloadLockObject); } } }
private void StartQueuedDownloadIfExists() { if (this.paused) { return; } if (Monitor.TryEnter(this.changeDownloadLockObject)) { try { if (!this.downloads.Where(d => d.DownloadStatus == DownloadStatus.Active).Any()) { TwitchVideoDownload download = this.downloads.Where(d => d.DownloadStatus == DownloadStatus.Queued).FirstOrDefault(); if (download == null) { return; } DownloadParameters downloadParams = download.DownloadParams; CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = cancellationTokenSource.Token; string downloadId = download.Id; string urlIdTrimmed = downloadParams.Video.IdTrimmed; string tempDir = Path.Combine(downloadParams.Folder, TEMP_PREFIX + downloadId); string playlistFile = Path.Combine(tempDir, PLAYLIST_NAME); string ffmpegFile = Path.Combine(appDir, Environment.Is64BitOperatingSystem ? FFMPEG_EXE_X64 : FFMPEG_EXE_X86); string outputFile = downloadParams.FullPath; bool cropStart = downloadParams.CropStart; bool cropEnd = downloadParams.CropEnd; TimeSpan cropStartTime = downloadParams.CropStartTime; TimeSpan cropEndTime = downloadParams.CropEndTime; TwitchVideoResolution resolution = downloadParams.Resolution; Action <DownloadStatus> setDownloadStatus = download.SetDownloadStatus; Action <string> log = download.AppendLog; Action <string> setStatus = download.SetStatus; Action <int> setProgress = download.SetProgress; Action <bool> setIsEncoding = download.SetIsEncoding; Task downloadVideoTask = new Task(() => { setStatus("Initializing"); log("Download task has been started!"); this.WriteDownloadInfo(log, downloadParams, ffmpegFile, tempDir); this.CheckTempDirectory(log, tempDir); using (WebClient webClient = new WebClient()) { AuthInfo authInfo = this.RetrieveAuthInfo(log, webClient, urlIdTrimmed); cancellationToken.ThrowIfCancellationRequested(); string playlistUrl = this.RetrievePlaylistUrlForQuality(log, webClient, resolution, urlIdTrimmed, authInfo); cancellationToken.ThrowIfCancellationRequested(); WebChunkList webChunkList = this.RetrieveWebChunkList(log, webClient, tempDir, playlistUrl); cancellationToken.ThrowIfCancellationRequested(); CropInfo cropInfo = this.CropWebChunkList(webChunkList, cropStart, cropEnd, cropStartTime, cropEndTime); cancellationToken.ThrowIfCancellationRequested(); this.DownloadChunks(log, setStatus, setProgress, webChunkList, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); this.WriteNewPlaylist(log, webChunkList, playlistFile); cancellationToken.ThrowIfCancellationRequested(); this.EncodeVideo(log, setStatus, setProgress, setIsEncoding, ffmpegFile, playlistFile, outputFile, cropInfo); } }, cancellationToken); Task continueTask = downloadVideoTask.ContinueWith(task => { log(Environment.NewLine + Environment.NewLine + "Starting temporary download folder cleanup!"); this.CleanUp(tempDir, log); setProgress(100); setIsEncoding(false); bool success = false; if (task.IsFaulted) { setDownloadStatus(DownloadStatus.Error); log(Environment.NewLine + Environment.NewLine + "Download task ended with an error!"); if (task.Exception != null) { log(Environment.NewLine + Environment.NewLine + task.Exception.ToString()); } } else if (task.IsCanceled) { setDownloadStatus(DownloadStatus.Canceled); log(Environment.NewLine + Environment.NewLine + "Download task was canceled!"); } else { success = true; setDownloadStatus(DownloadStatus.Finished); log(Environment.NewLine + Environment.NewLine + "Download task ended successfully!"); } DownloadTask downloadTask; if (!this.downloadTasks.TryRemove(downloadId, out downloadTask)) { throw new ApplicationException("Could not remove download task with ID '" + downloadId + "' from download task collection!"); } if (success && this.preferencesService.CurrentPreferences.DownloadRemoveCompleted) { this.eventAggregator.GetEvent <DownloadCompletedEvent>().Publish(downloadId); } }); if (this.downloadTasks.TryAdd(downloadId, new DownloadTask(downloadVideoTask, continueTask, cancellationTokenSource))) { downloadVideoTask.Start(); setDownloadStatus(DownloadStatus.Active); } } } finally { Monitor.Exit(this.changeDownloadLockObject); } } }
public DownloadRecord(TwitchVideoDownload download) { this.DownloadString = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(download))); Download = download; }