// ==UserScript== // @name Udemy - Improved Course Library // @namespace https://github.com/TadWohlrapp/UserScripts // @description Shows rating, number of reviews and number of students enrolled for all courses in your library // @icon https://github.com/TadWohlrapp/UserScripts/raw/master/udemy-improved-course-library/icon.png // @icon64 https://github.com/TadWohlrapp/UserScripts/raw/master/udemy-improved-course-library/icon64.png // @author Tad Wohlrapp // @homepageURL https://github.com/TadWohlrapp/UserScripts/tree/master/udemy-improved-course-library // @version 0.6.0 // @supportURL https://github.com/TadWohlrapp/UserScripts/issues // @match https://www.udemy.com/home/my-courses/* // @compatible chrome Tested with Tampermonkey v4.9 and Violentmonkey v2.12.7 // @compatible firefox Tested with Greasemonkey v4.9 // @copyright 2020, Tad Wohlrapp (https://github.com/TadWohlrapp/UserScripts) // @license MIT // @downloadURL none // ==/UserScript== // ==OpenUserJS== // @author Taddiboy // ==/OpenUserJS== (function () { const i18n = loadTranslations(); const lang = getLang(document.documentElement.lang); function fetchCourses() { listenForArchiveToggle(); const courseContainers = document.querySelectorAll('[data-purpose="enrolled-course-card"]:not(.details-done)'); if (courseContainers.length == 0) { return; } [...courseContainers].forEach((courseContainer) => { const courseId = courseContainer.querySelector('.card--learning__image').href.replace('https://www.udemy.com/course-dashboard-redirect/?course_id=', ''); const courseCustomDiv = document.createElement('div'); courseCustomDiv.classList.add('card__custom'); const courseLinkDiv = document.createElement('div'); courseLinkDiv.innerHTML = ` ${i18n[lang].overview} `; courseLinkDiv.classList.add('card__custom-row'); courseCustomDiv.appendChild(courseLinkDiv); const courseStatsDiv = document.createElement('div'); courseStatsDiv.classList.add('card__custom-row', 'card__course-stats-ct'); courseCustomDiv.appendChild(courseStatsDiv); const ratingStripDiv = document.createElement('div'); ratingStripDiv.style.height = '4px'; courseCustomDiv.appendChild(ratingStripDiv); courseContainer.appendChild(courseCustomDiv); courseContainer.classList.add('details-done'); // If course page has draft status, do not even attempt to fetch its data if (courseContainer.querySelector('.card--learning__details').href.includes('/draft/')) { courseContainer.querySelector('.card__course-link').style.textDecoration = "line-through"; courseStatsDiv.innerHTML += '
' + i18n[lang].notavailable + '
'; ratingStripDiv.style.backgroundColor = '#faebeb'; 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'; fetch(fetchUrl) .then(response => { if (response.ok) { return response.json(); } else if (response.status === 403) { throw new Error('This course is no longer accepting enrollments.'); } else { throw new Error(response.status); } }) .then(json => { if (typeof json === 'undefined') { return; } const rating = json.rating.toFixed(1); const reviews = json.num_reviews; const enrolled = json.num_subscribers; const runtime = json.content_length_video; const updateDate = json.last_update_date const ratingPercentage = Math.round((rating / 5) * 100); const ratingStars = ` ${buildStars()} ${buildStars()} `; courseStatsDiv.innerHTML = `
${ratingStars}${setDecimal(rating, lang)}(${setSeparator(reviews, lang)})
${setSeparator(enrolled, lang)} ${i18n[lang].enrolled}
${i18n[lang].updated}${updateDate}
`; const getColor = v => `hsl(${((1 - v) * 120)},100%,50%)`; const colorValue = Math.min(Math.max((5 - rating) / 2, 0), 1); ratingStripDiv.style.backgroundColor = getColor(colorValue); // Add course runtime, YouTube style const thumbnailDiv = courseContainer.querySelector('.card__image'); const runtimeSpan = document.createElement('span'); runtimeSpan.classList.add('card__course-runtime'); runtimeSpan.innerHTML = parseRuntime(runtime); thumbnailDiv.appendChild(runtimeSpan); }) .catch(error => { courseStatsDiv.innerHTML += '
' + i18n[lang].notavailable + '
'; ratingStripDiv.style.backgroundColor = '#faebeb'; console.info('Could not fetch stats for course ' + courseId + '.', error); }); }); } fetchCourses(); const mutationObserver = new MutationObserver(fetchCourses); const targetNode = document.querySelector('div[data-module-id="my-courses-v3"]'); const observerConfig = { childList: true, subtree: true }; mutationObserver.observe(targetNode, observerConfig); function listenForArchiveToggle() { document.querySelectorAll('[data-purpose="toggle-archived"]').forEach(item => { item.addEventListener('click', event => { mutationObserver.disconnect(); const doneContainers = document.querySelectorAll('.details-done'); [...doneContainers].forEach((doneContainer) => { doneContainer.classList.remove('details-done'); doneContainer.removeChild(doneContainer.querySelector('.card__custom')); }); mutationObserver.observe(targetNode, observerConfig); }); }); } function buildStars() { let stars = ''; for (i = 0; i < 5; i++) { stars += ''; } return stars; } function setSeparator(int, lang) { return int.toString().replace(/\B(?=(\d{3})+(?!\d))/g, i18n[lang].separator); } function setDecimal(rating, lang) { return rating.toString().replace('.', i18n[lang].decimal); } function getLang(lang) { return i18n.hasOwnProperty(lang) ? lang : 'en-us'; } function addGlobalStyle(css) { const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); } function parseRuntime(seconds) { const h = Math.floor(seconds / 60 / 60); const m = Math.floor(seconds / 60) - (h * 60); const s = seconds % 60; const timeString = h.toString().padStart(2, '0') + ':' + m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0'); return timeString.replace(/^[0:]+/, ''); } function loadTranslations() { return { 'en-us': { 'overview': 'Course overview', 'enrolled': 'students', 'updated': 'Last updated ', 'notavailable': 'Course stats not available', 'separator': ',', 'decimal': '.' }, 'de-de': { 'overview': 'Kursübersicht', 'enrolled': 'Teilnehmer', 'updated': 'Zuletzt aktualisiert ', 'notavailable': 'Kursstatistiken nicht verfügbar', 'separator': '.', 'decimal': ',' }, 'es-es': { 'overview': 'Descripción del curso', 'enrolled': 'estudiantes', 'updated': 'Última actualización ', 'notavailable': 'Las estadísticas del curso no están disponibles', 'separator': '.', 'decimal': ',' }, 'fr-fr': { 'overview': 'Aperçu du cours', 'enrolled': 'participants', 'updated': 'Dernière mise à jour : ', 'notavailable': 'Statistiques sur les cours non disponibles', 'separator': ' ', 'decimal': ',' }, 'it-it': { 'overview': 'Panoramica del corso', 'enrolled': 'studenti', 'updated': 'Ultimo aggiornamento ', 'notavailable': 'Statistiche del corso non disponibili', 'separator': '.', 'decimal': ',' } }; } addGlobalStyle(` span[class^="leave-rating--helper-text"] { font-size: 10px; white-space: nowrap; } .card__course-runtime { position: absolute; bottom: 0; right: 0; background-color: rgba(0,0,0,0.75); color: #ffffff; display: inline-block; font-size: 10px; font-weight: 700; margin: 4px; padding: 2px 4px; border-radius: 2px; } .card__custom-row { color: #29303b; font-size: 13px; padding: 0 15px; } .card__course-link { color: #007791 !important; display: inline-block !important; } .card__course-link:hover { color: #003845 !important; } .card__course-stats-ct { height: 64px; display: flex; align-items: center; } .card__course-stats { font-size: 12px; font-weight: 400; color: #686f7a; line-height: 1.5; } .card__stars { display: inline-block; width: 7rem; height: 1.6rem; vertical-align: text-bottom; } .card__star--bordered { stroke: #eb8a2f; } .card__star--filled { fill: #eb8a2f; } .card__rating-text { font-weight: 700; color: #505763; margin-left: 2px; margin-right: 6px; font-size: 13px; } .card__nodata { color: #73726c; font-weight: 500; } `); })();