/// <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 DownloadCourseListAsync(CourseModel course, MoocRequest mooc) { var total = course?.Chapters?.Sum(c => c.Lessons?.Sum(l => l?.Units?.Count ?? 0)) ?? 0; if (total == 0) { return; } Log.Information($@"当前一共有 {total} 个课程单元."); var current = 0; int chapterIndex; for (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. UpdateTotalBar(CalculatePercentage(++current, total)); var unit = lesson?.Units?[unitIndex]; if (unit is null) { break; } // 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.Information($@"下载视频: {unitFileName}.mp4"); WriteLog($@"下载视频: {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"; Log.Information($@"下载字幕: {srtName}"); try { var srtContent = await mooc.DownloadSubtitleAsync(caption.Url); if (srtContent is null) { WriteLog($"字幕 {srtName} 下载失败."); break; } File.WriteAllBytes(Path.Combine(unitPath, srtName), srtContent); } catch (Exception exception) { WriteLog($"字幕 {srtName} 下载失败, 原因: {exception.Message}"); break; } } } var quality = video.Result.Videos.Select(v => v.Quality ?? 1).ToList().Max(); VideoModel videoInfo; if (quality < (int)_config.VideoQuality) { videoInfo = video.Result.Videos.FirstOrDefault( v => v.Quality.HasValue && v.Quality == quality ); } else { videoInfo = video.Result.Videos.FirstOrDefault( v => v.Quality.HasValue && (VideoQuality)v.Quality == _config.VideoQuality ); } var videoFormat = videoInfo.Format.ToLower(); switch (videoFormat) { case "hls": // m3u8 format. var ts2Mp4File = Path.Combine(courseVideo.SavePath, courseVideo.VideoFileName); if (File.Exists(ts2Mp4File)) { Log.Warning($@"课程 {courseVideo.VideoFileName} 已存在, 跳过下载."); WriteLog($@"课程 {courseVideo.VideoFileName} 已存在, 跳过下载."); break; } 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); string m3u8List; try { m3u8List = await mooc.DownloadM3U8ListAsync(videoUrl); if (string.IsNullOrEmpty(m3u8List)) { WriteLog($"下载课程 {unitFileName} 的视频列表失败."); break; } } catch (Exception exception) { WriteLog($"下载课程 {unitFileName} 的视频列表发生错误, 原因: {exception.Message}"); break; } try { 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"; var downloadVideoSuccess = false; for (var j = 0; j < MAX_TIMES; j++) { try { var tsBytes = await mooc.DownloadM3U8TSAsync(m3u8Info.MediaFiles[i].Uri); if (tsBytes is null) { WriteLog( $"下载视频片段 {tsSavedName} 失败, 准备重试, 当前重试第 {j + 1} 次." ); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, j))); } else { File.WriteAllBytes( Path.Combine(unitPath, tsSavedName), tsBytes); merger.AppendLine( $@"file '{Path.Combine(unitPath, tsSavedName)}'" ); // combine ts file path and add to list. courseVideo.TSFiles.Add(tsSavedName); downloadVideoSuccess = true; break; } } catch (Exception exception) { WriteLog( $"下载视频片段 {tsSavedName} 发生异常, 原因: {exception.Message}, 准备重试, 当前重试第 {j + 1} 次." ); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, j))); } } if (!downloadVideoSuccess) { WriteLog($"下载视频片段 {tsSavedName} 失败, 已跳过."); } } WriteLog($@"课程 {unitFileName} 已下载完成."); File.WriteAllText( Path.Combine(unitPath, $@"{courseVideo.MergeListFile}"), merger.ToString() ); FFmpegWorker.Instance.Enqueue(courseVideo); } catch (Exception exception) { WriteLog($"下载课程 {unitFileName} 的视频发生错误, 原因: {exception.Message}"); } break; case "mp4": var mp4File = Path.Combine(unitPath, $"{unitFileName}.mp4"); if (File.Exists(mp4File)) // exist mp4, skip. { Log.Warning($@"课程: {$"{unitFileName}.mp4"} 已存在, 跳过下载."); WriteLog($@"课程: {$"{unitFileName}.mp4"} 已存在, 跳过下载."); } var mp4Url = videoInfo.VideoUrl; if (string.IsNullOrEmpty(mp4Url)) { switch (_config.VideoQuality) { case VideoQuality.SD: mp4Url = unitResult.VideoVo.Mp4SdUrl; break; case VideoQuality.HD: mp4Url = unitResult.VideoVo.Mp4HdUrl; break; case VideoQuality.UHD: mp4Url = unitResult.VideoVo.Mp4ShdUrl; break; } } Log.Information($@"{unitFileName} 的下载链接是: {mp4Url}"); for (var i = 0; i < MAX_TIMES; i++) { try { var videoBytes = await mooc.DownloadVideoAsync(mp4Url); if (videoBytes is null) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); WriteLog($"下载课程视频 {unitFileName} 失败, 准备重试, 当前重试第 {i + 1} 次."); } else { File.WriteAllBytes(mp4File, videoBytes); WriteLog($@"课程 {unitFileName} 已下载完成."); break; } } catch (Exception exception) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); WriteLog($"下载课程 {unitFileName} 的视频发生错误, 原因: {exception.Message}"); } } break; case "flv": var flvFile = Path.Combine(unitPath, $"{unitFileName}.flv"); if (File.Exists(flvFile)) // exist mp4, skip. { Log.Warning($@"课程: {$"{unitFileName}.flv"} 已存在, 跳过下载."); WriteLog($@"课程: {$"{unitFileName}.flv"} 已存在, 跳过下载."); } var flvUrl = string.Empty; switch (_config.VideoQuality) { case VideoQuality.SD: flvUrl = unitResult.VideoVo.FlvSdUrl; break; case VideoQuality.HD: flvUrl = unitResult.VideoVo.FlvHdUrl; break; case VideoQuality.UHD: flvUrl = unitResult.VideoVo.FlvShdUrl; break; } if (string.IsNullOrEmpty(flvUrl)) { flvUrl = videoInfo.VideoUrl; } Log.Information($@"{unitFileName} 的下载链接是: {flvUrl}"); for (var i = 0; i < MAX_TIMES; i++) { try { var videoBytes = await mooc.DownloadVideoAsync(flvUrl); if (videoBytes is null) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); WriteLog($"下载课程视频 {unitFileName} 失败, 准备重试, 当前重试第 {i + 1} 次."); } else { File.WriteAllBytes(flvFile, videoBytes); WriteLog($@"课程 {unitFileName} 已下载完成."); break; } } catch (Exception exception) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); WriteLog($"下载课程 {unitFileName} 的视频发生错误, 原因: {exception.Message}"); } } break; } } break; case UnitType.Document: // document type. E.g pdf. await DownloadDocumentAsync(unitResult, unitFileName, mooc, unitPath); break; case UnitType.Attachment: // attachment type. E.g source code. await DownloadAttachmentAsync(unit, mooc, unitPath, unitFileName); break; default: // not recognized type WriteLog($"当前课程单元: {unitFileName} 类型不支持下载, 已忽略."); break; } } } } SetUIStatus(true); if (_isCancel) { WriteLog("已取消下载."); ResetCurrentBar(); ResetTotalBar(); } else { UpdateTotalBar(100); UpdateCurrentBar(100); SetStatus("下载完成"); WriteLog($"课程 {course.CourseName} 已下载完成!"); MessageBox.Show( $@"课程 {course.CourseName} 已下载完成!", @"提示", MessageBoxButtons.OK, MessageBoxIcon.Information ); } }