/// <summary> /// start download. /// </summary> public async void StartDownload() { if (!_cookies.Any()) { MessageBox.Show(@"未登录中国大学 MOOC.", @"提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (string.IsNullOrEmpty(_config.CourseUrl)) { MessageBox.Show(@"课程链接未输入.", @"提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var courseUrl = _config.CourseUrl; if (!_config.IsDownloadDocument && !_config.IsDownloadVideo && !_config.IsDownloadSubtitle && !_config.IsDownloadAttachment) // checked at least one of them { MessageBox.Show(@"至少勾选下载视频, 文档, 字幕, 附件其中一种类型.", @"提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (MessageBox.Show(@"开始下载?", @"提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Information) == DialogResult.Cancel) { Log("取消下载."); return; } if (!Directory.Exists(_config.CourseSavePath)) { Log($@"路径: {_config.CourseSavePath} 不存在, 准备创建."); try { Directory.CreateDirectory(_config.CourseSavePath); Log($@"路径: {_config.CourseSavePath} 创建成功."); } catch (Exception exception) { Log($@"路径: {_config.CourseSavePath} 创建失败, 原因: {exception.Message}."); return; } } SetStatus("准备下载"); Log($@"课程将会下载到文件夹: {_config.CourseSavePath}"); SetUIStatus(false); ResetCurrentBar(); ResetTotalBar(); // 1. initializes a mooc request. var mooc = new MoocRequest(_cookies, courseUrl); // 2. get term id. var termId = await mooc.GetTermIdAsync(); SetStatus("正在下载"); Log($@"提取到课程 ID 是 {termId}"); // 3. get Mooc term JavaScript code. var moocTermCode = await mooc.GetMocTermJavaScriptCodeAsync(termId); // 4. evaluate mooc term JavaScript code. moocTermCode = FixCourseBeanCode(moocTermCode); var moocTermJSON = EvaluateJavaScriptCode(moocTermCode, COURSE_BEAN_NAME) as string; // 5. deserialize moocTermJSON. var course = DeserializeObject <CourseModel>(moocTermJSON ?? string.Empty); FFmpegWorker.Instance.Start(); for (var chapterIndex = 0; chapterIndex < course.Chapters.Count && !_isCancel; chapterIndex++) { var chapter = course.Chapters[chapterIndex]; for (var lessonIndex = 0; lessonIndex < chapter.Lessons.Count && !_isCancel; lessonIndex++) { var lesson = chapter.Lessons[lessonIndex]; for (var unitIndex = 0; unitIndex < lesson.Units.Count && !_isCancel; unitIndex++) { // update total progress bar. var totalMax = course.Chapters.Count + chapter.Lessons.Count + lesson.Units.Count; var totalCurrent = (chapterIndex + 1) + (lessonIndex + 1) + (unitIndex + 1); UpdateTotalBar(CalculatePercentage(totalCurrent, totalMax)); var unit = lesson.Units[unitIndex]; // create unit save path. var chapterDir = $@"{chapterIndex + 1:00}-{FixPath(chapter.Name)}"; var lessonDir = $@"{lessonIndex + 1:00}-{FixPath(lesson.Name)}"; var unitPath = Path.Combine(_config.CourseSavePath, course.CourseName, chapterDir, lessonDir); var unitFileName = $@"{unitIndex + 1:00}-{FixPath(unit.Name)}"; if (!Directory.Exists(unitPath)) { Directory.CreateDirectory(unitPath); } var unitCode = await mooc.GetUnitJavaScriptCodeAsync( unit.Id, unit.ContentId, unit.TermId, unit.ContentType ); if (unitCode.Contains("dwr.engine._remoteHandleException")) { Console.WriteLine(@"Error: system error."); break; } unitCode = FixCourseBeanCode(unitCode); var unitJSON = EvaluateJavaScriptCode(unitCode, COURSE_BEAN_NAME) as string; var unitResult = DeserializeObject <UnitResultModel>(unitJSON ?? string.Empty); // Parse video / document / attachment link. var unitType = (UnitType)(unit.ContentType ?? 0); switch (unitType) { case UnitType.Other: // type is null. break; case UnitType.Video: // video type. { if (!_config.IsDownloadVideo) { break; } // get access token. var tokenJSON = await mooc.GetResourceTokenJsonAsync( $"{unit.Id}", $@"{unit.TermId}", $"{unit.ContentType}" ); var tokenObject = JObject.Parse(tokenJSON); var signature = tokenObject["result"]?["videoSignDto"]?["signature"]?.ToString(); var videoJSON = await mooc.GetVideoJsonAsync($@"{unit.ContentId}", signature); var video = DeserializeObject <VideoResponseModel>(videoJSON); var courseVideo = new CourseVideoInfo { SavePath = unitPath, VideoFileName = $@"{unitFileName}.mp4", MergeListFile = $@"{unitFileName}.text" }; Log($@"下载视频: {unitFileName}"); // subtitles if (_config.IsDownloadSubtitle) { foreach (var caption in video.Result.SrtCaptions) { // subtitle file. E.g: // 01-第一节 Java明天 视频.zh.srt // 01-第一节 Java明天 视频.en.srt var srtName = $@"{unitFileName}.{caption.LanguageCode}.srt"; var srtContent = await mooc.DownloadSubtitleAsync(caption.Url); File.WriteAllBytes(Path.Combine(unitPath, srtName), srtContent); } } var videoInfo = video.Result.Videos.FirstOrDefault( v => v.Quality.HasValue && (VideoQuality)v.Quality == _config.VideoQuality ); if (videoInfo != null) { var videoUrl = new Uri(videoInfo.VideoUrl); // video url. var baseUrl = $@"{videoUrl.Scheme}://{videoUrl.Host}" + string.Join("", videoUrl.Segments.Take(videoUrl.Segments.Length - 1)); Configuration.Default.BaseUri = new Uri(baseUrl, UriKind.Absolute); var m3u8List = await mooc.DownloadM3U8ListAsync(videoUrl); using var reader = new M3UFileReader(m3u8List); var m3u8Info = reader.Read(); var merger = new StringBuilder(); for (var i = 0; i < m3u8Info.MediaFiles.Count && !_isCancel; i++) { UpdateCurrentBar(CalculatePercentage(i + 1, m3u8Info.MediaFiles.Count)); var tsSavedName = $@"{unitFileName}-{i:00}.ts"; for (var j = 0; j < MAX_TIMES; j++) { var tsBytes = await mooc.DownloadM3U8TSAsync(m3u8Info.MediaFiles[i].Uri); if (tsBytes is null) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, j + 1))); } else { File.WriteAllBytes(Path.Combine(unitPath, tsSavedName), tsBytes); break; } } merger.AppendLine( $@"file '{Path.Combine(unitPath, tsSavedName)}'" ); // combine ts file path and add to list. courseVideo.TSFiles.Add(tsSavedName); } Log($@"课程 {unitFileName} 已下载完成."); File.WriteAllText( Path.Combine(unitPath, $@"{courseVideo.MergeListFile}"), merger.ToString() ); FFmpegWorker.Instance.Enqueue(courseVideo); } } break; case UnitType.Document: // document type. E.g pdf. { if (!_config.IsDownloadDocument) { break; } var documentUrl = unitResult.TextOrigUrl; var fileName = $@"{unitFileName}.pdf"; Log($@"准备下载文档: {fileName}"); for (var i = 0; i < MAX_TIMES; i++) { var document = await mooc.DownloadDocumentAsync(documentUrl); if (document is null) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i + 1))); } else { File.WriteAllBytes(Path.Combine(unitPath, fileName), document); Log($@"文档 {fileName} 已下载完成."); break; } } } break; case UnitType.Attachment: // attachment type. E.g source code. { if (!_config.IsDownloadAttachment) { break; } const string attachmentBaseUrl = "https://www.icourse163.org/course/attachment.htm"; var content = JObject.Parse(unit.JsonContent); var nosKey = content["nosKey"]?.ToString(); var fileName = content["fileName"]?.ToString(); var attachmentUrl = $@"{attachmentBaseUrl}?fileName={fileName}&nosKey={nosKey}"; Log($@"准备下载附件: {fileName}"); for (var i = 0; i < MAX_TIMES; i++) { var attachment = await mooc.DownloadAttachmentAsync(attachmentUrl); if (attachment is null) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i + 1))); } else { File.WriteAllBytes( Path.Combine(unitPath, $@"{unitFileName}-{FixPath(fileName)}"), attachment ); Log($@"附件 {fileName} 已下载完成."); break; } } } break; default: // not recognized type throw new ArgumentOutOfRangeException(); } } } } SetUIStatus(true); if (_isCancel) { Log("已取消下载."); } else { UpdateTotalBar(100); UpdateCurrentBar(100); SetStatus("下载完成"); Log("下载完成!"); } }
private async Task DownloadAttachmentAsync(UnitModel unit, MoocRequest mooc, string unitPath, string unitFileName) { if (!_config.IsDownloadAttachment) { return; } const string attachmentBaseUrl = "https://www.icourse163.org/course/attachment.htm"; if (string.IsNullOrEmpty(unit.JsonContent)) { WriteLog($"附件 {unit.Name} 下载链接为空, 跳过下载."); return; } var content = JObject.Parse(unit.JsonContent); var nosKey = content["nosKey"]?.ToString(); var fileName = content["fileName"]?.ToString(); var attachmentUrl = $@"{attachmentBaseUrl}?fileName={fileName}&nosKey={nosKey}"; var downloadAttSuccess = false; var attachSavePath = Path.Combine(unitPath, $@"{unitFileName}-{FixPath(fileName)}"); if (File.Exists(attachSavePath)) // exist attachment, skip. { WriteLog($@"附件 {fileName} 已下载, 跳过."); return; } WriteLog($@"准备下载附件: {fileName}"); for (var i = 0; i < MAX_TIMES; i++) { try { var attachment = await mooc.DownloadAttachmentAsync(attachmentUrl); if (attachment is null) { WriteLog($"下载附件 {fileName} 失败, 准备重试, 当前重试第 {i + 1} 次."); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); } else { File.WriteAllBytes(attachSavePath, attachment); downloadAttSuccess = true; WriteLog($@"附件 {fileName} 已下载完成."); break; } } catch (Exception exception) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); WriteLog($@"下载附件 {fileName} 发生错误, 原因: {exception.Message}, 准备重试, 当前重试第 {i + 1} 次."); } } if (!downloadAttSuccess) { WriteLog($"下载附件 {fileName} 失败, 已跳过."); } }