diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index 100ec253..0c020684 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -1,5 +1,13 @@ # Change Log +## Version [1.14.0](https://github.com/heliomarpm/udemy-downloader-gui/compare/v1.13.4...v1.14.0) +##### Dec, 21 2024 +![](https://img.shields.io/github/downloads/heliomarpm/udemy-downloader-gui/v1.14.0/total) + +### features +- Single search for courses purchased individually or subscribed to via subscription +- Download list m3u file + ## Version [1.13.4](https://github.com/heliomarpm/udemy-downloader-gui/compare/v1.13.3...v1.13.4) ##### Dec, 7 2024 ![](https://img.shields.io/github/downloads/heliomarpm/udemy-downloader-gui/v1.13.4/total) diff --git a/app/app.js b/app/app.js index d33028fa..2fb004fa 100644 --- a/app/app.js +++ b/app/app.js @@ -117,6 +117,10 @@ $(".ui.dashboard .content").on("click", ".download-success, .course-encrypted", $(this).parents(".course").find(".download-status").show(); }); +$(".ui.dashboard .content").on("click", ".save_m3u.button", function (e) { + e.stopImmediatePropagation(); + saveM3u($(this).parents(".course")); +}); $(".ui.dashboard .content").on("click", ".download.button, .download-error", function (e) { e.stopImmediatePropagation(); prepareDownloading($(this).parents(".course")); @@ -301,7 +305,7 @@ async function checkLogin(alertExpired = true) { ui.busyLogin(false); ui.showDashboard(); - Settings.subscriber = utils.toBoolean(userContext.header.user.enableLabsInPersonalPlan); + Settings.subscriber = utils.toBoolean(userContext.header.user.enableLabsInPersonalPlan) || utils.toBoolean(userContext.header.user.consumer_subscription_active); fetchCourses(Settings.subscriber).then(() => { console.log("fetchCourses done"); }); @@ -553,9 +557,10 @@ function renderCourses(response, isResearch = false) { $coursesItems.append(courseElements); if (response.next) { + const dataUrl = Array.isArray(response.next) ? response.next : [response.next]; // added loadMore Button $coursesSection.append( - `` ); @@ -707,6 +712,7 @@ async function fetchCourseContent(courseId, courseName, courseUrl) { } else { switch ( (lecture.quality || "").toLowerCase()) { + case "": case "auto": case "highest": lecture.quality = streams.maxQuality; @@ -715,10 +721,10 @@ async function fetchCourseContent(courseId, courseName, courseUrl) { lecture.quality = streams.minQuality; break; default: - lecture.quality = utils.isNumber(lecture.quality) ? lecture.quality : lecture.quality.slice(0, -1); + lecture.quality = utils.isNumber(lecture.quality) ? lecture.quality : lecture.quality.slice(0, -1); } - if (!streams.sources[lecture.quality]) { + if (lecture.quality && !streams.sources[lecture.quality]) { if (utils.isNumber(lecture.quality) && streams.maxQuality != "auto") { const source = utils.getClosestValue(streams.sources, lecture.quality); lecture.quality = source?.key || streams.maxQuality; @@ -801,18 +807,26 @@ async function fetchCourses(isSubscriber) { function loadMore(loadMoreButton) { const $button = $(loadMoreButton); const $courses = $button.prev(".courses.items"); - const url = $button.data("url"); + const url = [...$button.data("url")]; ui.busyLoadCourses(true); udemyService - .fetchLoadMore(url) + .fetchLoadMore(url[0]) .then((resp) => { $courses.append(...resp.results.map((course) => createCourseElement(course, false))); if (!resp.next) { - $button.remove(); + if (url.length > 1) { + $button.data("url", [url[1]]); + } else { + $button.remove(); + } } else { - $button.data("url", resp.next); - } + if (url.length > 1) { + $button.data("url", [resp.next, url[1]]); + }else { + $button.data("url", [resp.next]); + } + } }) .catch((e) => { const statusCode = (e.response?.status || 0).toString() + (e.code ? ` :${e.code}` : ""); @@ -966,6 +980,68 @@ function removeCurseDownloads(courseId) { }); } +async function saveM3u($course) { + ui.prepareDownloading($course); + + const courseId = $course.attr("course-id"); + const courseName = $course.find(".coursename").text(); + const courseUrl = `https://${Settings.subDomain}.udemy.com${$course.attr("course-url")}`; + + console.clear(); + + let courseData = null; + try { + courseData = await fetchCourseContent(courseId, courseName, courseUrl); + if (!courseData) { + // ui.showProgress($course, false); + return; + } + + console.log(courseData); + dialog + .showSaveDialog({ + title: "Save M3U", + defaultPath: `${courseName}.m3u`, + filters: [{ name: "M3U File (*.m3u)", fileExtension: ["m3u"] }], + }) + .then((result) => { + if (!result.canceled) { + let filePath = result.filePath; + if (!filePath.endsWith(".m3u")) filePath += ".m3u"; + + let content = "#EXTM3U"; + let index = 0; + courseData.chapters.forEach((chapter) => { + chapter.lectures.forEach((lecture, lec_index) => { + index++; + content += `\n#EXTINF:-1,${lec_index+1}. ${lecture.name}\n${lecture.src}`; + + if (lecture.attachments && lecture.attachments.length > 0) + lecture.attachments.forEach((attachment, attach_index) => { + content += `\n#EXTINF:-1,${lec_index+1}.${attach_index+1} ${attachment.name}\n${attachment.src}`; + }) + }) + }); + + fs.writeFile(filePath, content, (error) => { + if (error) { + appendLog("saveM3u_Error", error); + return; + } + console.log("File successfully create!"); + }); + } + }); + + } catch (error) { + handleApiError(error, "ESAVE_M3U", null, false); + ui.busyOff(); + $course.find(".prepare-downloading").hide(); + } finally { + ui.showProgress($course, false); + } +} + async function prepareDownloading($course, subtitle) { ui.prepareDownloading($course); // ui.showProgress($course, true); diff --git a/app/core/services/udemy.service.js b/app/core/services/udemy.service.js index 2c99b857..0a2b6689 100644 --- a/app/core/services/udemy.service.js +++ b/app/core/services/udemy.service.js @@ -247,7 +247,26 @@ class UdemyService { pageSize = Math.max(pageSize, 10); const param = `page=1&ordering=title&fields[user]=job_title&page_size=${pageSize}&search=${keyword}`; - const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`; + // const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`; + const url = `${this.#URL_COURSES}?${param}`; + const urlEnroll = `${this.#URL_COURSES_ENROLL}?${param}`; + + if (isSubscriber) { + const [courses, enrolledCourses] = await Promise.all([ + this.#fetchEndpoint(url, "GET", httpTimeout), + this.#fetchEndpoint(urlEnroll, "GET", httpTimeout) + ]); + + const next = [courses.next, enrolledCourses.next].filter((n) => n !== null); + const previous = [courses.previous, enrolledCourses.previous].filter((p) => p !== null); + + return { + count: courses.count + enrolledCourses.count, + next: next.length > 0 ? next : null, + previous: previous.length > 0 ? previous : null, + results: [...courses.results, ...enrolledCourses.results] + } + } return await this.#fetchEndpoint(url, "GET", httpTimeout); } @@ -256,7 +275,26 @@ class UdemyService { pageSize = Math.max(pageSize, 10); const param = `page_size=${pageSize}&ordering=-last_accessed`; - const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`; + // const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`; + const url = `${this.#URL_COURSES}?${param}`; + const urlEnroll = `${this.#URL_COURSES_ENROLL}?${param}`; + + if (isSubscriber) { + const [courses, enrolledCourses] = await Promise.all([ + this.#fetchEndpoint(url, "GET", httpTimeout), + this.#fetchEndpoint(urlEnroll, "GET", httpTimeout) + ]); + + const next = [courses.next, enrolledCourses.next].filter((n) => n !== null); + const previous = [courses.previous, enrolledCourses.previous].filter((p) => p !== null); + + return { + count: courses.count + enrolledCourses.count, + next: next.length > 0 ? next : null, + previous: previous.length > 0 ? previous : null, + results: [...courses.results, ...enrolledCourses.results] + } + } return await this.#fetchEndpoint(url, "GET", httpTimeout); } diff --git a/app/helpers/ui.js b/app/helpers/ui.js index d710a2e3..0f589953 100644 --- a/app/helpers/ui.js +++ b/app/helpers/ui.js @@ -72,6 +72,8 @@ const ui = { get actionCardTemplate() { return `
+ +
diff --git a/app/helpers/utils.js b/app/helpers/utils.js index bcacdfb8..9786669a 100644 --- a/app/helpers/utils.js +++ b/app/helpers/utils.js @@ -135,9 +135,11 @@ const utils = { // human-readable page numbers usually start with 1, so we reduce 1 in the first argument return array.slice((page_number - 1) * page_size, page_number * page_size); }, + sleep: (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }, + newError(name, message = "") { const error = new Error(); error.name = name; diff --git a/package-lock.json b/package-lock.json index 7a613156..17001402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "udeler", - "version": "1.13.3", + "version": "1.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "udeler", - "version": "1.13.3", + "version": "1.14.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 8775856e..a98be5c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "udeler", "productName": "Udeler", - "version": "1.13.4", + "version": "1.14.0", "description": "A cross platform (Windows, Mac, Linux) desktop application for downloading Udemy Courses.", "main": "main.js", "type": "commonjs",