private ContextMenuStrip CreateItemMenu(object model, BrightIdeasSoftware.OLVColumn column) { if (model != null && model as IVideoJob != null) { IVideoJob job = model as IVideoJob; ContextMenuStrip menu = new ContextMenuStrip(); { IUserInputRequest uir = job.UserInputRequest; if (uir != null) { ToolStripMenuItem item = new ToolStripMenuItem(uir.GetQuestion()); foreach (string option in uir.GetOptions()) { item.DropDownItems.Add(option).Click += (sender, e) => { if (uir != null) { uir.SelectOption(option); } }; } menu.Items.Add(item); menu.Items.Add(new ToolStripSeparator()); } } bool queueOptionsAvailable = false; if ((job.JobStatus == VideoJobStatus.NotStarted || job.JobStatus == VideoJobStatus.Dead) && !VideoTaskGroups[job.VideoInfo.Service].IsInQueue(job)) { queueOptionsAvailable = true; ToolStripItem item = menu.Items.Add("Enqueue"); item.Click += (sender, e) => { VideoTaskGroups[job.VideoInfo.Service].Add(new WaitingVideoJob(job)); }; } if ((job.JobStatus == VideoJobStatus.NotStarted || job.JobStatus == VideoJobStatus.Dead) && VideoTaskGroups[job.VideoInfo.Service].IsInQueue(job)) { queueOptionsAvailable = true; ToolStripItem item = menu.Items.Add("Dequeue"); item.Click += (sender, e) => { VideoTaskGroups[job.VideoInfo.Service].Dequeue(job); }; } if (job.JobStatus == VideoJobStatus.NotStarted || job.JobStatus == VideoJobStatus.Dead) { queueOptionsAvailable = true; ToolStripItem item = menu.Items.Add("Download now"); item.Click += (sender, e) => { if (job.JobStatus == VideoJobStatus.NotStarted || job.JobStatus == VideoJobStatus.Dead) { VideoTaskGroups[job.VideoInfo.Service].Add(new WaitingVideoJob(job, true)); } }; } if (queueOptionsAvailable) { menu.Items.Add(new ToolStripSeparator()); } { ToolStripItem item = menu.Items.Add("Copy Video ID"); item.Click += (sender, e) => { string id = job?.VideoInfo?.VideoId; if (id != null) { Clipboard.SetText(id); } }; } { ToolStripItem item = menu.Items.Add("Copy Output Filename"); item.Click += (sender, e) => { string fn = job?.GenerateOutputFilename(); if (fn != null) { Clipboard.SetText(fn); } }; } menu.Items.Add(new ToolStripSeparator()); if (job.JobStatus == VideoJobStatus.Running) { ToolStripItem item = menu.Items.Add("Stop"); item.Click += (sender, e) => { if (job.JobStatus == VideoJobStatus.Running) { VideoTaskGroups[job.VideoInfo.Service].CancelJob(job); } }; } if (job.JobStatus == VideoJobStatus.NotStarted) { ToolStripItem item = menu.Items.Add("Kill"); item.Click += (sender, e) => { if (job.JobStatus == VideoJobStatus.NotStarted) { job.JobStatus = VideoJobStatus.Dead; job.Status = "[Manually killed] " + job.Status; } }; } if (job.JobStatus != VideoJobStatus.Running) { ToolStripItem item = menu.Items.Add("Remove"); item.Click += (sender, e) => { if (job.JobStatus != VideoJobStatus.Running) { objectListViewDownloads.RemoveObject(job); JobSet.Remove(job); } }; } return(menu.Items.Count > 0 ? menu : null); } return(null); }
public override async Task <ResultType> Run(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } JobStatus = VideoJobStatus.Running; Status = "Retrieving video info..."; var video_json = await TwitchYTDL.GetVideoJson(long.Parse(VideoInfo.VideoId)); VideoInfo = new TwitchVideoInfo(TwitchYTDL.VideoFromJson(video_json), StreamService.TwitchChatReplay); if (!AssumeFinished && VideoInfo.VideoRecordingState == RecordingState.Live) { _UserInputRequest = new UserInputRequestStreamLive(this); return(ResultType.TemporarilyUnavailable); } string tempname = Path.Combine(Util.TempFolderPath, GetTempFilenameWithoutExtension() + ".json.tmp"); string finalintmpname = Path.Combine(Util.TempFolderPath, GetTempFilenameWithoutExtension() + ".json"); string filename = Path.Combine(Util.TargetFolderPath, GetTargetFilenameWithoutExtension() + ".json"); Random rng = new Random(int.Parse(VideoInfo.VideoId)); if (!await Util.FileExists(filename)) { if (!await Util.FileExists(finalintmpname)) { Status = "Downloading chat (Initial)..."; StringBuilder concatJson = new StringBuilder(); string url = GetStartUrl(VideoInfo); int attemptsLeft = 5; TimeSpan? lastTimeSpan = new TimeSpan(0); int nextDelayMilliseconds = 0; while (true) { using (var client = new KeepAliveWebClient()) using (var cancellationCallback = cancellationToken.Register(client.CancelAsync)) { try { try { if (nextDelayMilliseconds != 0) { await Task.Delay(nextDelayMilliseconds > 0?nextDelayMilliseconds : rng.Next(90000, 270000), cancellationToken); } } catch (TaskCanceledException) { return(ResultType.Cancelled); } string commentJson = await TwitchAPI.GetLegacy(url, Util.TwitchClientId); JObject responseObject = JObject.Parse(commentJson); if (responseObject["comments"] == null) { throw new Exception("Nonsense JSON returned, no comments."); } string offset = "Unknown"; try { JToken c = ((JArray)responseObject["comments"]).Last; double val = (double)c["content_offset_seconds"]; TimeSpan ts = TimeSpan.FromSeconds(val); if (lastTimeSpan != null) { TimeSpan diff = ts - lastTimeSpan.Value; double delay = diff.TotalMilliseconds; if (delay < 0.0) { nextDelayMilliseconds = -1; } else if (delay < 90000.0) { nextDelayMilliseconds = (int)delay; } else if (delay < 270000.0) { nextDelayMilliseconds = rng.Next(90000, (int)delay); } else { nextDelayMilliseconds = -1; } } else { nextDelayMilliseconds = -1; } lastTimeSpan = ts; offset = ts.ToString(); } catch (Exception) { lastTimeSpan = null; nextDelayMilliseconds = -1; } concatJson.Append(commentJson); if (responseObject["_next"] != null) { string next = (string)responseObject["_next"]; attemptsLeft = 5; Status = "Downloading chat (Last offset: " + offset + "; next file: " + next + ")..."; url = GetNextUrl(VideoInfo, next); } else { // presumably done? break; } } catch (System.Net.WebException ex) { Console.WriteLine(ex.ToString()); --attemptsLeft; Status = "Downloading chat (Error; " + attemptsLeft + " attempts left)..."; if (attemptsLeft <= 0) { throw; } continue; } } } await StallWrite(tempname, concatJson.Length * 4, cancellationToken); // size not accurate because encoding but whatever if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } File.WriteAllText(tempname, concatJson.ToString()); File.Move(tempname, finalintmpname); } if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } Status = "Moving to final location..."; await StallWrite(filename, new FileInfo( finalintmpname ).Length, cancellationToken); if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } Util.MoveFileOverwrite(finalintmpname, filename); } Status = "Done!"; JobStatus = VideoJobStatus.Finished; return(ResultType.Success); }
public override async Task <ResultType> Run(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } JobStatus = VideoJobStatus.Running; Status = "Retrieving video info..."; (ResultType getFileUrlsResult, List <DownloadInfo> downloadInfos) = await GetFileUrlsOfVod(cancellationToken); if (getFileUrlsResult == ResultType.UserInputRequired) { Status = "Need manual fetch of file URLs."; return(ResultType.UserInputRequired); } if (getFileUrlsResult != ResultType.Success) { Status = "Failed retrieving file URLs."; return(ResultType.Failure); } string combinedTempname = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_combined.ts"); string combinedFilename = Path.Combine(GetTempFolder(), GetFinalFilenameWithoutExtension() + ".ts"); string remuxedTempname = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_combined.mp4"); string remuxedFilename = Path.Combine(GetTempFolder(), GetFinalFilenameWithoutExtension() + ".mp4"); string targetFilename = Path.Combine(GetTargetFolder(), GetFinalFilenameWithoutExtension() + ".mp4"); string baseurlfilepath = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_baseurl.txt"); string tsnamesfilepath = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_tsnames.txt"); if (!await Util.FileExists(targetFilename)) { if (!await Util.FileExists(combinedFilename)) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } Status = "Downloading files..."; string[] files; while (true) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); timer.Start(); var downloadResult = await Download(this, cancellationToken, GetTempFolderForParts(), downloadInfos); if (downloadResult.result != ResultType.Success) { return(downloadResult.result); } files = downloadResult.files; if (this.AssumeFinished || this.VideoInfo.VideoRecordingState != RecordingState.Live) { break; } else { // we're downloading a stream that is still streaming timer.Stop(); // if too little time has passed wait a bit to allow the stream to provide new data if (timer.Elapsed.TotalMinutes < 2.5) { TimeSpan ts = TimeSpan.FromMinutes(2.5) - timer.Elapsed; Status = "Waiting " + ts.TotalSeconds + " seconds for stream to update..."; _UserInputRequest = new UserInputRequestStreamLive(this); try { await Task.Delay(ts, cancellationToken); } catch (TaskCanceledException) { return(ResultType.Cancelled); } } if (File.Exists(tsnamesfilepath)) { File.Delete(tsnamesfilepath); } (getFileUrlsResult, downloadInfos) = await GetFileUrlsOfVod(cancellationToken); if (getFileUrlsResult != ResultType.Success) { Status = "Failed retrieving file URLs."; return(ResultType.Failure); } } } _UserInputRequest = null; Status = "Waiting for free disk IO slot to combine..."; try { await Util.ExpensiveDiskIOSemaphore.WaitAsync(cancellationToken); } catch (OperationCanceledException) { return(ResultType.Cancelled); } try { long expectedTargetFilesize = 0; foreach (var file in files) { expectedTargetFilesize += new FileInfo(file).Length; } Status = "Combining downloaded video parts..."; if (await Util.FileExists(combinedTempname)) { await Util.DeleteFile(combinedTempname); } await StallWrite(combinedFilename, expectedTargetFilesize, cancellationToken); if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } ResultType combineResult = await TsVideoJob.Combine(cancellationToken, combinedTempname, files); if (combineResult != ResultType.Success) { return(combineResult); } // sanity check Status = "Sanity check on combined video..."; TimeSpan actualVideoLength = (await FFMpegUtil.Probe(combinedTempname)).Duration; TimeSpan expectedVideoLength = VideoInfo.VideoLength; if (!IgnoreTimeDifferenceCombined && actualVideoLength.Subtract(expectedVideoLength).Duration() > TimeSpan.FromSeconds(5)) { // if difference is bigger than 5 seconds something is off, report Status = "Large time difference between expected (" + expectedVideoLength.ToString() + ") and combined (" + actualVideoLength.ToString() + "), stopping."; _UserInputRequest = new UserInputRequestTimeMismatchCombined(this); _IsWaitingForUserInput = true; return(ResultType.UserInputRequired); } Util.MoveFileOverwrite(combinedTempname, combinedFilename); await Util.DeleteFiles(files); System.IO.Directory.Delete(GetTempFolderForParts()); } finally { Util.ExpensiveDiskIOSemaphore.Release(); } } Status = "Waiting for free disk IO slot to remux..."; try { await Util.ExpensiveDiskIOSemaphore.WaitAsync(cancellationToken); } catch (OperationCanceledException) { return(ResultType.Cancelled); } try { Status = "Remuxing to MP4..."; if (await Util.FileExists(remuxedTempname)) { await Util.DeleteFile(remuxedTempname); } await StallWrite(remuxedFilename, new FileInfo( combinedFilename ).Length, cancellationToken); if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } await Task.Run(() => TsVideoJob.Remux(remuxedFilename, combinedFilename, remuxedTempname)); // sanity check Status = "Sanity check on remuxed video..."; TimeSpan actualVideoLength = (await FFMpegUtil.Probe(remuxedFilename)).Duration; TimeSpan expectedVideoLength = VideoInfo.VideoLength; if (!IgnoreTimeDifferenceRemuxed && actualVideoLength.Subtract(expectedVideoLength).Duration() > TimeSpan.FromSeconds(5)) { // if difference is bigger than 5 seconds something is off, report Status = "Large time difference between expected (" + expectedVideoLength.ToString() + ") and remuxed (" + actualVideoLength.ToString() + "), stopping."; _UserInputRequest = new UserInputRequestTimeMismatchRemuxed(this); _IsWaitingForUserInput = true; return(ResultType.UserInputRequired); } Util.MoveFileOverwrite(remuxedFilename, targetFilename); } finally { Util.ExpensiveDiskIOSemaphore.Release(); } } Status = "Done!"; JobStatus = VideoJobStatus.Finished; if (File.Exists(tsnamesfilepath)) { File.Delete(tsnamesfilepath); } if (File.Exists(baseurlfilepath)) { File.Delete(baseurlfilepath); } return(ResultType.Success); }