diff --git a/app/app.js b/app/app.js
index f98ae1aa..5173fa75 100644
--- a/app/app.js
+++ b/app/app.js
@@ -14,14 +14,16 @@ const https = require("https");
const cookie = require("cookie");
const { Settings, ui, utils } = require('./helpers')
+const { default: UdemyService } = require('./core/services');
-const pageSize = 25;
-const msgDRMProtected = translate("Contains DRM protection and cannot be downloaded");
-const ajaxTimeout = 40000; // 40 segundos
+const PAGE_SIZE = 25;
+const MSG_DRM_PROTECTED = translate("Contains DRM protection and cannot be downloaded");
+const HTTP_TIMEOUT = 40000; // 40 segundos
-let loggers = [];
+const loggers = [];
let headersAuth;
let repoAccount = "heliomarpm";
+let udemyService;
ipcRenderer.on("saveDownloads", () => saveDownloads(true));
@@ -118,7 +120,7 @@ $(".ui.dashboard .content").on("click", ".download-success, .course-encrypted",
$(".ui.dashboard .content").on("click", ".download.button, .download-error", function (e) {
e.stopImmediatePropagation();
- initializeDownload($(this).parents(".course"));
+ setupDownload($(this).parents(".course"));
});
$(".ui.dashboard .content").on("click", "#clear_logger", clearLogArea);
@@ -275,70 +277,46 @@ async function checkUpdate(account, silent = false) {
if (!silent) {
showAlert(translate("Failed to check for updates"), translate("Check for updates"));
}
- appendLog("Failed to check for updates", error.message, true);
+ appendLog("Failed to check for updates", error);
} finally {
ui.busyCheckUpdate(false);
}
}
-function checkLogin() {
+async function checkLogin() {
if (Settings.accessToken) {
- ui.busyLogin(true);
-
+ // TODO: remover isso
headersAuth = { Authorization: `Bearer ${Settings.accessToken}` };
- let url = "https://www.udemy.com/api-2.0/contexts/me/?header=True";
-
- axios({
- timeout: ajaxTimeout,
- method: "GET",
- url,
- headers: headersAuth,
- }).then((response) => {
- ui.busyLogin(false);
- ui.busyLoadCourses(true);
- ui.showDashboard();
- const resp = response.data;
+ try {
+ ui.busyLogin(true);
- const subscriber = utils.toBoolean(resp.header.user.enableLabsInPersonalPlan);
- Settings.subscriber = subscriber;
+ udemyService = new UdemyService(Settings.subDomain, HTTP_TIMEOUT);
+ const userContext = await udemyService.fetchProfile(Settings.accessToken, 30000);
- url = !subscriber
- ? `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscribed-courses?page_size=${pageSize}&page_size=30&ordering=-last_accessed`
- : `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscription-course-enrollments?page_size=${pageSize}&page_size=30&ordering=-last_accessed`;
+ if (!userContext.header.isLoggedIn) {
+ ui.resetToLogin();
+ return;
+ }
+ ui.busyLogin(false);
+ ui.showDashboard();
- axios({
- timeout: ajaxTimeout,
- method: "GET",
- url,
- headers: headersAuth,
- }).then((response) => {
- renderCourses(response.data);
- ui.busyLoadCourses(false);
+ Settings.subscriber = utils.toBoolean(userContext.header.user.enableLabsInPersonalPlan);
+ fetchCourses(Settings.subscriber);
- if (Settings.downloadedCourses) {
- renderDownloads();
- }
+ if (Settings.download.checkNewVersion) {
+ checkUpdate("heliomarpm", true);
+ }
- if (Settings.download.checkNewVersion) {
- checkUpdate(repoAccount, true);
- }
- }).catch((error) => {
- ui.busyOff();
- console.error("beforeLogin_Error:", error);
- showAlert(error.message, "Profile Error");
- });
- }).catch((error) => {
+ } catch (error) {
+ console.error("Failed to fetch user profile", error);
if (!process.env.DEBUG_MODE) Settings.accessToken = null;
- ui.busyOff();
- console.error("checkLogin_Error:", error);
- showAlert(error.message, "Login Error");
-
ui.resetToLogin();
- }).finally(() => {
+ showAlert(error.message, error.name || "Error");
+ } finally {
console.log("access-token", Settings.accessToken);
- });
+ }
}
}
@@ -415,7 +393,7 @@ function loginWithAccessToken() {
});
}
-function htmlCourseCard(course, downloadSection = false) {
+function createCourseElement(course, downloadSection = false) {
if (!course.completed) {
course.completed = false;
}
@@ -456,7 +434,7 @@ function htmlCourseCard(course, downloadSection = false) {
${downloadSection ? tagDismiss : ""}
-
@@ -483,7 +461,7 @@ function htmlCourseCard(course, downloadSection = false) {
${translate("Click to dismiss")}
@@ -535,302 +513,260 @@ function htmlCourseCard(course, downloadSection = false) {
return $course;
}
-function initializeDownload($course, subtitle) {
- ui.prepareDownload($course);
+function resetCourse($course, $elMessage, autoRetry, courseData, subtitle) {
+ if ($elMessage.hasClass("download-success")) {
+ $course.attr("course-completed", true);
+ } else {
+ $course.attr("course-completed", "");
- const courseId = $course.attr("course-id");
+ if ($elMessage.hasClass("download-error")) {
+ if (autoRetry && courseData.errorCount++ < 5) {
+ debugger;
+ $course.length = 1;
+ startDownload($course, courseData, subtitle);
+ return;
+ }
+ }
+ }
- const skipSubtitles = Boolean(Settings.download.skipSubtitles);
- const defaultSubtitle = skipSubtitles ? null : subtitle ?? Settings.download.defaultSubtitle;
- const downloadType = Number(Settings.download.type);
+ $course.find(".download-quality").hide();
+ $course.find(".download-speed").hide().find(".value").html(0);
+ $course.find(".download-status").hide().html(ui.actionCardTemplate);
+ $course.css("padding", "14px 0px");
+ $elMessage.css("display", "flex");
+}
- const url = `https://${Settings.subDomain}.udemy.com/api-2.0/courses/${courseId}/cached-subscriber-curriculum-items?page_size=10000`;
+function renderCourses(response, isResearch = false) {
+ console.log("renderCourse", { response, isResearch });
- console.clear();
- console.log("downloadButtonClick", url);
+ const $coursesSection = $(".ui.dashboard .ui.courses.section");
+ const $coursesItems = $coursesSection.find(".ui.courses.items").empty();
- ui.busyPrepareDownload(true);
- axios({
- timeout: ajaxTimeout,
- type: "GET",
- url,
- headers: headersAuth,
- }).then((response) => {
- const resp = response.data;
+ $coursesSection.find(".disposable").remove();
- ui.enableDownloadButton($course, false);
- ui.showProgress($course, true);
+ if (response.results.length) {
+ // response.results.forEach(course => {
+ // $coursesItems.append(htmlCourseCard(course));
+ // });
+ const courseElements = response.results.map(course => createCourseElement(course));
+ $coursesItems.append(courseElements);
- const courseData = [];
- courseData["id"] = courseId;
- courseData["chapters"] = [];
- courseData["name"] = $course.find(".coursename").text();
- courseData["totalLectures"] = 0;
- courseData["encryptedVideos"] = 0;
- courseData["errorCount"] = 0;
-
- let chapterIndex = -1;
- let lectureIndex = -1;
- let remaining = resp.count;
- let availableSubs = [];
-
- if (resp.results[0]._class == "lecture") {
- chapterIndex++;
- lectureIndex = 0;
- courseData["chapters"][chapterIndex] = [];
- courseData["chapters"][chapterIndex]["name"] = "Chapter 1";
- courseData["chapters"][chapterIndex]["lectures"] = [];
- remaining--;
+ if (response.next) {
+ // added loadMore Button
+ $coursesSection.append(
+ `
`
+ );
}
- $.each(resp.results, function (i, v) {
- if (v._class.toLowerCase() == "chapter") {
- chapterIndex++;
- lectureIndex = 0;
- courseData["chapters"][chapterIndex] = [];
- courseData["chapters"][chapterIndex]["name"] = v.title;
- courseData["chapters"][chapterIndex]["lectures"] = [];
- remaining--;
- } else if (
- v._class.toLowerCase() == "lecture" &&
- (v.asset.asset_type.toLowerCase() == "video" ||
- v.asset.asset_type.toLowerCase() == "article" ||
- v.asset.asset_type.toLowerCase() == "file" ||
- v.asset.asset_type.toLowerCase() == "e-book")
- ) {
- if (v.asset.asset_type.toLowerCase() != "video" && downloadType == Settings.DownloadType.OnlyLectures) {
- remaining--;
- if (!remaining) {
- if (Object.keys(availableSubs).length) {
- askForSubtitle(availableSubs, initDownload, $course, courseData, defaultSubtitle);
- } else {
- initDownload($course, courseData);
- }
- }
- return;
- }
+ } else {
+ let msg = "";
+ if (!isResearch) {
+ msg = getMsgChangeSearchMode();
+ appendLog(translate("No Courses Found"), msg);
+ }
- function getLecture(lectureName, chapterIndex, lectureIndex) {
- const url = `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscribed-courses/${courseId}/lectures/${v.id}?fields[lecture]=asset,supplementary_assets&fields[asset]=stream_urls,download_urls,captions,title,filename,data,body,media_sources,media_license_token`;
- // console.log("getLecture", url);
-
- axios({
- timeout: ajaxTimeout,
- type: "GET",
- url,
- headers: headersAuth,
- }).then((response) => {
- const resp = response.data;
-
- var src = "";
- var videoQuality = "";
- var type = "";
-
- if (v.asset.asset_type.toLowerCase() == "article") {
- if (resp.asset.data) {
- src = resp.asset.data.body;
- } else {
- src = resp.asset.body;
- }
- videoQuality = v.asset.asset_type;
- type = "Article";
- } else if (v.asset.asset_type.toLowerCase() == "file" || v.asset.asset_type.toLowerCase() == "e-book") {
- src = resp.asset.download_urls[v.asset.asset_type][0].file;
- videoQuality = v.asset.asset_type;
- type = "File";
- } else {
- const qualities = [];
- const qualitySrcMap = {};
-
- const medias = resp.asset.stream_urls?.Video ?? resp.asset.media_sources;
- medias.forEach(function (val) {
- if (val.type != "application/dash+xml") {
- if (val.label.toLowerCase() != "auto") {
- qualities.push(val.label);
- }
- qualitySrcMap[val.label] = val.file ?? val.src;
- }
- });
-
- const lowest = Math.min(...qualities);
- const highest = Math.max(...qualities);
-
- videoQuality = (qualities.length == 0 ? "Auto" : Settings.download.videoQuality).toString();
- type = "Video";
- src = medias[0].src ?? medias[0].file;
-
- switch (videoQuality.toLowerCase()) {
- case "auto":
- videoQuality = medias[0].label;
- break;
- case "lowest":
- src = qualitySrcMap[lowest];
- videoQuality = lowest;
- break;
- case "highest":
- // has stream use it otherwise user highest quality
- if (qualitySrcMap["Auto"]) {
- src = qualitySrcMap["Auto"];
- } else {
- src = qualitySrcMap[highest];
- videoQuality = highest;
- }
- break;
- default:
- videoQuality = videoQuality.slice(0, -1);
- if (qualitySrcMap[videoQuality]) {
- src = qualitySrcMap[videoQuality];
- } else {
- videoQuality = medias[0].label;
- }
- }
- }
+ $coursesItems.append(
+ `
+ ${translate("No Courses Found")}
+ ${translate("Remember, you will only be able to see the courses you are enrolled in")}
+ ${msg}
+
`
+ );
+ }
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex] = {
- src: src,
- name: lectureName,
- quality: videoQuality,
- type: type,
- };
+}
- if (!skipSubtitles && resp.asset.captions.length) {
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex].caption = [];
+async function renderDownloads() {
+ console.log("renderDownloads");
- resp.asset.captions.forEach(function (caption) {
- caption.video_label in availableSubs
- ? (availableSubs[caption.video_label] = availableSubs[caption.video_label] + 1)
- : (availableSubs[caption.video_label] = 1);
+ const $downloadsSection = $(".ui.downloads.section .ui.courses.items");
+ if ($downloadsSection.find(".ui.course.item").length) {
+ return;
+ }
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex].caption[caption.video_label] =
- caption.url;
- });
- }
+ const downloadedCourses = Settings.downloadedCourses || [];
+ if (!downloadedCourses.length) {
+ // if ($downloadsSection.find(".ui.yellow.message").length) {
+ // return;
+ // }
+ // $downloadsSection.append(
+ // `
+ // ${translate("There are no Downloads to display")}
+ //
`
+ // );
+ }
+ else {
+ ui.busyLoadDownloads(true);
+ // await utils.sleep(10);
+ // // downloadedCourses.forEach(course => {
+ // downloadedCourses.map(course => {
+ // const $courseItem = htmlCourseCard(course, true);
+ // $downloadsSection.append($courseItem);
- if (resp.supplementary_assets.length && downloadType != Settings.DownloadType.OnlyLectures) {
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex]["supplementary_assets"] = [];
- let supplementary_assets_remaining = resp.supplementary_assets.length;
+ // if (!course.completed && Settings.download.autoStartDownload) {
+ // initializeDownload($courseItem, course.selectedSubtitle);
+ // // $courseItem.find(".action.buttons").find(".pause.button").removeClass("disabled");
+ // }
+ // });
+ // ui.busyLoadDownloads(false);
- $.each(resp.supplementary_assets, function (a, b) {
- const url = `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscribed-courses/${courseId}/lectures/${v.id}/supplementary-assets/${b.id}?fields[asset]=download_urls,external_url,asset_type`;
- // console.log("getLecture&Attachments", url);
+ function addCourseToDOM(course) {
+ return new Promise((resolve, _reject) => {
+ const $courseItem = createCourseElement(course, true);
+ $downloadsSection.append($courseItem);
- axios({
- timeout: ajaxTimeout,
- type: "GET",
- url,
- headers: headersAuth,
- }).then((response) => {
- const resp = response.data;
-
- console.log("carregando anexos");
- if (resp.download_urls) {
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex][
- "supplementary_assets"
- ].push({
- src: resp.download_urls[resp.asset_type][0].file,
- name: b.title,
- quality: "Attachment",
- type: "File",
- });
- } else {
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex][
- "supplementary_assets"
- ].push({
- src: ``,
- name: b.title,
- quality: "Attachment",
- type: "Url",
- });
- }
- supplementary_assets_remaining--;
- if (!supplementary_assets_remaining) {
- remaining--;
- courseData["totalLectures"] += 1;
-
- if (!remaining) {
- console.log("download de video anexo", courseData);
- if (Object.keys(availableSubs).length) {
- askForSubtitle(availableSubs, initDownload, $course, courseData, defaultSubtitle);
- } else {
- initDownload($course, courseData);
- }
- }
- }
- }).catch((error) => {
- const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
- appendLog(`getLectureAndAttachments_Error: (${statusCode})`, error.message);
- resetCourse($course, $course.find(".download-error"), false, courseData);
- });
- });
- } else {
- remaining--;
- courseData["totalLectures"] += 1;
-
- if (!remaining) {
- if (Object.keys(availableSubs).length) {
- askForSubtitle(availableSubs, initDownload, $course, courseData, defaultSubtitle);
- } else {
- initDownload($course, courseData);
- }
- }
- }
- }).catch((error) => {
- const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
- appendLog(`getLecture_Error: (${statusCode})`, error.message);
- resetCourse($course, $course.find(".download-error"), false, courseData);
- });
+ if (!course.completed && Settings.download.autoStartDownload) {
+ setupDownload($courseItem, course.selectedSubtitle);
}
- getLecture(v.title, chapterIndex, lectureIndex);
- lectureIndex++;
- } else if (downloadType != Settings.DownloadType.OnlyLectures) {
- const srcUrl = `https://${Settings.subDomain}.udemy.com${$course.attr("course-url")}t/${v._class}/${v.id}`;
-
- // Adiciona um chapter default, para cursos que tem apenas quiz
- if (courseData["chapters"].length === 0) {
- chapterIndex++;
- lectureIndex = 0;
- courseData["chapters"][chapterIndex] = [];
- courseData["chapters"][chapterIndex]["name"] = "Chapter 0";
- courseData["chapters"][chapterIndex]["lectures"] = [];
+ // Simula atraso de 200ms para demonstração
+ // setTimeout(() => { resolve(); }, 200);
+ resolve();
+ });
+ }
+
+ const promises = downloadedCourses.map(course => addCourseToDOM(course));
+
+ // Executa todas as Promessas em paralelo
+ Promise.all(promises)
+ .then(() => ui.busyLoadDownloads(false))
+ .catch(error => {
+ console.error("Error adding courses:", error);
+ ui.busyLoadDownloads(false);
+ });
+
+ }
+}
+
+async function fetchCourseContent(courseId, courseName, courseUrl) {
+ try {
+ ui.busyBuildCourseData(true);
+
+ const response = await udemyService.fetchCourseContent(courseId, "all")
+ if (!response) {
+ ui.busyBuildCourseData(false);
+ showAlert(`Id: ${courseId}`, translate("Course not found"));
+ return null;
+ }
+
+ const downloadType = Number(Settings.download.type);
+ const downloadAttachments = downloadType === Settings.DownloadType.Both || downloadType === Settings.DownloadType.OnlyAttachments;
+
+ const courseData = {
+ id: courseId,
+ name: courseName,
+ chapters: [],
+ totalLectures: 0,
+ encryptedVideos: 0,
+ errorCount: 0,
+ availableSubs: [],
+ };
+
+ let chapterData = null;
+ response.results.forEach((item) => {
+ const type = item._class.toLowerCase();
+ if (type == "chapter") {
+ if (chapterData) {
+ courseData.chapters.push(chapterData);
}
+ chapterData = { id: item.id, name: item.title.trim(), lectures: [] };
+ }
+ else if (type == "quiz") {
+ const srcUrl = `${courseUrl}t/${item._class}/${item.id}`;
- courseData["chapters"][chapterIndex]["lectures"][lectureIndex] = {
+ chapterData.lectures.push({
+ type: "url",
+ name: item.title,
src: ``,
- name: v.title,
quality: "Attachment",
- type: "Url",
- };
- remaining--;
- courseData["totalLectures"] += 1;
-
- if (!remaining) {
- if (Object.keys(availableSubs).length) {
- askForSubtitle(availableSubs, initDownload, $course, courseData, defaultSubtitle);
- } else {
- initDownload($course, courseData);
+ });
+ courseData.totalLectures++;
+ }
+ else {
+ const lecture = { type, name: item.title, src: "", quality: Settings.download.videoQuality, isEncrypted: false };
+ const { asset, supplementary_assets } = item;
+
+ if (asset.asset_type.toLowerCase() == "article") {
+ lecture.type = "article";
+ lecture.quality = asset.asset_type;
+ lecture.src = asset.data?.body ?? asset.body;
+
+ } else if (asset.asset_type.toLowerCase() == "file" || asset.asset_type.toLowerCase() == "e-book") {
+ lecture.type = "file";
+ lecture.quality = asset.asset_type;
+ lecture.src = asset.download_urls[asset.asset_type][0].file;
+
+ } else {
+ const streams = asset.streams;
+ switch (lecture.quality.toLowerCase()) {
+ case "auto":
+ case "highest":
+ lecture.quality = streams.maxQuality.toString();
+ break;
+ case "lowest":
+ lecture.quality = streams.minQuality.toString();
+ break;
+ default:
+ lecture.quality = lecture.quality.slice(0, -1);
}
- }
- lectureIndex++;
- } else {
- remaining--;
- if (!remaining) {
- if (Object.keys(availableSubs).length) {
- askForSubtitle(availableSubs, initDownload, $course, courseData, defaultSubtitle);
- } else {
- initDownload($course, courseData);
+ if (!streams.sources[lecture.quality]) {
+ lecture.quality = streams.maxQuality.toString();
+ }
+ lecture.src = streams.sources[lecture.quality].url;
+ lecture.type = streams.sources[lecture.quality].type;
+ if (streams.isEncrypted) {
+ lecture.isEncrypted = true;
+ courseData.encryptedVideos++;
}
}
+
+ if (!Settings.download.skipSubtitles && asset.captions.length > 0) {
+ // TODO: renomear para subtitles
+ lecture.caption = {};
+
+ asset.captions.forEach((caption) => {
+ caption.video_label in courseData.availableSubs
+ ? (courseData.availableSubs[caption.video_label] = courseData.availableSubs[caption.video_label] + 1)
+ : (courseData.availableSubs[caption.video_label] = 1);
+
+ lecture.caption[caption.video_label] = caption.url;
+ });
+ }
+
+ if (downloadAttachments && supplementary_assets.length > 0) {
+ const attachments = lecture.supplementary_assets = [];
+
+ supplementary_assets.forEach((attachment) => {
+ const type = attachment.download_urls ? "file" : "url";
+ const src = attachment.download_urls
+ ? attachment.download_urls[attachment.asset_type][0].file
+ : ``;
+
+ attachments.push({ type, name: attachment.title, src, quality: "Attachment" });
+ });
+ }
+
+ chapterData.lectures.push(lecture);
+ courseData.totalLectures++;
}
});
- }).catch((error) => {
+
+ if (chapterData) {
+ courseData.chapters.push(chapterData);
+ }
+
+ ui.busyBuildCourseData(false);
+ return courseData;
+ } catch (error) {
let msgError;
- const statusCode = error.response?.status || 0;
+ const statusCode = error.response ? error.response.status : 0;
switch (statusCode) {
case 403:
- msgError = translate("You do not have permission to access this course") + `\nId: ${courseId}`;
- showAlert(msgError, "Download Error");
+ msgError = translate("You do not have permission to access this course");
+ prompt.alert(msgError);
break;
case 504:
msgError = "Gateway timeout";
@@ -839,87 +775,312 @@ function initializeDownload($course, subtitle) {
msgError = error.message;
break;
}
- const errorCode = error.code ? ` :${error.code}` : "";
- appendLog(`download_Error: (${statusCode}${errorCode})`, msgError);
-
- ui.enableDownloadButton($course, true);
- ui.showProgress($course, false);
- }).finally(() => {
- ui.busyPrepareDownload(false);
- });
+ appendLog(`fetchCourseContent: ${error.code}(${statusCode})`, msgError);
+ throw utils.newError("EBUILD_COURSE_DATA", msgError);
+ }
}
-function initDownload($course, courseData, subTitle = "") {
- const $clone = $course.clone();
- const subtitle = (Array.isArray(subTitle) ? subTitle[0] : subTitle).split("|");
- const $downloads = $(".ui.downloads.section .ui.courses.items");
- const $courses = $(".ui.courses.section .ui.courses.items");
+async function fetchCourses(isSubscriber) {
+ ui.busyLoadCourses(true);
- $course.find('input[name="selectedSubtitle"]').val(subtitle);
- if ($course.parents(".courses.section").length) {
- const $downloadItem = $downloads.find("[course-id=" + $course.attr("course-id") + "]");
- if ($downloadItem.length) {
- $downloadItem.replaceWith($clone);
- } else {
- $downloads.prepend($clone);
- }
- } else {
- const $courseItem = $courses.find("[course-id=" + $course.attr("course-id") + "]");
- if ($courseItem.length) {
- $courseItem.replaceWith($clone);
+ try {
+ const courses = await udemyService.fetchCourses(PAGE_SIZE, isSubscriber);
+ console.log("fetch courses", courses);
+ renderCourses(courses);
+ if (Settings.downloadedCourses) {
+ renderDownloads();
}
- }
- $course.push($clone[0]);
-
- let timerDownloader = null;
- const downloader = new Downloader();
- const $actionButtons = $course.find(".action.buttons");
- const $pauseButton = $actionButtons.find(".pause.button");
- const $resumeButton = $actionButtons.find(".resume.button");
- const lectureChapterMap = {};
- const labelColorMap = {
- 144: "purple",
- 240: "orange",
- 360: "blue",
- 480: "teal",
- 720: "olive",
- 1080: "green",
- Highest: "green",
- auto: "red",
- Attachment: "pink",
- Subtitle: "black",
- };
- let currentLecture = 0;
- courseData["chapters"].forEach(function (lecture, chapterIndex) {
- lecture["lectures"].forEach(function (x, lectureIndex) {
- currentLecture++;
- lectureChapterMap[currentLecture] = { chapterIndex, lectureIndex };
- });
- });
-
- const courseName = sanitize(courseData["name"]); //, { replacement: (s) => "? ".indexOf(s) > -1 ? "" : "-", }).trim();
- const $progressCombined = $course.find(".combined.progress");
- const $progressIndividual = $course.find(".individual.progress");
-
- const $downloadSpeed = $course.find(".download-speed");
- const $downloadSpeedValue = $downloadSpeed.find(".value");
- const $downloadSpeedUnit = $downloadSpeed.find(".download-unit");
- const $downloadQuality = $course.find(".download-quality");
- const setLabelQuality = (label) => {
- const lastClass = $downloadQuality.attr("class").split(" ").pop();
-
- $downloadQuality
- .html(label.toString() + (!isNaN(parseFloat(label)) ? "p" : ""))
- .removeClass(lastClass)
- .addClass(labelColorMap[label] || "grey");
+ } catch (error) {
+ appendLog("Error Fetching Courses", error);
+ showAlert(error.message, "Fetching Courses");
+ } finally {
+ ui.busyLoadCourses(false);
}
+}
- $course.css("cssText", "padding-top: 35px !important").css("padding-bottom", "25px");
+function loadMore(loadMoreButton) {
+ const $button = $(loadMoreButton);
+ const $courses = $button.prev(".courses.items");
+ const url = $button.data("url");
+
+ ui.busyLoadCourses(true);
+ axios({
+ timeout: HTTP_TIMEOUT,
+ method: "GET",
+ url,
+ headers: { Authorization: `Bearer weweqvasdvsadfw` } //headersAuth,
+ }).then(({ data: resp }) => {
+ // $.each(resp.results, (_index, course) => {
+ // htmlCourseCard(course).appendTo($courses);
+ // });
+ $courses.append(...resp.results.map(createCourseElement));
+ if (!resp.next) {
+ $button.remove();
+ } else {
+ $button.data("url", resp.next);
+ }
+ }).catch(error => {
+ const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
+ appendLog(`loadMore_Error: (${statusCode})`, error);
+ }).finally(() => {
+ ui.busyLoadCourses(false);
+ });
+}
+
+async function search(keyword) {
+ ui.busyLoadCourses(true);
+
+ try {
+ const courses = await udemyService.fetchSearchCourses(keyword, PAGE_SIZE, Settings.subscriber);
+ console.log("search courses", courses);
+ renderCourses(courses, !!keyword);
+ } catch (error) {
+ const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
+ appendLog(`search_Error: (${statusCode})`, error);
+ } finally {
+ ui.busyLoadCourses(false);
+ }
+}
+
+function getMsgChangeSearchMode() {
+ const msg = Settings.subscriber
+ ? translate("This account has been identified with a subscription plan")
+ : translate("This account was identified without a subscription plan");
+
+ const button = `
+
+
+
`;
+
+ return `
${msg}
${translate("If it's wrong, change the search mode and try again")}${button}
`;
+}
+
+/**
+ * Toggles the subscriber setting and clears the search field.
+ */
+function toggleSubscriber() {
+ Settings.subscriber = !Settings.subscriber;
+ search("");
+}
+
+function addDownloadHistory(courseId, completed = false, encryptedVideos = 0, selectedSubtitle = "", pathDownloaded = "") {
+ const items = Settings.downloadHistory;
+ const index = items.findIndex((x) => x.id == courseId);
+
+ completed = Boolean(completed);
+
+ if (index !== -1) {
+ const item = items[index];
+ if (completed !== item.completed) {
+ item.completed = completed;
+ item.date = new Date(Date.now()).toLocaleDateString();
+ }
+ item.encryptedVideos = encryptedVideos;
+ item.selectedSubtitle = selectedSubtitle;
+ item.pathDownloaded = pathDownloaded;
+ } else {
+ items.push({
+ id: courseId,
+ completed,
+ date: new Date(Date.now()).toLocaleDateString(),
+ encryptedVideos,
+ selectedSubtitle,
+ pathDownloaded,
+ });
+ }
+
+ Settings.downloadHistory = items;
+}
+
+function getDownloadHistory(courseId) {
+ return Settings.downloadHistory.find((x) => x.id === courseId) || undefined;
+}
+
+function saveDownloads(shouldQuitApp) {
+ ui.busySavingHistory(true);
+
+ function getProgress($progress) {
+ const dataPercent = $progress.attr("data-percent");
+ return parseInt(dataPercent, 10);
+ }
+
+ const downloadedCourses = [];
+ const downloads = $(".ui.downloads.section .ui.courses.items .ui.course.item");
+
+ downloads.each((_index, element) => {
+ const $el = $(element);
+ const hasProgress = $el.find(".progress.active").length > 0;
+ const individualProgress = hasProgress ? getProgress($el.find(".download-status .individual.progress")) : 0;
+ const combinedProgress = hasProgress ? getProgress($el.find(".download-status .combined.progress")) : 0;
+ const isCompleted = !hasProgress && $el.attr("course-completed") === "true";
+
+ const courseData = {
+ id: $el.attr("course-id"),
+ url: $el.attr("course-url"),
+ title: $el.find(".coursename").text(),
+ image: $el.find(".image img").attr("src"),
+ individualProgress: Math.min(100, individualProgress),
+ combinedProgress: Math.min(100, combinedProgress),
+ completed: isCompleted,
+ progressStatus: $el.find(".download-status .label").text(),
+ encryptedVideos: $el.find('input[name="encryptedvideos"]').val(),
+ selectedSubtitle: $el.find('input[name="selectedSubtitle"]').val(),
+ pathDownloaded: $el.find('input[name="path-downloaded"]').val(),
+ };
+
+ downloadedCourses.push(courseData);
+ addDownloadHistory(
+ courseData.id,
+ courseData.completed,
+ courseData.encryptedVideos,
+ courseData.selectedSubtitle,
+ courseData.pathDownloaded
+ );
+ });
+
+ Settings.downloadedCourses = downloadedCourses;
+
+ if (shouldQuitApp) {
+ ipcRenderer.send("quitApp");
+ } else {
+ ui.busySavingHistory(false);
+ }
+}
+
+function removeCurseDownloads(courseId) {
+ const $downloads = $(".ui.downloads.section .ui.courses.items .ui.course.item"); //.slice(0);
+
+ $downloads.each((_index, element) => {
+ const $el = $(element);
+ if ($el.attr("course-id") == courseId) {
+ $el.remove();
+ }
+ });
+}
+
+async function setupDownload($course, subtitle) {
+ ui.prepareDownload($course);
+
+ const courseId = $course.attr("course-id");
+ const courseName = $course.find(".coursename").text();
+ const courseUrl = `https://${Settings.subDomain}.udemy.com${$course.attr("course-url")}`;
+
+ const skipSubtitles = Boolean(Settings.download.skipSubtitles);
+ const defaultSubtitle = skipSubtitles ? null : subtitle ?? Settings.download.defaultSubtitle;
+
+ console.clear();
+
+ try {
+ const courseData = await fetchCourseContent(courseId, courseName, courseUrl);
+ if (!courseData) {
+ return;
+ }
+
+ ui.enableDownloadButton($course, false);
+ ui.showProgress($course, true);
+
+ try {
+ console.log("Downloading", courseData);
+ askForSubtitle(courseData.availableSubs, courseData.totalLectures, defaultSubtitle, (subtitle) => {
+ startDownload($course, courseData, subtitle);
+ });
+ } catch (error) {
+ throw utils.newError("EASK_FOR_SUBTITLE", error.message);
+ }
+
+ } catch (error) {
+ let msgError;
+ const statusCode = error.response?.status || 0;
+ switch (statusCode) {
+ case 403:
+ msgError = translate("You do not have permission to access this course") + `\nId: ${courseId}`;
+ showAlert(msgError, "Download Error");
+ break;
+ case 504:
+ msgError = "Gateway timeout";
+ break;
+ default:
+ msgError = error;
+ }
+ const errorCode = error.code ? ` :${error.code}` : "";
+ appendLog(`download_Error: (${statusCode}${errorCode})`, msgError);
+ ui.busyOff();
+ }
+}
+
+function startDownload($course, courseData, subTitle = "") {
+ const $clone = $course.clone();
+ const subtitle = (Array.isArray(subTitle) ? subTitle[0] : subTitle).split("|");
+ const $downloads = $(".ui.downloads.section .ui.courses.items");
+ const $courses = $(".ui.courses.section .ui.courses.items");
+
+ $course.find('input[name="selectedSubtitle"]').val(subtitle);
+ if ($course.parents(".courses.section").length) {
+ const $downloadItem = $downloads.find("[course-id=" + $course.attr("course-id") + "]");
+ if ($downloadItem.length) {
+ $downloadItem.replaceWith($clone);
+ } else {
+ $downloads.prepend($clone);
+ }
+ } else {
+ const $courseItem = $courses.find("[course-id=" + $course.attr("course-id") + "]");
+ if ($courseItem.length) {
+ $courseItem.replaceWith($clone);
+ }
+ }
+ $course.push($clone[0]);
+
+ let timerDownloader = null;
+ const downloader = new Downloader();
+ const $actionButtons = $course.find(".action.buttons");
+ const $pauseButton = $actionButtons.find(".pause.button");
+ const $resumeButton = $actionButtons.find(".resume.button");
+
+ const lectureChapterMap = {};
+ let sequenceMap = 0;
+ courseData.chapters.forEach((chapter, chapterIndex) => {
+ chapter.lectures.forEach((_lecture, lectureIndex) => {
+ sequenceMap++;
+ // console.log("currentLecture", sequenceMap, chapterIndex, lectureIndex);
+ lectureChapterMap[sequenceMap] = { chapterIndex, lectureIndex };
+ });
+ });
+
+ const courseName = sanitize(courseData["name"]); //, { replacement: (s) => "? ".indexOf(s) > -1 ? "" : "-", }).trim();
+ const $progressCombined = $course.find(".combined.progress");
+ const $progressIndividual = $course.find(".individual.progress");
+
+ const $downloadSpeed = $course.find(".download-speed");
+ const $downloadSpeedValue = $downloadSpeed.find(".value");
+ const $downloadSpeedUnit = $downloadSpeed.find(".download-unit");
+ const $downloadQuality = $course.find(".download-quality");
+
+ const labelColorMap = {
+ 144: "purple",
+ 240: "orange",
+ 360: "blue",
+ 480: "teal",
+ 720: "olive",
+ 1080: "green",
+ Highest: "green",
+ auto: "red",
+ Attachment: "pink",
+ Subtitle: "black",
+ };
+
+ const setLabelQuality = (label) => {
+ const lastClass = $downloadQuality.attr("class").split(" ").pop();
+ $downloadQuality
+ .html(label.toString() + (!isNaN(parseFloat(label)) ? "p" : ""))
+ .removeClass(lastClass)
+ .addClass(labelColorMap[label] || "grey");
+ }
const downloadDirectory = Settings.downloadDirectory();
$course.find('input[name="path-downloaded"]').val(`${downloadDirectory}/${courseName}`);
$course.find(".open-dir.button").show();
+ $course.css("cssText", "padding-top: 35px !important").css("padding-bottom", "25px");
$pauseButton.click(function () {
stopDownload();
@@ -981,9 +1142,9 @@ function initDownload($course, courseData, subTitle = "") {
fs.mkdirSync(seqName.fullPath, { recursive: true });
downloadLecture(chapterIndex, lectureIndex, countLectures, seqName.name);
- } catch (err) {
- appendLog("downloadChapter_Error:", err.message);
- dialog.showErrorBox("downloadChapter_Error", err.message);
+ } catch (error) {
+ appendLog("downloadChapter_Error:", error);
+ dialog.showErrorBox("downloadChapter_Error", error.message);
resetCourse($course, $course.find(".download-error"), false, courseData);
}
@@ -1004,9 +1165,9 @@ function initDownload($course, courseData, subTitle = "") {
return;
}
- const lectureData = courseData["chapters"][chapterIndex]["lectures"][lectureIndex];
- const lectureType = lectureData["type"].toLowerCase();
- const lectureName = lectureData["name"].trim();
+ const lectureData = courseData.chapters[chapterIndex].lectures[lectureIndex];
+ const lectureType = lectureData.type.toLowerCase();
+ const lectureName = lectureData.name.trim();
const sanitizedLectureName = sanitize(lectureName);
function dlStart(dl, typeVideo, callback) {
@@ -1054,6 +1215,7 @@ function initDownload($course, courseData, subTitle = "") {
case 1:
case -1:
+ console.log(`dl~status: ${dl.status}`);
const stats = dl.getStats();
const speedAndUnit = utils.getDownloadSpeed(stats.present.speed || 0);
$downloadSpeedValue.html(speedAndUnit.value);
@@ -1066,35 +1228,24 @@ function initDownload($course, courseData, subTitle = "") {
dl.emit("end");
clearInterval(timerDownloader);
} else if (dl.status === -1) {
+ console.warn("Download error, retrying...");
axios({
- timeout: ajaxTimeout,
+ timeout: HTTP_TIMEOUT,
type: "HEAD",
url: dl.url,
}).then(() => {
- resetCourse(
- $course,
- $course.find(".download-error"),
- Settings.download.autoRetry,
- courseData,
- subtitle
- );
+ resetCourse($course, $course.find(".download-error"), Settings.download.autoRetry, courseData, subtitle);
}).catch((error) => {
const statusCode = error.response?.status || 0;
const errorCode = error.code ? ` :${error.code}` : "";
- appendLog(`downloadLecture_Error: (${statusCode}${errorCode})`, error.message);
+ appendLog(`downloadLecture_Error: (${statusCode}${errorCode})`, error);
try {
if (statusCode == 401 || statusCode == 403) {
fs.unlinkSync(dl.filePath);
}
} finally {
- resetCourse(
- $course,
- $course.find(".download-error"),
- Settings.download.autoRetry,
- courseData,
- subtitle
- );
+ resetCourse($course, $course.find(".download-error"), Settings.download.autoRetry, courseData, subtitle);
}
});
@@ -1111,6 +1262,7 @@ function initDownload($course, courseData, subTitle = "") {
}, 1000);
dl.on("error", function (dl) {
+ console.error("dl.on(error)", dl.error.message);
if (hasDRMProtection(dl)) {
dl.emit("end");
} else {
@@ -1120,20 +1272,20 @@ function initDownload($course, courseData, subTitle = "") {
dl.on("start", function () {
let file = dl.filePath.split("/").slice(-2).join("/");
-
- console.log("startDownload", file);
+ console.log("dl.on(start)", file);
$pauseButton.removeClass("disabled");
});
dl.on("stop", function () {
- console.warn("stopDownload");
+ console.warn("dl.on(stop)");
});
dl.on("end", function () {
+ console.log("dl.on(end)", { path: dl.filePath, typeVideo });
if (typeVideo && hasDRMProtection(dl)) {
$course.find('input[name="encryptedvideos"]').val(++courseData.encryptedVideos);
- appendLog(`DRM Protected::${courseData.name}`, dl.filePath, false);
+ appendLog(`DRM Protected::${courseData.name}`, dl.filePath);
fs.unlink(dl.filePath + ".mtd", (err) => {
if (err) {
console.error("dl.on(end)__fs.unlink", err.message);
@@ -1154,16 +1306,16 @@ function initDownload($course, courseData, subTitle = "") {
function downloadAttachments(index, totalAttachments) {
$progressIndividual.progress("reset");
- const attachment = lectureData["supplementary_assets"][index];
- const attachmentName = attachment["name"].trim();
+ const attachment = lectureData.supplementary_assets[index];
+ const attachmentName = attachment.name.trim();
- setLabelQuality(attachment["quality"]);
+ setLabelQuality(attachment.quality);
- if (attachment["type"] == "Article" || attachment["type"] == "Url") {
+ if (["article", "url"].includes(attachment.type)) {
const wfDir = downloadDirectory + "/" + courseName + "/" + chapterName;
fs.writeFile(
utils.getSequenceName(lectureIndex + 1, countLectures, attachmentName + ".html", `.${index + 1} `, wfDir).fullPath,
- attachment["src"],
+ attachment.src,
function () {
index++;
if (index == totalAttachments) {
@@ -1176,7 +1328,6 @@ function initDownload($course, courseData, subTitle = "") {
}
);
} else {
- //Download anexos
let fileExtension = attachment.src.split("/").pop().split("?").shift().split(".").pop();
fileExtension = attachment.name.split(".").pop() == fileExtension ? "" : "." + fileExtension;
@@ -1188,24 +1339,21 @@ function initDownload($course, courseData, subTitle = "") {
`${downloadDirectory}/${courseName}/${chapterName}`
);
+ // try deleting the download started without data
if (fs.existsSync(seqName.fullPath + ".mtd") && !fs.statSync(seqName.fullPath + ".mtd").size) {
fs.unlinkSync(seqName.fullPath + ".mtd");
}
if (fs.existsSync(seqName.fullPath + ".mtd")) {
- console.log("downloadAttachments: Reiniciando download", seqName.fullPath);
var dl = downloader.resumeDownload(seqName.fullPath);
} else if (fs.existsSync(seqName.fullPath)) {
endDownload();
return;
} else {
- if (seqName.fullPath.includes(".mp4") || attachment["type"].toLowerCase() == "video") {
- console.log("downloadAttachements: Iniciando download do Video", attachment["src"]);
- }
- var dl = downloader.download(attachment["src"], seqName.fullPath);
+ var dl = downloader.download(attachment.src, seqName.fullPath);
}
- dlStart(dl, attachment["type"].toLowerCase() == "video", endDownload);
+ dlStart(dl, attachment.type.includes("video"), endDownload);
function endDownload() {
index++;
@@ -1224,10 +1372,10 @@ function initDownload($course, courseData, subTitle = "") {
function checkAttachment() {
$progressIndividual.progress("reset");
- const attachment = lectureData["supplementary_assets"];
+ const attachment = lectureData.supplementary_assets;
if (attachment) {
- lectureData["supplementary_assets"].sort(utils.dynamicSort("name"));
+ lectureData.supplementary_assets.sort(utils.dynamicSort("name"));
downloadAttachments(0, attachment.length);
} else {
@@ -1315,8 +1463,8 @@ function initDownload($course, courseData, subTitle = "") {
return await response.text();
} else console.log("getFile_Buffer", response.statusText);
- } catch (err) {
- appendLog("getFile_Error", err.message);
+ } catch (error) {
+ appendLog("getFile_Error", error);
}
retry++;
@@ -1360,9 +1508,9 @@ function initDownload($course, courseData, subTitle = "") {
maximumQuality = readQuality;
getUrl = true;
}
- } catch (err) {
- appendLog("getPlaylist_Error", err.message);
- captureException(err);
+ } catch (error) {
+ appendLog("getPlaylist_Error", error);
+ captureException(error);
}
}
}
@@ -1380,20 +1528,20 @@ function initDownload($course, courseData, subTitle = "") {
$progressIndividual.progress("reset");
- const lectureQuality = lectureData["quality"];
+ const lectureQuality = lectureData.quality;
setLabelQuality(lectureQuality);
if (lectureType == "article" || lectureType == "url") {
const wfDir = `${downloadDirectory}/${courseName}/${chapterName}`;
fs.writeFile(
utils.getSequenceName(lectureIndex + 1, countLectures, sanitizedLectureName + ".html", ". ", wfDir).fullPath,
- lectureData["src"],
+ lectureData.src,
function () {
- if (lectureData["supplementary_assets"]) {
- lectureData["supplementary_assets"].sort(
+ if (lectureData.supplementary_assets) {
+ lectureData.supplementary_assets.sort(
utils.dynamicSort("name")
);
- const totalAttachments = lectureData["supplementary_assets"].length;
+ const totalAttachments = lectureData.supplementary_assets.length;
let indexador = 0;
downloadAttachments(indexador, totalAttachments);
} else {
@@ -1416,7 +1564,7 @@ function initDownload($course, courseData, subTitle = "") {
const skipLecture = Number(Settings.download.type) === Settings.DownloadType.OnlyAttachments;
// if not stream
- if (lectureQuality != "Highest") {
+ if (lectureType == "video/mp4" || lectureType == "video") {
if (fs.existsSync(seqName.fullPath + ".mtd") && !fs.statSync(seqName.fullPath + ".mtd").size) {
fs.unlinkSync(seqName.fullPath + ".mtd");
}
@@ -1428,11 +1576,11 @@ function initDownload($course, courseData, subTitle = "") {
endDownloadAttachment();
return;
} else {
- console.log("downloadLecture: Iniciando download do Video ", lectureData["src"]);
- var dl = downloader.download(lectureData["src"], seqName.fullPath);
+ console.log("downloadLecture: Iniciando download do Video ", lectureData.src);
+ var dl = downloader.download(lectureData.src, seqName.fullPath);
}
- dlStart(dl, lectureType == "video", endDownloadAttachment);
+ dlStart(dl, lectureType.includes("video"), endDownloadAttachment);
} else {
if (fs.existsSync(seqName.fullPath + ".mtd")) {
fs.unlinkSync(seqName.fullPath + ".mtd");
@@ -1441,8 +1589,8 @@ function initDownload($course, courseData, subTitle = "") {
return;
}
- getPlaylist(lectureData["src"]).then(async (list) => {
- console.log("getPlaylist~getFile(binary): ", lectureData["src"]);
+ getPlaylist(lectureData.src).then(async (list) => {
+ console.log("getPlaylist~getFile(binary): ", lectureData.src);
if (list.length > 0) {
const result = [list.length];
@@ -1477,16 +1625,16 @@ function initDownload($course, courseData, subTitle = "") {
function endDownloadAttachment() {
clearInterval(timerDownloader);
- if (courseData["chapters"][chapterIndex]["lectures"][lectureIndex].caption) {
+ if (courseData.chapters[chapterIndex].lectures[lectureIndex].caption) {
downloadSubtitle();
} else {
checkAttachment();
}
}
}
- } catch (err) {
- appendLog("downloadLecture_Error:", err.message);
- captureException(err);
+ } catch (error) {
+ appendLog("downloadLecture_Error:", error);
+ captureException(error);
resetCourse($course, $course.find(".download-error"), false, courseData);
}
@@ -1505,13 +1653,16 @@ function initDownload($course, courseData, subTitle = "") {
}
}
-function askForSubtitle(subtitlesAvailable, fnDownload, $course, courseData, defaultSubtitle = "") {
+function askForSubtitle(subtitlesAvailable, totalLectures, defaultSubtitle = "", callback) {
const subtitleLanguages = [];
const languages = [];
const totals = {};
const languageKeys = {};
- if (!subtitlesAvailable) return;
+ if (Object.keys(subtitlesAvailable).length === 0) {
+ callback("");
+ return;
+ }
defaultSubtitle = defaultSubtitle.replace(/\s*\[.*?\]/g, '').trim();
for (const key in subtitlesAvailable) {
@@ -1519,7 +1670,7 @@ function askForSubtitle(subtitlesAvailable, fnDownload, $course, courseData, def
// default subtitle exists
if (subtitle === defaultSubtitle) {
- fnDownload($course, courseData, key);
+ callback(key);
return;
}
@@ -1534,14 +1685,14 @@ function askForSubtitle(subtitlesAvailable, fnDownload, $course, courseData, def
};
if (languages.length === 1) {
- fnDownload($course, courseData, languageKeys[0]);
+ callback(languageKeys[0]);
return;
} else if (languages.length === 0) {
return;
}
languages.forEach(language => {
- totals[language] = Math.min(courseData["totalLectures"], totals[language]);
+ totals[language] = Math.min(totalLectures, totals[language]);
});
languages.sort();
@@ -1551,6 +1702,7 @@ function askForSubtitle(subtitlesAvailable, fnDownload, $course, courseData, def
value: languageKeys[language].join("|"),
});
});
+ subtitleLanguages.unshift({ name: "", value: "" });
const $subtitleModal = $(".ui.subtitle.modal");
const $subtitleDropdown = $subtitleModal.find(".ui.dropdown");
@@ -1561,310 +1713,11 @@ function askForSubtitle(subtitlesAvailable, fnDownload, $course, courseData, def
onChange: (subtitle) => {
$subtitleModal.modal("hide");
$subtitleDropdown.dropdown({ values: [] });
- fnDownload($course, courseData, subtitle);
+ callback(subtitle);
},
});
}
-
-function resetCourse($course, $elMessage, autoRetry, courseData, subtitle) {
- if ($elMessage.hasClass("download-success")) {
- $course.attr("course-completed", true);
- } else {
- $course.attr("course-completed", "");
-
- if ($elMessage.hasClass("download-error")) {
- if (autoRetry && courseData.errorCount++ < 5) {
- $course.length = 1;
- initDownload($course, courseData, subtitle);
- return;
- }
- }
- }
-
- $course.find(".download-quality").hide();
- $course.find(".download-speed").hide().find(".value").html(0);
- $course.find(".download-status").hide().html(ui.actionCardTemplate);
- $course.css("padding", "14px 0px");
- $elMessage.css("display", "flex");
-}
-
-function renderCourses(response, keyword = "") {
- console.log("renderCourse", { response, keyword });
-
- const $coursesSection = $(".ui.dashboard .ui.courses.section");
- const $coursesItems = $coursesSection.find(".ui.courses.items").empty();
-
- $coursesSection.find(".disposable").remove();
-
- if (response.results.length) {
- // response.results.forEach(course => {
- // $coursesItems.append(htmlCourseCard(course));
- // });
- const courseElements = response.results.map(course => htmlCourseCard(course));
- $coursesItems.append(courseElements);
-
- if (response.next) {
- // added loadMore Button
- $coursesSection.append(
- `
`
- );
- }
-
- } else {
- let msg = "";
- if (!keyword.length) {
- msg = getMsgChangeSearchMode();
- appendLog(translate("No Courses Found"), msg, false);
- }
-
- $coursesItems.append(
- `
- ${translate("No Courses Found")}
- ${translate("Remember, you will only be able to see the courses you are enrolled in")}
- ${msg}
-
`
- );
- }
-
-}
-
-async function renderDownloads() {
- console.log("renderDownloads");
-
- const $downloadsSection = $(".ui.downloads.section .ui.courses.items");
- if ($downloadsSection.find(".ui.course.item").length) {
- return;
- }
-
- const downloadedCourses = Settings.downloadedCourses || [];
- if (!downloadedCourses.length) {
- $downloadsSection.append(
- `
- ${translate("There are no Downloads to display")}
-
`
- );
- }
- else {
- ui.busyLoadDownloads(true);
- // await utils.sleep(10);
- // // downloadedCourses.forEach(course => {
- // downloadedCourses.map(course => {
- // const $courseItem = htmlCourseCard(course, true);
- // $downloadsSection.append($courseItem);
-
- // if (!course.completed && Settings.download.autoStartDownload) {
- // initializeDownload($courseItem, course.selectedSubtitle);
- // // $courseItem.find(".action.buttons").find(".pause.button").removeClass("disabled");
- // }
- // });
- // ui.busyLoadDownloads(false);
-
- function addCourseToDOM(course) {
- return new Promise((resolve, _reject) => {
- const $courseItem = htmlCourseCard(course, true);
- $downloadsSection.append($courseItem);
-
- if (!course.completed && Settings.download.autoStartDownload) {
- initializeDownload($courseItem, course.selectedSubtitle);
- }
-
- // Simula atraso de 200ms para demonstração
- // setTimeout(() => { resolve(); }, 200);
- resolve();
- });
- }
-
- const promises = downloadedCourses.map(course => addCourseToDOM(course));
-
- // Executa todas as Promessas em paralelo
- Promise.all(promises)
- .then(() => ui.busyLoadDownloads(false))
- .catch(error => {
- console.error("Error adding courses:", error);
- ui.busyLoadDownloads(false);
- });
-
- }
-}
-
-function loadMore(loadMoreButton) {
- const $button = $(loadMoreButton);
- const $courses = $button.prev(".courses.items");
- const url = $button.data("url");
-
- ui.busyLoadCourses(true);
- axios({
- timeout: ajaxTimeout,
- method: "GET",
- url,
- headers: { Authorization: `Bearer weweqvasdvsadfw` } //headersAuth,
- }).then(({ data: resp }) => {
- // $.each(resp.results, (_index, course) => {
- // htmlCourseCard(course).appendTo($courses);
- // });
- $courses.append(...resp.results.map(htmlCourseCard));
- if (!resp.next) {
- $button.remove();
- } else {
- $button.data("url", resp.next);
- }
- }).catch(error => {
- const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
- appendLog(`loadMore_Error: (${statusCode})`, error.message);
- }).finally(() => {
- ui.busyLoadCourses(false);
- });
-}
-
-function search(keyword) {
- const subscriber = Settings.subscriber;
- const url = !subscriber
- ? `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscribed-courses?page=1&page_size=30&ordering=title&fields[user]=job_title&page_size=${pageSize}&search=${keyword}`
- : `https://${Settings.subDomain}.udemy.com/api-2.0/users/me/subscription-course-enrollments?page=1&page_size=30&ordering=title&fields[user]=job_title&page_size=${pageSize}&search=${keyword}`;
-
- console.log("search", url);
-
- ui.busyLoadCourses(true);
- axios({
- timeout: ajaxTimeout, // timeout to 5 seconds
- method: "GET",
- url,
- headers: headersAuth,
- }).then(response => {
- console.log("search done");
- renderCourses(response.data, keyword);
- ui.busyLoadCourses(false);
- }).catch(error => {
- const statusCode = (error.response?.status || 0).toString() + (error.code ? ` :${error.code}` : "");
- appendLog(`search_Error: (${statusCode})`, error.message);
- ui.busyLoadCourses(false);
- });
-}
-
-/**
- * Returns a message for changing search mode based on account subscription status
- *
- * @return {string} Message for changing search mode
- */
-function getMsgChangeSearchMode() {
- const msg = Settings.subscriber
- ? translate("This account has been identified with a subscription plan")
- : translate("This account was identified without a subscription plan");
-
- const button = `
-
-
-
`;
-
- return `
${msg}
${translate("If it's wrong, change the search mode and try again")}${button}
`;
-}
-
-/**
- * Toggles the subscriber setting and clears the search field.
- */
-function toggleSubscriber() {
- Settings.subscriber = !Settings.subscriber;
- search("");
-}
-
-function addDownloadHistory(courseId, completed = false, encryptedVideos = 0, selectedSubtitle = "", pathDownloaded = "") {
- const items = Settings.downloadHistory;
- const index = items.findIndex((x) => x.id == courseId);
-
- completed = Boolean(completed);
-
- if (index !== -1) {
- const item = items[index];
- if (completed !== item.completed) {
- item.completed = completed;
- item.date = new Date(Date.now()).toLocaleDateString();
- }
- item.encryptedVideos = encryptedVideos;
- item.selectedSubtitle = selectedSubtitle;
- item.pathDownloaded = pathDownloaded;
- } else {
- items.push({
- id: courseId,
- completed,
- date: new Date(Date.now()).toLocaleDateString(),
- encryptedVideos,
- selectedSubtitle,
- pathDownloaded,
- });
- }
-
- Settings.downloadHistory = items;
-}
-
-function getDownloadHistory(courseId) {
- return Settings.downloadHistory.find((x) => x.id === courseId) || undefined;
-}
-
-function saveDownloads(shouldQuitApp) {
- ui.busySavingHistory(true);
-
- function getProgress($progress) {
- const dataPercent = $progress.attr("data-percent");
- return parseInt(dataPercent, 10);
- }
-
- const downloadedCourses = [];
- const downloads = $(".ui.downloads.section .ui.courses.items .ui.course.item");
-
- downloads.each((_index, element) => {
- const $el = $(element);
- const hasProgress = $el.find(".progress.active").length > 0;
- const individualProgress = hasProgress ? getProgress($el.find(".download-status .individual.progress")) : 0;
- const combinedProgress = hasProgress ? getProgress($el.find(".download-status .combined.progress")) : 0;
- const isCompleted = !hasProgress && $el.attr("course-completed") === "true";
-
- const courseData = {
- id: $el.attr("course-id"),
- url: $el.attr("course-url"),
- title: $el.find(".coursename").text(),
- image: $el.find(".image img").attr("src"),
- individualProgress: Math.min(100, individualProgress),
- combinedProgress: Math.min(100, combinedProgress),
- completed: isCompleted,
- progressStatus: $el.find(".download-status .label").text(),
- encryptedVideos: $el.find('input[name="encryptedvideos"]').val(),
- selectedSubtitle: $el.find('input[name="selectedSubtitle"]').val(),
- pathDownloaded: $el.find('input[name="path-downloaded"]').val(),
- };
-
- downloadedCourses.push(courseData);
- addDownloadHistory(
- courseData.id,
- courseData.completed,
- courseData.encryptedVideos,
- courseData.selectedSubtitle,
- courseData.pathDownloaded
- );
- });
-
- Settings.downloadedCourses = downloadedCourses;
-
- if (shouldQuitApp) {
- ipcRenderer.send("quitApp");
- } else {
- ui.busySavingHistory(false);
- }
-}
-
-function removeCurseDownloads(courseId) {
- const $downloads = $(".ui.downloads.section .ui.courses.items .ui.course.item"); //.slice(0);
-
- $downloads.each((_index, element) => {
- const $el = $(element);
- if ($el.attr("course-id") == courseId) {
- $el.remove();
- }
- });
-}
-
function sendNotification(pathCourse, courseName, urlImage = null) {
try {
new Notification(courseName, {
@@ -1874,7 +1727,7 @@ function sendNotification(pathCourse, courseName, urlImage = null) {
shell.openPath(pathCourse);
};
} catch (error) {
- appendLog("sendNotification", error.message, true);
+ appendLog("sendNotification", error);
}
}
@@ -1889,7 +1742,9 @@ function clearBagdeLoggers() {
$("#badge-logger").hide();
}
-function appendLog(title, description, isError = true) {
+function appendLog(title, error = "" | Error) {
+ const description = error instanceof Error ? error.message : error;
+
// item added to list to display
$(".ui.logger.section .ui.list").prepend(
`
@@ -1914,8 +1769,8 @@ function appendLog(title, description, isError = true) {
$badge.text(qtd > 99 ? "99+" : qtd);
$badge.show();
- if (isError) {
- console.error(`[${title}] ${description}`);
+ if (error instanceof Error) {
+ console.error(`[${title}] ${error.message}\n ${error.stack}`);
} else {
console.warn(`[${title}] ${description}`);
}
@@ -1941,9 +1796,9 @@ function saveLogFile() {
content += `${item.datetime} - ${item.title}: ${item.description}\n`;
});
- fs.writeFile(filePath, content, (err) => {
- if (err) {
- appendLog("saveLogFile_Error", err.message);
+ fs.writeFile(filePath, content, (error) => {
+ if (error) {
+ appendLog("saveLogFile_Error", error);
// captureException(err);
return;
}
@@ -1963,12 +1818,12 @@ function captureException(exception) {
}
process.on("uncaughtException", (error) => {
- appendLog("uncaughtException", error.stack);
+ appendLog("uncaughtException", error);
captureException(error);
});
process.on("unhandledRejection", (error) => {
- appendLog("unhandledRejection", error.stack);
+ appendLog("unhandledRejection", error);
captureException(error);
});
diff --git a/app/core/services/index.js b/app/core/services/index.js
new file mode 100644
index 00000000..eeb35987
--- /dev/null
+++ b/app/core/services/index.js
@@ -0,0 +1,7 @@
+const UdemyService = require("./udemy.service");
+const M3U8Service = require("./m3u8.service");
+
+module.exports = {
+ default: UdemyService,
+ M3U8Service,
+}
\ No newline at end of file
diff --git a/app/core/services/m3u8.service.js b/app/core/services/m3u8.service.js
new file mode 100644
index 00000000..14177c19
--- /dev/null
+++ b/app/core/services/m3u8.service.js
@@ -0,0 +1,168 @@
+"use strict"
+
+class M3U8Service {
+
+ /**
+ * Creates a new instance of M3U8Playlist.
+ * @param {string} m3u8Url - The URL of the M3U8 playlist.
+ * @returns {M3U8Service} - The newly created M3U8Playlist instance.
+ */
+ constructor(m3u8Url) {
+ if (!this._isValidUrl(m3u8Url)) {
+ throw new Error('Invalid URL');
+ }
+ this._m3u8Url = m3u8Url;
+ /** @type {Array<{quality: number, resolution: string, url: string}>} */
+ this._playlist = [];
+ }
+
+ /**
+ * Validates the URL.
+ * @param {string} url - The URL to validate.
+ * @returns {boolean} - True if the URL is valid, false otherwise.
+ */
+ _isValidUrl(url) {
+ try {
+ new URL(url);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the content is a valid M3U8 playlist.
+ * @param {string} content - The content to check.
+ * @returns {boolean} - True if the content is a valid M3U8 playlist, false otherwise.
+ */
+ _isValidM3U8Content(content) {
+ return content.startsWith('#EXTM3U');
+ }
+
+ /**
+ * Extracts URLs and qualities from an M3U8 playlist content.
+ * @param {string} m3u8Content - The content of the M3U8 playlist.
+ * @returns {Array<{quality: number, resolution: string, url: string}>} - An array of objects containing the quality,
+ * resolution, and URL of each playlist.
+ */
+ _extractUrlsAndQualities(m3u8Content) {
+ const lines = m3u8Content.split('\n');
+ const urlsAndQualities = [];
+
+ let currentResolution = null;
+ let currentQuality = null;
+
+ lines.forEach(line => {
+ if (line.startsWith('#EXT-X-STREAM-INF')) {
+ const match = line.match(/RESOLUTION=(\d+x\d+)/);
+ if (match) {
+ currentResolution = match[1];
+ currentQuality = parseInt(match[1].split('x')[1], 10);
+ }
+ } else if (line.startsWith('http')) {
+ if (currentResolution) {
+ urlsAndQualities.push({
+ quality: currentQuality,
+ resolution: currentResolution,
+ url: line
+ });
+ currentResolution = null;
+ currentQuality = null;
+ }
+ }
+ });
+
+ return urlsAndQualities;
+ }
+
+ /**
+ * Fetches a file from the given URL.
+ *
+ * @param {string} url - The URL of the file to fetch.
+ * @param {boolean} [isBinary=false] - Whether the file is binary or text.
+ * @param {number} [maxRetries=3] - The maximum number of retries to fetch the file.
+ * @returns {Promise
} - A promise that resolves with the file content.
+ * @throws {Error} - If the file fails to load after multiple attempts.
+ */
+ static async getFile(url, isBinary = false, maxRetries = 3) {
+ let retries = 0;
+
+ while (retries < maxRetries) {
+ try {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch ${isBinary ? 'binary' : 'text'} file: ${response.statusText}`);
+ }
+
+ return isBinary ? await response.arrayBuffer() : await response.text();
+ } catch (error) {
+ retries++;
+ }
+ }
+
+ throw new Error('Failed to load file after multiple attempts');
+ }
+
+ /**
+ * Loads the M3U8 playlist.
+ *
+ * @param {number} [maxRetries=3] - The maximum number of retries to fetch the playlist.
+ * @returns {Promise} - A promise that resolves when the playlist is loaded successfully.
+ * @throws {Error} - If the playlist fails to load after multiple attempts.
+ */
+ async loadPlaylist(maxRetries = 3) {
+ try {
+ const playlistContent = await M3U8Service.getFile(this._m3u8Url, false, maxRetries);
+ if (!this._isValidM3U8Content(playlistContent)) {
+ throw new Error('Invalid M3U8 playlist content');
+ }
+ this._playlist = this._extractUrlsAndQualities(playlistContent);
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves the playlist.
+ *
+ * @return {Array<{quality: number, resolution: string, url: string}>} The playlist.
+ */
+ getPlaylist() {
+ return this._playlist;
+ }
+
+ _sortPlaylistByQuality(ascending = true) {
+ return [...this._playlist].sort((a, b) => {
+ const heightA = parseInt(a.resolution.split('x')[1], 10);
+ const heightB = parseInt(b.resolution.split('x')[1], 10);
+ return ascending ? heightA - heightB : heightB - heightA;
+ });
+ }
+
+ /**
+ * Retrieves the highest quality item from the playlist.
+ *
+ * @return {Object|null} The highest quality item from the playlist, or null if the playlist is empty.
+ */
+ getHighestQuality() {
+ if (this._playlist.length === 0) {
+ return null;
+ }
+ return this._sortPlaylistByQuality(false)[0]; // Retornar o item de maior qualidade
+ }
+
+ /**
+ * Retrieves the lowest quality item from the playlist.
+ *
+ * @return {Object|null} The lowest quality item from the playlist, or null if the playlist is empty.
+ */
+ getLowestQuality() {
+ if (this._playlist.length === 0) {
+ return null;
+ }
+ return this._sortPlaylistByQuality(true)[0]; // Retornar o item de menor qualidade
+ }
+}
+
+module.exports = M3U8Service;
diff --git a/app/core/services/udemy.service.js b/app/core/services/udemy.service.js
new file mode 100644
index 00000000..a6ade376
--- /dev/null
+++ b/app/core/services/udemy.service.js
@@ -0,0 +1,291 @@
+"use strict";
+
+const axios = require('axios');
+const NodeCache = require('node-cache');
+const M3U8Service = require("./m3u8.service");
+
+class UdemyService {
+ #timeout = 40000;
+ #headerAuth = null;
+
+ #urlBase;
+ #urlLogin;
+ #URL_COURSES = "/users/me/subscribed-courses";
+ #URL_COURSES_ENROLL = "/users/me/subscription-course-enrollments";
+ #ASSETS_FIELDS = "&fields[asset]=asset_type,title,filename,body,captions,media_sources,stream_urls,download_urls,external_url,media_license_token";
+
+ #cache = new NodeCache({ stdTTL: 3600 }); // TTL padrão de 1 hora
+
+ constructor(subDomain = "www", httpTimeout = 40000) {
+ subDomain = (subDomain.trim().length === 0 ? "www" : subDomain.trim()).toLowerCase();
+
+ this.#urlBase = `https://${subDomain}.udemy.com`;
+ this.#timeout = httpTimeout;
+ this.#headerAuth = null;
+ this.#urlLogin = `${this.#urlBase}/join/login-popup`;
+ }
+
+ /**
+ * Creates and returns a new Error object with the specified name and message.
+ *
+ * @param {string} name - The name of the error.
+ * @param {string} [message=""] - The optional error message. Default is an empty string.
+ * @returns {Error} The newly created Error object.
+ */
+ _error(name, message = "") {
+ const error = new Error();
+ error.name = name;
+ error.message = message;
+ return error;
+ }
+
+ async _prepareStreamSource(el) {
+ try {
+ if (el._class === "lecture" && el.asset?.asset_type.toLowerCase() === "video") {
+ const asset = el.asset;
+ const stream_urls = asset.stream_urls?.Video || asset.media_sources;
+ const isEncrypted = Boolean(asset.media_license_token);
+ if (stream_urls) {
+ const streams = await this._convertToStreams(stream_urls, isEncrypted);
+
+ delete el.asset.stream_urls;
+ delete el.asset.media_sources;
+ el.asset.streams = streams;
+ }
+ }
+ } catch (error) {
+ throw this._error("EPREPARE_STREAM_SOURCE", error.message);
+ }
+ }
+
+ async _prepareStreamsSource(items) {
+ console.log("Preparing stream urls...", items);
+ try {
+ const promises = items.map(async el => {
+ await this._prepareStreamSource(el);
+ });
+
+ await Promise.all(promises);
+ } catch (error) {
+ throw this._error("EPREPARE_STREAMS_SOURCE", error.message);
+ }
+ }
+
+ /**
+ * Transforms media sources into a standardized format.
+ *
+ * @param {Array