您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
GaTech Course Browser parsed from registration.banner.gatech.edu
// ==UserScript== // @name GT Course Browser // @author jerryc05 // @namespace https://github.com/jerryc05 // @supportURL https://github.com/jerryc05/GT-Course-Browser // @version 0.13 // @description GaTech Course Browser parsed from registration.banner.gatech.edu // @match https://registration.banner.gatech.edu/BannerExtensibility/customPage/page/HOMEPAGE_Registration // @icon https://www.google.com/s2/favicons?sz=64&domain=gatech.edu // @grant none // ==/UserScript== // eslint-disable-next-line require-await (async() => { 'use strict' const SQUARE_BRACKETED_NAME = '[GT Course Browser]', PAGE_SIZE = 50, UNIQ_SESS_ID = `12345${Date.now()}`, // Parsed from https://registration.banner.gatech.edu/StudentRegistrationSsb/assets/modules/searchResultsView-mf.unminified.js DOM_ID = 'gt_course_browser', MAX_SUBJECTS = 90, SYNC_TOKEN = String(document.querySelector('meta[name="synchronizerToken"]').getAttribute('content')) let subject = '' let term = '' /** @type {{code:string, description:string}[]} */ let subjects = [] /** * @param {string} s */ function unescapeHTML(s) { return new DOMParser().parseFromString(s, 'text/html').documentElement.innerText } /** * @param {HTMLDivElement} div * @param {string} campus * @param {'open'|'close'|'all'} filterOpen */ async function doSearch(div, campus, filterOpen) { async function f(offset = 0) { const x = await fetch('https://registration.banner.gatech.edu/' + 'StudentRegistrationSsb/ssb/searchResults/searchResults' + `?txt_subject=${subject}` + `&txt_campus=${campus}` + `&txt_term=${term}` + '&startDatepicker=&endDatepicker=' + `&uniqueSessionId=${UNIQ_SESS_ID}` + `&pageOffset=${offset}` + `&pageMaxSize=${PAGE_SIZE}` + '&sortColumn=subjectDescription&sortDirection=asc', { headers: { 'X-Synchronizer-Token': SYNC_TOKEN } }) return x.json() } // Must do this POST request to authorize your cookies await fetch('https://registration.banner.gatech.edu/StudentRegistrationSsb/ssb/term/search?mode=search', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' // Must include this header }, body: `term=${term}&studyPath=&studyPathText=&` + `startDatepicker=&endDatepicker=&uniqueSessionId=${UNIQ_SESS_ID}` }) let data = [] const js = await f() // console.log(`${SQUARE_BRACKETED_NAME} js:${JSON.stringify(js)}`) if (js.data === null) { console.error(`${SQUARE_BRACKETED_NAME} [searchResults] returned null [data]! Something is not working!`) return } data = data.concat(js.data) console.log(`${SQUARE_BRACKETED_NAME} got ${js.data.length}, total ${data.length} courses`) const reqs = [] for (let i = PAGE_SIZE; i < js.totalCount; i += PAGE_SIZE) reqs.push(f(i)) const jss = await Promise.all(reqs) for (const js2 of jss) { // console.log(`${SQUARE_BRACKETED_NAME} js2:${JSON.stringify(js2)}`) data = data.concat(js2.data) console.log(`${SQUARE_BRACKETED_NAME} got ${js2.data.length}, total ${data.length} courses`) } console.log(`${SQUARE_BRACKETED_NAME} courses:`) console.dir(data) const pre = document.createElement('pre') for (const c of data) { if ((filterOpen === 'open' && !c.openSection) || (filterOpen === 'close' && c.openSection)) continue pre.textContent += `${c.courseReferenceNumber} |` + ` ${c.subjectCourse.padEnd(7)} - ${c.sequenceNumber.padEnd(3)} |` + ` ${c.openSection ? 'OPEN ' : 'CLOSE'} |` + ` ${c.creditHours === null ? `${c.creditHourLow}+` : String(c.creditHours).padStart(2)} cr |` + ` ${unescapeHTML(c.courseTitle).padEnd(25)} |` + ` Seat: ${String(c.enrollment).padEnd(3)}/${String(c.maximumEnrollment).padEnd(3)} |` + ` WL: ${String(c.waitCount).padEnd(3)}/${String(c.waitCapacity).padEnd(3)}\n` } div.append(pre) } const div = document.createElement('div') div.id = DOM_ID // @ts-ignore document.getElementById('content').append(div) function getNullOption() { const x = document.createElement('option') x.innerText = '=== Select below ===' x.selected = true return x } const btn = document.createElement('button') btn.disabled = true const subjectSelect = document.createElement('select') const termSelect = document.createElement('select') subjectSelect.append(getNullOption()) subjectSelect.onchange = x => { subject = x.target.value const isTermSelected = termSelect.value !== '' btn.disabled = subject === '' || !isTermSelected } termSelect.append(getNullOption()) termSelect.addEventListener('click', async() => { // eslint-disable-next-line max-len const terms = await (await fetch('https://registration.banner.gatech.edu/StudentRegistrationSsb/ssb/classRegistration/getTerms?searchTerm=&offset=1&max=10', { headers: { 'X-Synchronizer-Token': SYNC_TOKEN }})).json() const f = document.createDocumentFragment() for (const t of terms) { const opt = document.createElement('option') opt.value = t.code opt.innerText = t.description f.append(opt) } termSelect.append(f) }, {once: true}) termSelect.onchange = async x => { term = x.target.value const isSubjectSelected = subjectSelect.value !== '' btn.disabled = term === '' || !isSubjectSelected if (term !== '') { // "mutex" lock termSelect.disabled = true // eslint-disable-next-line max-len subjects = await (await fetch(`https://registration.banner.gatech.edu/StudentRegistrationSsb/ssb/classSearch/get_subject?searchTerm=&term=${term}&offset=1&max=${MAX_SUBJECTS}&uniqueSessionId=${UNIQ_SESS_ID}`)).json() while (subjectSelect.lastChild !== null) subjectSelect.removeChild(subjectSelect.lastChild) const f = document.createDocumentFragment() f.append(getNullOption()) for (const s of subjects) { const opt = document.createElement('option') opt.value = s.code opt.innerText = unescapeHTML(s.description) f.append(opt) } subjectSelect.append(f) termSelect.disabled = false } } const atlOption = document.createElement('option') atlOption.value = 'A' atlOption.innerText = 'Atlanta' const campusSelect = document.createElement('select') campusSelect.append(atlOption) const openOnlyOption = document.createElement('option') openOnlyOption.value = 'open' openOnlyOption.innerText = 'Open Only' const closeOnlyOption = document.createElement('option') closeOnlyOption.value = 'close' closeOnlyOption.innerText = 'Close Only' const allSectionsOption = document.createElement('option') allSectionsOption.value = 'all' allSectionsOption.innerText = 'All sections' const filterOpenSelect = document.createElement('select') filterOpenSelect.append(openOnlyOption, closeOnlyOption, allSectionsOption) btn.innerText = 'Search' btn.onclick = () => { doSearch(div, campusSelect.value, filterOpenSelect.value) btn.disabled = true } const searchBar = document.createElement('div') searchBar.style.margin = '5rem auto' searchBar.style.width = 'fit-content' searchBar.append(termSelect, subjectSelect, campusSelect, filterOpenSelect, btn) div.append(searchBar) const css = document.createElement('style') css.textContent = ` #${DOM_ID}>div:first-child{height:2rem} #${DOM_ID}>div:first-child *{height:inherit} #${DOM_ID} button:not([disabled]){background-color:#4CAF50;} ` document.head.append(css) })()