// ==UserScript==
// @name Udemy - Improved Course Library
// @name:de Udemy - Verbesserte Kursbibliothek
// @name:fr Udemy - Bibliothèque de cours améliorée
// @name:es Udemy - Biblioteca de cursos mejorada
// @name:it Udemy - Libreria dei corsi migliorata
// @name:ja Udemy - コースライブラリの改良
// @description Adds current ratings and other detailed data to all courses in your Udemy library.
// @description:de Fügt aktuelle Bewertungen und andere detaillierte Informationen zu allen Kursen in deiner Udemy-Bibliothek hinzu.
// @description:fr Ajoute les évaluations actuelles et d'autres données détaillées à tous les cours de ta bibliothèque Udemy.
// @description:es Añade valoraciones actuales y otros datos detallados a todos los cursos de tu biblioteca Udemy.
// @description:it Aggiunge valutazioni attuali e altri dati dettagliati a tutti i corsi nella tua libreria Udemy.
// @description:ja Udemyのライブラリにある全てのコースに現在の評価やその他の詳細情報を追加します。
// @namespace https://github.com/tadwohlrapp
// @author Tad Wohlrapp
// @version 1.1.2
// @license MIT
// @homepageURL https://github.com/tadwohlrapp/udemy-improved-course-library
// @supportURL https://github.com/tadwohlrapp/udemy-improved-course-library/issues
// @icon https://github.com/tadwohlrapp/udemy-improved-course-library/raw/main/src/icon48.png
// @icon64 https://github.com/tadwohlrapp/udemy-improved-course-library/raw/main/src/icon64.png
// @run-at document-end
// @match https://www.udemy.com/home/my-courses/*
// @compatible firefox Tested on Firefox v117.0 with Violentmonkey v2.15.0 and Tampermonkey v4.19.0
// @compatible chrome Tested on Chrome v115.0 with Violentmonkey v2.15.0 and Tampermonkey v4.19.0
// @downloadURL https://update.greasyfork.icu/scripts/402838/Udemy%20-%20Improved%20Course%20Library.user.js
// @updateURL https://update.greasyfork.icu/scripts/402838/Udemy%20-%20Improved%20Course%20Library.meta.js
// ==/UserScript==
fetchCourses();
const mutationObserver = new MutationObserver(fetchCourses);
const observerConfig = {
childList: true,
subtree: true
};
mutationObserver.observe(document, observerConfig);
const i18n = loadTranslations();
const lang = getLang(document.documentElement.lang);
function fetchCourses() {
listenForArchiveToggle();
const courseContainers = document.querySelectorAll('[class^="enrolled-course-card--container--"]:not(.details-done)');
if (courseContainers.length == 0) return;
[...courseContainers].forEach((courseContainer) => {
const isPartialRefresh = courseContainer.classList.contains('partial-refresh');
const courseId = courseContainer.querySelector('h3[data-purpose="course-title-url"]>a').href.replace('https://www.udemy.com/course-dashboard-redirect/?course_id=', '');
const courseCustomDiv = document.createElement('div');
courseCustomDiv.classList.add('improved-course-card--additional-details', 'js-removepartial');
const innerContainer = courseContainer.querySelector('div[data-purpose="container"]')
innerContainer.appendChild(courseCustomDiv);
courseContainer.classList.add('details-done');
courseContainer.classList.add('improved-course-card--container');
courseContainer.classList.remove('partial-refresh');
// Add Link to course overview to options dropdown
const courseLinkLi = document.createElement('li');
courseLinkLi.innerHTML = `
`;
courseLinkLi.classList.add('js-removepartial');
const allDropdowns = courseContainer.parentElement.querySelectorAll('.udlite-block-list');
if (allDropdowns[1]) {
allDropdowns[1].appendChild(courseLinkLi);
}
// Find existing elements in DOM
const imageWrapper = courseContainer.querySelector('div[class^="course-card-module--image-container--"]');
imageWrapper.classList.add('improved-course-card--image-container');
const mainContent = courseContainer.querySelector('div[class^="course-card-module--main-content--"]');
mainContent.classList.add('improved-course-card--main-content');
const courseTitle = courseContainer.querySelector('h3[data-purpose="course-title-url"]');
courseTitle.classList.add('improved-course-card--course-title');
const priceTextContainer = courseContainer.querySelector('div[class^="course-card-module--price-text-container--"]');
if (priceTextContainer) priceTextContainer.parentNode.removeChild(priceTextContainer);
const courseBadges = courseContainer.querySelector('div[class^="course-card-module--badges-container--"]');
if (courseBadges) courseBadges.parentNode.removeChild(courseBadges);
const progressBar = courseContainer.querySelector('div[class^="enrolled-course-card--meter--"]');
progressBar?.classList.add('improved-course-card--meter');
const progressAndRating = courseContainer.querySelector('div[class*="enrolled-course-card--progress-and-rating--"]');
progressAndRating?.classList.add('improved-course-card--progress-and-rating');
const progressText = progressAndRating.firstChild;
const progressMade = /%/.test(progressText.textContent);
if (!progressMade) progressAndRating.parentNode.removeChild(progressAndRating);
// If progress made
if (progressMade) {
// Add progress bar below thumbnail
const progressBarSpan = document.createElement('span');
progressBarSpan.classList.add('impr__progress-bar', 'js-removepartial');
progressBarSpan.innerHTML = progressBar.innerHTML;
imageWrapper.appendChild(progressBarSpan);
// Add progress percentage to thumbnail bottom right
const progressTextSpan = document.createElement('span');
progressTextSpan.classList.add('card__thumb-overlay', 'card__course-runtime', 'hover-show', 'js-removepartial');
progressTextSpan.textContent = progressText.textContent;
imageWrapper.appendChild(progressTextSpan);
// Remove existing progress percentage
progressText.parentNode.removeChild(progressText);
}
// Remove existing progress bar
if (!isPartialRefresh) {
progressBar.parentNode.removeChild(progressBar);
}
// If course page has draft status, do not even to fetch its data via API
if (courseContainer.querySelector('[data-purpose="course-title-url"] a').href.includes('/draft/')) {
courseContainer.querySelector('.card__course-link').style.textDecoration = "line-through";
courseCustomDiv.classList.add('card__nodata');
courseCustomDiv.innerHTML += i18n[lang].notavailable;
// We're done with this course
return;
}
const fetchUrl = 'https://www.udemy.com/api-2.0/courses/' + courseId + '?fields[course]=rating,num_reviews,num_subscribers,content_length_video,last_update_date,created,locale,has_closed_caption,caption_languages,num_published_lectures';
fetch(fetchUrl)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.status);
}
})
.then(json => {
if (typeof json === 'undefined') { return; }
// Get everything from JSON and put it in variables
const rating = json.rating.toFixed(1);
const reviews = json.num_reviews;
const enrolled = json.num_subscribers;
const runtime = json.content_length_video;
const date = json.last_update_date ?? json.created.slice(0, 10); // 'created' comes as full iso string with time
const locale = json.locale.title;
const localeCode = json.locale.locale;
const hasCaptions = json.has_closed_caption;
const captionsLangs = json.caption_languages;
// Format "Last updated / Created" Dates
const updateDateShort = date ? date.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2\/$1') : '';
const updateDateLong = date ? new Date(date).toLocaleDateString(lang, { year: 'numeric', month: 'long', day: 'numeric' }) : '';
// Small helper for rating strip color
const getColor = v => `hsl(${(Math.round((1 - v) * 120))},100%,45%)`;
const colorValue = r => Math.min(Math.max((5 - r) / 2, 0), 1);
// If captions are available, create the tag for it. We'll add it in template string later
let captionsTag = '';
if (hasCaptions) {
const captionsString = captionsLangs.join('
');
captionsTag = `
`;
}
// Returns true or false depending if stars are visible
const reviewButton = courseContainer.querySelector('[data-purpose="review-button"]');
// Now let's handle own ratings
// Set up empty html
let myRatingHtml = '';
let ratingButton;
let ratingOwn = 0;
// If ratings stars ARE visible, proceed to build own rating stars
if (reviewButton != null) {
// Find the rating-button, and remove its css class
ratingButton = reviewButton;
// If I have voted, count the stars and tell me how I voted
ratingOwn = getRatingFromSvg(ratingButton.querySelector('svg')); // between 0 and 5
// Remove the old stars from ratingButton
ratingButton.removeChild(ratingButton.querySelector('span'));
// Build the html
myRatingHtml = `
Rating: ${ratingOwn} out of 5
${buildSvgStars(courseId.toString() + '-own', ratingOwn)}
${setDecimal(ratingOwn, lang)}()