Created
August 8, 2018 19:24
-
-
Save cuylerstuwe/eea76f5f6c9b1311c9776180780d506e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Udemy "List All Courses" And Course Planning Checkmarks | |
| // @namespace salembeats | |
| // @version 1.31 | |
| // @description . | |
| // @author Cuyler Stuwe (salembeats) | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @require file:///C:\private-keys-for-public-tampermonkey-scripts\udemy-list-all-courses.js | |
| // @include https://www.udemy.com/home/my-courses/learning/* | |
| // ==/UserScript== | |
| const globals = { | |
| url: `https://www.udemy.com/api-2.0/users/me/subscribed-courses?fields%5Bcourse%5D=@min,visible_instructors,image_240x135,image_480x270,favorite_time,archive_time,completion_ratio,last_accessed_time,enrollment_time,is_practice_test_course,features,num_collections,published_title&fields%5Buser%5D=@min,job_title&ordering=-access_time,-enrolled&page=1&page_size=100`, | |
| bearerToken: udemyPrivate.bearerToken, | |
| courseStatuses: JSON.parse(GM_getValue("courseStatuses") || "{}") | |
| }; | |
| function storeCourseStatuses() { | |
| GM_setValue("courseStatuses", JSON.stringify(globals.courseStatuses)); | |
| } | |
| function upsertCourseRecord(course) { | |
| const existingCourseRecord = globals.courseStatuses[course.title] || {}; | |
| globals.courseStatuses[course.title] = { | |
| ...existingCourseRecord, | |
| title: course.title, | |
| author: course.visible_instructors[0].display_name, | |
| completion: course.completion_ratio | |
| }; | |
| } | |
| function upsertCourses(courses) { | |
| for(const course of courses) { | |
| upsertCourseRecord(course); | |
| } | |
| } | |
| function isScheduled(course) { | |
| return !!globals.courseStatuses[course.title].isScheduled; | |
| } | |
| function isFinished(course) { | |
| return !!globals.courseStatuses[course.title].isFinished || course.completion_ratio === 100; | |
| } | |
| function isPlanned(course) { | |
| return !!globals.courseStatuses[course.title].isPlanned; | |
| } | |
| function courseTable(courses) { | |
| return `<table> | |
| <tr class='tampermonkey-course-table-header'> | |
| <td>Title</td> | |
| <td>Author</td> | |
| <td>% Complete</td> | |
| <td>Finished?</td> | |
| <td>Scheduled?</td> | |
| <td>Planned?</td> | |
| </tr> | |
| ${ | |
| courses.map(course => { | |
| const courseHTML = ` | |
| <tr> | |
| <td>${course.title}</td> | |
| <td>${course.visible_instructors[0].display_name}</td> | |
| <td>${course.completion_ratio}</td> | |
| <td><input type="checkbox" class="is-finished" ${isFinished(course) ? "checked" : ""}></td> | |
| <td><input type="checkbox" class="is-scheduled" ${isScheduled(course) ? "checked" : ""}></td> | |
| <td><input type="checkbox" class="is-planned" ${isPlanned(course) ? "checked" : ""}></td> | |
| </tr> | |
| `; | |
| return courseHTML; | |
| }).join("") | |
| } | |
| </table> | |
| <style> | |
| .tampermonkey-course-table-header { | |
| font-weight: 700; | |
| } | |
| tr:nth-child(even) { | |
| background: #DDDDDD; | |
| } | |
| </style>`; | |
| } | |
| function scrapeCourseTitleFromParentRow(checkbox) { | |
| const checkboxParentRow = checkbox.parentElement.parentElement; | |
| return checkboxParentRow.querySelector("td").innerText.trim(); | |
| } | |
| function markCourseByCheckbox(checkbox) { | |
| const courseTitle = scrapeCourseTitleFromParentRow(checkbox); | |
| const classToPropertyMap = { | |
| "is-finished": "isFinished", | |
| "is-scheduled": "isScheduled", | |
| "is-planned": "isPlanned" | |
| }; | |
| const propertyToUpdate = classToPropertyMap[checkbox.className]; | |
| globals.courseStatuses[courseTitle][propertyToUpdate] = checkbox.checked; | |
| } | |
| async function main() { | |
| const fetchResults = await fetch(globals.url, { | |
| method: "GET", | |
| headers: { | |
| "accept": "application/json", | |
| "authorization": `Bearer ${globals.bearerToken}` | |
| } | |
| }); | |
| const apiCourseResults = (await fetchResults.json()).results; | |
| const sortedApiCourseResults = apiCourseResults.sort((a, b) => { | |
| return ( | |
| ({ | |
| "true": 1, | |
| "false": -1 | |
| })[(a.completion_ratio > b.completion_ratio).toString()] | |
| ); | |
| }); | |
| upsertCourses(sortedApiCourseResults); | |
| storeCourseStatuses(); | |
| document.querySelector(".next.btn.btn-default").insertAdjacentHTML("afterend", "<div><button id='createCoursesPopup' class='btn btn-default'><span class='udi'>List All Courses</span></button></div>"); | |
| const createCoursesPopup = document.getElementById('createCoursesPopup'); | |
| createCoursesPopup.addEventListener("click", e => { | |
| const newWindow = unsafeWindow.open(`about:blank`, null, "left=0, top=0, width=800, height=500, scrollbars=1"); | |
| const sortedTable = courseTable(sortedApiCourseResults); | |
| newWindow.document.write(sortedTable); | |
| newWindow.document.body.addEventListener("click", e => { | |
| if(e.target.tagName === "INPUT" && | |
| e.target.getAttribute("type") === "checkbox") { | |
| markCourseByCheckbox(e.target); | |
| storeCourseStatuses(); | |
| } | |
| }); | |
| }); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment