Skip to content

Commit ed4af8d

Browse files
committed
v1.14.0
1 parent 063bc3d commit ed4af8d

File tree

7 files changed

+140
-14
lines changed

7 files changed

+140
-14
lines changed

CHANGE_LOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change Log
22

3+
## Version [1.14.0](https://github.com/heliomarpm/udemy-downloader-gui/compare/v1.13.4...v1.14.0)
4+
##### Dec, 21 2024
5+
![](https://img.shields.io/github/downloads/heliomarpm/udemy-downloader-gui/v1.14.0/total)
6+
7+
### features
8+
- Single search for courses purchased individually or subscribed to via subscription
9+
- Download list m3u file
10+
311
## Version [1.13.4](https://github.com/heliomarpm/udemy-downloader-gui/compare/v1.13.3...v1.13.4)
412
##### Dec, 7 2024
513
![](https://img.shields.io/github/downloads/heliomarpm/udemy-downloader-gui/v1.13.4/total)

app/app.js

+85-9
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ $(".ui.dashboard .content").on("click", ".download-success, .course-encrypted",
117117
$(this).parents(".course").find(".download-status").show();
118118
});
119119

120+
$(".ui.dashboard .content").on("click", ".save_m3u.button", function (e) {
121+
e.stopImmediatePropagation();
122+
saveM3u($(this).parents(".course"));
123+
});
120124
$(".ui.dashboard .content").on("click", ".download.button, .download-error", function (e) {
121125
e.stopImmediatePropagation();
122126
prepareDownloading($(this).parents(".course"));
@@ -301,7 +305,7 @@ async function checkLogin(alertExpired = true) {
301305
ui.busyLogin(false);
302306
ui.showDashboard();
303307

304-
Settings.subscriber = utils.toBoolean(userContext.header.user.enableLabsInPersonalPlan);
308+
Settings.subscriber = utils.toBoolean(userContext.header.user.enableLabsInPersonalPlan) || utils.toBoolean(userContext.header.user.consumer_subscription_active);
305309
fetchCourses(Settings.subscriber).then(() => {
306310
console.log("fetchCourses done");
307311
});
@@ -553,9 +557,10 @@ function renderCourses(response, isResearch = false) {
553557
$coursesItems.append(courseElements);
554558

555559
if (response.next) {
560+
const dataUrl = Array.isArray(response.next) ? response.next : [response.next];
556561
// added loadMore Button
557562
$coursesSection.append(
558-
`<button class="ui basic blue fluid load-more button disposable" data-url=${response.next}>
563+
`<button class="ui basic blue fluid load-more button disposable" data-url=${JSON.stringify(dataUrl)}>
559564
${translate("Load More")}
560565
</button>`
561566
);
@@ -707,6 +712,7 @@ async function fetchCourseContent(courseId, courseName, courseUrl) {
707712
} else {
708713

709714
switch ( (lecture.quality || "").toLowerCase()) {
715+
case "":
710716
case "auto":
711717
case "highest":
712718
lecture.quality = streams.maxQuality;
@@ -715,10 +721,10 @@ async function fetchCourseContent(courseId, courseName, courseUrl) {
715721
lecture.quality = streams.minQuality;
716722
break;
717723
default:
718-
lecture.quality = utils.isNumber(lecture.quality) ? lecture.quality : lecture.quality.slice(0, -1);
724+
lecture.quality = utils.isNumber(lecture.quality) ? lecture.quality : lecture.quality.slice(0, -1);
719725
}
720726

721-
if (!streams.sources[lecture.quality]) {
727+
if (lecture.quality && !streams.sources[lecture.quality]) {
722728
if (utils.isNumber(lecture.quality) && streams.maxQuality != "auto") {
723729
const source = utils.getClosestValue(streams.sources, lecture.quality);
724730
lecture.quality = source?.key || streams.maxQuality;
@@ -801,18 +807,26 @@ async function fetchCourses(isSubscriber) {
801807
function loadMore(loadMoreButton) {
802808
const $button = $(loadMoreButton);
803809
const $courses = $button.prev(".courses.items");
804-
const url = $button.data("url");
810+
const url = [...$button.data("url")];
805811

806812
ui.busyLoadCourses(true);
807813
udemyService
808-
.fetchLoadMore(url)
814+
.fetchLoadMore(url[0])
809815
.then((resp) => {
810816
$courses.append(...resp.results.map((course) => createCourseElement(course, false)));
811817
if (!resp.next) {
812-
$button.remove();
818+
if (url.length > 1) {
819+
$button.data("url", [url[1]]);
820+
} else {
821+
$button.remove();
822+
}
813823
} else {
814-
$button.data("url", resp.next);
815-
}
824+
if (url.length > 1) {
825+
$button.data("url", [resp.next, url[1]]);
826+
}else {
827+
$button.data("url", [resp.next]);
828+
}
829+
}
816830
})
817831
.catch((e) => {
818832
const statusCode = (e.response?.status || 0).toString() + (e.code ? ` :${e.code}` : "");
@@ -966,6 +980,68 @@ function removeCurseDownloads(courseId) {
966980
});
967981
}
968982

983+
async function saveM3u($course) {
984+
ui.prepareDownloading($course);
985+
986+
const courseId = $course.attr("course-id");
987+
const courseName = $course.find(".coursename").text();
988+
const courseUrl = `https://${Settings.subDomain}.udemy.com${$course.attr("course-url")}`;
989+
990+
console.clear();
991+
992+
let courseData = null;
993+
try {
994+
courseData = await fetchCourseContent(courseId, courseName, courseUrl);
995+
if (!courseData) {
996+
// ui.showProgress($course, false);
997+
return;
998+
}
999+
1000+
console.log(courseData);
1001+
dialog
1002+
.showSaveDialog({
1003+
title: "Save M3U",
1004+
defaultPath: `${courseName}.m3u`,
1005+
filters: [{ name: "M3U File (*.m3u)", fileExtension: ["m3u"] }],
1006+
})
1007+
.then((result) => {
1008+
if (!result.canceled) {
1009+
let filePath = result.filePath;
1010+
if (!filePath.endsWith(".m3u")) filePath += ".m3u";
1011+
1012+
let content = "#EXTM3U";
1013+
let index = 0;
1014+
courseData.chapters.forEach((chapter) => {
1015+
chapter.lectures.forEach((lecture, lec_index) => {
1016+
index++;
1017+
content += `\n#EXTINF:-1,${lec_index+1}. ${lecture.name}\n${lecture.src}`;
1018+
1019+
if (lecture.attachments && lecture.attachments.length > 0)
1020+
lecture.attachments.forEach((attachment, attach_index) => {
1021+
content += `\n#EXTINF:-1,${lec_index+1}.${attach_index+1} ${attachment.name}\n${attachment.src}`;
1022+
})
1023+
})
1024+
});
1025+
1026+
fs.writeFile(filePath, content, (error) => {
1027+
if (error) {
1028+
appendLog("saveM3u_Error", error);
1029+
return;
1030+
}
1031+
console.log("File successfully create!");
1032+
});
1033+
}
1034+
});
1035+
1036+
} catch (error) {
1037+
handleApiError(error, "ESAVE_M3U", null, false);
1038+
ui.busyOff();
1039+
$course.find(".prepare-downloading").hide();
1040+
} finally {
1041+
ui.showProgress($course, false);
1042+
}
1043+
}
1044+
9691045
async function prepareDownloading($course, subtitle) {
9701046
ui.prepareDownloading($course);
9711047
// ui.showProgress($course, true);

app/core/services/udemy.service.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,26 @@ class UdemyService {
247247
pageSize = Math.max(pageSize, 10);
248248

249249
const param = `page=1&ordering=title&fields[user]=job_title&page_size=${pageSize}&search=${keyword}`;
250-
const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`;
250+
// const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`;
251+
const url = `${this.#URL_COURSES}?${param}`;
252+
const urlEnroll = `${this.#URL_COURSES_ENROLL}?${param}`;
253+
254+
if (isSubscriber) {
255+
const [courses, enrolledCourses] = await Promise.all([
256+
this.#fetchEndpoint(url, "GET", httpTimeout),
257+
this.#fetchEndpoint(urlEnroll, "GET", httpTimeout)
258+
]);
259+
260+
const next = [courses.next, enrolledCourses.next].filter((n) => n !== null);
261+
const previous = [courses.previous, enrolledCourses.previous].filter((p) => p !== null);
262+
263+
return {
264+
count: courses.count + enrolledCourses.count,
265+
next: next.length > 0 ? next : null,
266+
previous: previous.length > 0 ? previous : null,
267+
results: [...courses.results, ...enrolledCourses.results]
268+
}
269+
}
251270

252271
return await this.#fetchEndpoint(url, "GET", httpTimeout);
253272
}
@@ -256,7 +275,26 @@ class UdemyService {
256275
pageSize = Math.max(pageSize, 10);
257276

258277
const param = `page_size=${pageSize}&ordering=-last_accessed`;
259-
const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`;
278+
// const url = !isSubscriber ? `${this.#URL_COURSES}?${param}` : `${this.#URL_COURSES_ENROLL}?${param}`;
279+
const url = `${this.#URL_COURSES}?${param}`;
280+
const urlEnroll = `${this.#URL_COURSES_ENROLL}?${param}`;
281+
282+
if (isSubscriber) {
283+
const [courses, enrolledCourses] = await Promise.all([
284+
this.#fetchEndpoint(url, "GET", httpTimeout),
285+
this.#fetchEndpoint(urlEnroll, "GET", httpTimeout)
286+
]);
287+
288+
const next = [courses.next, enrolledCourses.next].filter((n) => n !== null);
289+
const previous = [courses.previous, enrolledCourses.previous].filter((p) => p !== null);
290+
291+
return {
292+
count: courses.count + enrolledCourses.count,
293+
next: next.length > 0 ? next : null,
294+
previous: previous.length > 0 ? previous : null,
295+
results: [...courses.results, ...enrolledCourses.results]
296+
}
297+
}
260298

261299
return await this.#fetchEndpoint(url, "GET", httpTimeout);
262300
}

app/helpers/ui.js

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const ui = {
7272
get actionCardTemplate() {
7373
return `
7474
<div class="ui tiny icon action buttons">
75+
<button class="ui basic blue save_m3u button"><i class="save outline icon"></i></button>
76+
<div style="height: 1px; width: 5px;"></div>
7577
<button class="ui basic blue download button"><i class="download icon"></i></button>
7678
<button class="ui basic red disabled pause button"><i class="pause icon"></i></button>
7779
<button class="ui basic green disabled resume button"><i class="play icon"></i></button>

app/helpers/utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,11 @@ const utils = {
135135
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
136136
return array.slice((page_number - 1) * page_size, page_number * page_size);
137137
},
138+
138139
sleep: (ms) => {
139140
return new Promise((resolve) => setTimeout(resolve, ms));
140141
},
142+
141143
newError(name, message = "") {
142144
const error = new Error();
143145
error.name = name;

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "udeler",
33
"productName": "Udeler",
4-
"version": "1.13.4",
4+
"version": "1.14.0",
55
"description": "A cross platform (Windows, Mac, Linux) desktop application for downloading Udemy Courses.",
66
"main": "main.js",
77
"type": "commonjs",

0 commit comments

Comments
 (0)