您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show time/faculty, enhance search, remove courses by prefix, Update data to rds2.northsouth.app
当前为
// ==UserScript== // @name RDS3 Assistant - Faculty Initial + Time // @namespace https://northsouth.app/ // @version 2.1.1 // @description Show time/faculty, enhance search, remove courses by prefix, Update data to rds2.northsouth.app // @author Nihal, Rayed & Walid - NSU CTRL ALT DELETE (NSU - CSE231) // @match https://rds3.northsouth.edu/* // @match https://rds3.northsouth.edu/students/* // @match https://rds3.northsouth.edu/students/advising* // @match https://rds3.northsouth.edu/index.php/students/advising // @icon https://rds3.northsouth.edu/assets/img/favicon.png // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @license MIT // @connect rds2csv.northsouth.app // ==/UserScript== (function () { 'use strict'; /******** PART 1: AUTO PUSH CSV FUNCTIONALITY ********/ /******** CONFIG ********/ const API_BASE = GM_getValue('API_BASE', 'https://rds2csv.northsouth.app/data'); // -> POST {API_BASE}/csv const RUN_KEY = GM_getValue('RUN_KEY', ''); // set only if your server checks X-Run-Key const FILENAME = 'course_data.csv'; const COURSE_CELL_SELECTOR = 'td[onclick*="addNewCourses"]'; // Observer & fallback timing const waitTimeoutMs = 5000; // wait up to 5s for cells to appear const pollFallbackMs = 800; // also try once after 800ms even if no cells (site sometimes builds time map first) const alreadyRanKey = `rds3_auto_push_once_${Date.now()}`; // (No per-URL run-lock anymore; we want this to try each load) console.log('[RDS3 AutoPush] Script loaded'); /******** ROOM MAP (as provided) ********/ const roomArr = []; roomArr[0]="TBA";roomArr[1]="NAC601";roomArr[2]="NAC602";roomArr[3]="NAC603";roomArr[4]="NAC604";roomArr[5]="NAC605"; roomArr[6]="NAC501";roomArr[7]="NAC502";roomArr[8]="NAC503";roomArr[9]="NAC504";roomArr[10]="NAC505";roomArr[11]="NAC506"; roomArr[12]="NAC507";roomArr[13]="NAC508";roomArr[14]="NAC509";roomArr[15]="NAC510";roomArr[16]="NAC409";roomArr[17]="NAC410"; roomArr[18]="NAC411";roomArr[19]="NAC414";roomArr[20]="NAC309";roomArr[21]="NAC310";roomArr[22]="NAC206";roomArr[23]="NAC207"; roomArr[24]="NAC208";roomArr[25]="NAC209";roomArr[26]="NAC210";roomArr[27]="SAC401";roomArr[28]="SAC402";roomArr[29]="SAC403"; roomArr[30]="SAC404";roomArr[31]="SAC405";roomArr[32]="SAC406";roomArr[33]="SAC407";roomArr[34]="NAC401";roomArr[35]="NAC402"; roomArr[36]="NAC403";roomArr[37]="NAC404";roomArr[38]="NAC405";roomArr[39]="NAC406";roomArr[40]="NAC407";roomArr[41]="NAC408"; roomArr[42]="NAC412";roomArr[43]="NAC413";roomArr[44]="SAC304";roomArr[45]="SAC305";roomArr[46]="SAC306";roomArr[47]="SAC307"; roomArr[48]="SAC308";roomArr[49]="SAC309";roomArr[50]="SAC310";roomArr[51]="SAC311";roomArr[52]="SAC312";roomArr[53]="SAC313"; roomArr[54]="SAC201";roomArr[55]="SAC207";roomArr[56]="SAC208";roomArr[57]="NAC301";roomArr[58]="NAC302";roomArr[59]="NAC303"; roomArr[60]="NAC304";roomArr[61]="NAC305";roomArr[62]="NAC306";roomArr[63]="NAC307";roomArr[64]="NAC308";roomArr[65]="SAC203"; roomArr[66]="SAC209";roomArr[67]="SAC210";roomArr[68]="SAC211";roomArr[69]="NAC201";roomArr[70]="NAC202";roomArr[71]="NAC203"; roomArr[72]="NAC204";roomArr[73]="NAC205";roomArr[81]="LAB1";roomArr[82]="LAB2";roomArr[84]="LAB4";roomArr[85]="OAT1001"; roomArr[86]="OAT1002";roomArr[87]="OAT1003";roomArr[89]="OAT901";roomArr[90]="OAT902";roomArr[91]="OAT903";roomArr[93]="OAT803"; roomArr[94]="LIB901";roomArr[95]="LIB902";roomArr[96]="LIB903";roomArr[97]="LIB904";roomArr[98]="LIB905";roomArr[100]="LIB907"; roomArr[101]="LIB908";roomArr[103]="SAC802";roomArr[121]="NAC514";roomArr[122]="NAC415";roomArr[123]="NAC311";roomArr[124]="SAC202"; roomArr[128]="NAC213";roomArr[129]="NAC214";roomArr[130]="NAC215";roomArr[131]="NAC216";roomArr[132]="SAC204";roomArr[133]="NAC517"; roomArr[135]="OAT601";roomArr[136]="OAT602";roomArr[144]="NAC990";roomArr[145]="NAC991";roomArr[146]="NAC992";roomArr[147]="NAC993"; roomArr[148]="LIB906";roomArr[150]="SAC501";roomArr[151]="SAC502";roomArr[152]="SAC504";roomArr[153]="SAC508";roomArr[154]="SAC206"; roomArr[155]="SAC205";roomArr[156]="NAC620";roomArr[158]="SAC510";roomArr[159]="SAC512";roomArr[160]="SAC514";roomArr[162]="SAC511"; roomArr[163]="SAC513";roomArr[164]="LIB602";roomArr[165]="LIB603";roomArr[166]="LIB601";roomArr[174]="NAC621";roomArr[175]="NAC619"; roomArr[176]="NAC619A";roomArr[177]="NAC211";roomArr[178]="LIB604";roomArr[179]="LIB605";roomArr[180]="LIB606";roomArr[181]="LIB607"; roomArr[182]="LIB608";roomArr[183]="SAC506";roomArr[184]="SAC507";roomArr[185]="SAC1018";roomArr[186]="NAC511";roomArr[187]="LIB609"; roomArr[188]="LIB610";roomArr[189]="LIB611";roomArr[190]="SAC509";roomArr[192]="SAC503";roomArr[193]="NAC512";roomArr[194]="NAC513"; roomArr[195]="NAC313";roomArr[196]="NAC314";roomArr[197]="NAC315";roomArr[198]="SAC314";roomArr[199]="SAC315";roomArr[200]="SAC316"; roomArr[203]="B113";roomArr[204]="B115";roomArr[205]="B117";roomArr[206]="B118";roomArr[207]="B310A";roomArr[209]="SAC724"; roomArr[210]="SAC726";roomArr[211]="SAC409";roomArr[212]="SAC412";roomArr[213]="SAC413";roomArr[214]="SAC414";roomArr[215]="SAC415"; roomArr[221]="OAT803_V";roomArr[224]="LIB903_V";roomArr[225]="LIB906_V";roomArr[230]="LIB901_V";roomArr[232]="LIB905_V";roomArr[234]="OAT902_V"; roomArr[235]="SAC411";roomArr[236]="SAC411_V";roomArr[244]="NAC617";roomArr[245]="NAC618";roomArr[246]="NAC1077";roomArr[247]="NAC1078"; roomArr[248]="NAC1079";roomArr[249]="NAC1080";roomArr[252]="SAC726_V";roomArr[254]="SAC412_V";roomArr[255]="SAC414_V";roomArr[256]="SAC413_V"; roomArr[260]="SAC409_V";roomArr[266]="NAC505";roomArr[275]="SAC726_V1";roomArr[280]="LIB910";roomArr[281]="LIB913";roomArr[284]="LIB913_V"; roomArr[286]="LIB1002";roomArr[287]="LIB1002_V";roomArr[288]="SAC801A";roomArr[290]="OAT1002_V";roomArr[292]="LIB902_V";roomArr[293]="LIB904_V"; roomArr[294]="";roomArr[295]="SAC415_v1";roomArr[305]="OAT803_V1";roomArr[306]="TBA_v2";roomArr[307]="SAC801";roomArr[308]="OAT903_V1"; roomArr[309]="LIB904_V1";roomArr[320]="LIB901_v1";roomArr[321]="SAC415B";roomArr[326]="SAC415B_V";roomArr[332]="Upper Plaza";roomArr[341]="NAC515"; roomArr[343]="OAT803_V2";roomArr[345]="TV LAB";roomArr[351]="NAC201-v1";roomArr[352]="LIB902_V1";roomArr[358]="TV STUDIO";roomArr[360]="NAC512A"; roomArr[361]="NTR304";roomArr[362]="NAC514";roomArr[363]="NTR301"; /******** TIME RESOLUTION HELPERS ********/ function getGlobalTimeMap() { const names = ['timeArray','TimeArray','TIME_ARRAY','timeArr','times','courseTimeArray']; for (const n of names) { try { if (n in unsafeWindow) { const v = unsafeWindow[n]; if (v && (Array.isArray(v)||typeof v==='object')) return v; } } catch {} try { if (n in window) { const v = window[n]; if (v && (Array.isArray(v)||typeof v==='object')) return v; } } catch {} } return null; } function buildTimeMapFromScripts() { const map = {}; const scripts = Array.from(document.scripts); const assignRe = /(?:^|[\s;])([A-Za-z_$][\w$]*)\s*\[\s*(\d+)\s*\]\s*=\s*["']([^"']+)["']/g; for (const s of scripts) { const txt = s.textContent || ''; let m; while ((m = assignRe.exec(txt)) !== null) { const idx = parseInt(m[2], 10); const val = m[3].trim(); if (!Number.isNaN(idx) && val) map[idx] = val; } } // Also try literal array/object assignments if (Object.keys(map).length === 0) { const all = scripts.map(s => s.textContent || '').join('\n'); const literalRe = /([A-Za-z_$][\w$]*)\s*=\s*(\{[\s\S]*?\}|\[[\s\S]*?\]);/g; let m; while ((m = literalRe.exec(all)) !== null) { try { const obj = Function(`"use strict";return (${m[2]});`)(); if (obj && typeof obj === 'object') { if (Array.isArray(obj)) obj.forEach((v,i)=>{ if (typeof v==='string') map[i]=v; }); else for (const [k,v] of Object.entries(obj)) { const idx = parseInt(k,10); if (!Number.isNaN(idx) && typeof v==='string') map[idx]=v; } } } catch {} } } return map; } function resolveCourseTime(timeId) { const id = Number.parseInt(timeId, 10); if (Number.isNaN(id)) return 'Unknown'; const g = getGlobalTimeMap(); if (g) { const v = Array.isArray(g) ? g[id] : g[id] || g[String(id)]; if (typeof v === 'string' && v.trim()) return v.trim(); } if (!window.__rds_time_map__) window.__rds_time_map__ = buildTimeMapFromScripts(); const m = window.__rds_time_map__; if (m && typeof m[id] === 'string' && m[id].trim()) return m[id].trim(); return 'Unknown'; } /******** CSV + UPLOAD ********/ function csvEscape(val){ if(val==null) return ''; const s=String(val); return /[",\n]/.test(s) ? `"${s.replace(/"/g,'""')}"` : s; } function convertToCSV(courses){ const ts = new Date().toISOString(); const headers = ['CourseCode','Section','Faculty','CourseTime','TotalSeat','TakenSeat','RoomId','Room']; const lines = [`# lastUpdated: ${ts}`, headers.join(',')]; for(const c of courses){ lines.push([csvEscape(c.CourseCode),csvEscape(c.Section),csvEscape(c.Faculty), csvEscape(c.CourseTime),csvEscape(c.TotalSeat),csvEscape(c.TakenSeat), csvEscape(c.RoomId),csvEscape(c.Room)].join(',')); } return lines.join('\n')+'\n'; } function makeMultipartBody(fieldName, filename, content){ const boundary = '----TMFormBoundary' + Math.random().toString(36).slice(2); const CRLF = '\r\n'; const parts = []; parts.push(`--${boundary}${CRLF}`); parts.push(`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}`); parts.push(`Content-Type: text/csv${CRLF}${CRLF}`); parts.push(content + CRLF); parts.push(`--${boundary}--${CRLF}`); return { body: parts.join(''), boundary }; } function httpPostMultipart(url, fieldName, filename, content, extraHeaders){ const { body, boundary } = makeMultipartBody(fieldName, filename, content); const headers = Object.assign({ 'Content-Type': 'multipart/form-data; boundary='+boundary }, extraHeaders||{}); return new Promise((resolve,reject)=>{ GM_xmlhttpRequest({ method:'POST', url, data: body, headers, onload: res => { console.log('[RDS3 AutoPush] POST status:', res.status); if (res.status>=200 && res.status<300) resolve(res); else reject(new Error(`HTTP ${res.status}: ${res.responseText||res.statusText}`)); }, onerror: e => reject(new Error(`Network error`)) }); }); } /******** SCRAPER ********/ function extractCourseData(){ const cells = document.querySelectorAll(COURSE_CELL_SELECTOR); console.log('[RDS3 AutoPush] Found cells:', cells.length); const out = []; const seen = new Set(); cells.forEach(cell=>{ const onclickAttr = cell.getAttribute('onclick'); if(!onclickAttr) return; let params = []; try{ const fn = new Function(` let params = []; const mockFunction = function(){ params = Array.from(arguments); }; const addNewCourses = mockFunction; ${onclickAttr}; return params; `); params = fn(); }catch(e){ console.warn('[RDS3 AutoPush] Param capture failed:', e); return; } if(!params || params.length<10) return; const courseCode = params[0]; const section = params[4]; const roomId = parseInt(params[5]); const timeId = parseInt(params[6]); const faculty = params[7]; const totalSeat = params[8]; const takenSeat = params[9]; const key = `${courseCode}-${section}-${timeId}`; if(seen.has(key)) return; seen.add(key); const courseTime = resolveCourseTime(timeId); out.push({ CourseCode: courseCode, Section: section, Faculty: faculty, CourseTime: courseTime, TotalSeat: totalSeat, TakenSeat: takenSeat, RoomId: roomId, Room: roomArr[roomId] || 'Unknown' }); }); console.log('[RDS3 AutoPush] Extracted courses:', out.length); return out; } async function runUpload(alwaysPostEvenIfEmpty = false){ try{ const courses = extractCourseData(); const csv = convertToCSV(courses); const headers = RUN_KEY ? {'X-Run-Key': RUN_KEY} : {}; if (courses.length == 0 ) { console.log('[RDS3 AutoPush] No courses; skipping upload'); return; } await httpPostMultipart(`${API_BASE}/csv`, 'file', FILENAME, csv, headers); console.log('[RDS3 AutoPush] Upload complete'); }catch(e){ console.warn('[RDS3 AutoPush] Upload failed:', e.message || e); } } /******** ORCHESTRATION ********/ function startObserver() { let ran = false; const stop = () => { try { obs.disconnect(); } catch {} }; const obs = new MutationObserver(() => { const cells = document.querySelectorAll(COURSE_CELL_SELECTOR); if (!ran && cells.length > 0) { ran = true; stop(); console.log('[RDS3 AutoPush] Cells detected by observer; uploading…'); runUpload(true); } }); obs.observe(document.documentElement || document.body, { childList: true, subtree: true }); // Fallback: try once after a short delay even if cells not found (some pages still have time map ready) setTimeout(() => { if (!ran) { console.log('[RDS3 AutoPush] Fallback attempt after', pollFallbackMs, 'ms'); runUpload(true); } }, pollFallbackMs); // Hard timeout: stop observing after waitTimeoutMs setTimeout(() => { if (!ran) { console.log('[RDS3 AutoPush] Timeout reached; stopping observer (no cells detected).'); } stop(); }, waitTimeoutMs); } /******** PART 2: FACULTY INITIAL + TIME DISPLAY ********/ // Inject custom styles const style = document.createElement('style'); style.innerHTML = ` .courselist td:nth-child(3) { width: 180px !important; white-space: nowrap; overflow: visible; color: blue; font-weight: bold; } #mainBody { text-align: center !important; width: 1200px !important; height: 900px !important; } #noticebar { width: 1190px !important; } #offeredCourses.body, #offeredCourses { width: 495px !important; height: 750px !important; } #coursesbox { width: 190% !important; height: 776px !important; } .right { width: 270px !important; } #topbar { width: 1190px !important; } #searchDiv { width: 350px !important; } .time-text { color: black !important; display: inline-block !important; vertical-align: middle !important; line-height: 1.4 !important; } #coursesbox { width: 505px !important; } #legendbox { position: relative !important; left: 130px !important; height: 115px !important; } img[src*="legend.png"] { padding: 10px !important; border: 2px solid black !important; } .courselist td { line-height: 23px !important; border-bottom: 2px solid #000 !important; font-size: 14px !important; cursor: pointer !important; font-weight: bold !important; padding: 0px !important; vertical-align: middle !important; width: 100% !important; } .abouticon.smallicon { display: none !important; } `; document.head.appendChild(style); function updateTimeText() { const icons = document.querySelectorAll('.abouticon'); icons.forEach(icon => { const onclickStr = icon.getAttribute('onclick'); if (!onclickStr) return; const paramsMatch = onclickStr.match(/displayToolTip\((.*)\)/); if (!paramsMatch) return; const paramsRaw = paramsMatch[1]; const params = paramsRaw.split(',').map(p => p.trim().replace(/^['"]|['"]$/g, '')); const timeIndex = params[3]; if (!timeIndex) return; // Use the same time resolution logic as the CSV extractor const courseTime = resolveCourseTime(timeIndex); if (courseTime === 'Unknown') return; let timeTextSpan = icon.parentElement.querySelector('.time-text'); if (!timeTextSpan) { timeTextSpan = document.createElement('span'); timeTextSpan.className = 'time-text'; icon.parentElement.appendChild(timeTextSpan); } timeTextSpan.textContent = ' ' + courseTime; }); } function updateCourseCodes() { const courseCells = document.querySelectorAll('table.courselist td:first-child'); courseCells.forEach(cell => { const onclickStr = cell.getAttribute('onclick'); if (!onclickStr) return; const params = onclickStr.match(/addNewCourses\(([^)]+)\)/); if (!params) return; const parts = params[1].split(',').map(p => p.trim().replace(/^['"]|['"]$/g, '')); const courseId = parts[0]; const section = parts[4]; const roomCode = parts[7]; if (courseId && section && roomCode) { cell.textContent = `${courseId}.${section}.${roomCode}`; } }); } function setupSearchFilter() { const searchInput = document.getElementById('searchText'); if (!searchInput) return; searchInput.addEventListener('input', function () { const filter = this.value.toLowerCase().trim(); const searchParts = filter.split(/\s+/); const rows = document.querySelectorAll('#courseList tr'); rows.forEach(row => { const courseCell = row.querySelector('td:first-child'); const timeSpan = row.querySelector('.time-text'); if (!courseCell) return; const courseText = courseCell.textContent.toLowerCase(); const timeText = timeSpan ? timeSpan.textContent.toLowerCase() : ''; const combinedText = `${courseText} ${timeText}`; const allMatch = searchParts.every(part => combinedText.includes(part)); row.style.display = allMatch ? '' : 'none'; }); }); } /******** COMBINED INITIALIZATION ********/ function initializeCombinedScript() { // Part 1: Start auto-push observer if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startObserver, { once: true }); } else { startObserver(); } // Part 2: Initialize UI enhancements updateTimeText(); updateCourseCodes(); setupSearchFilter(); } // Start the combined script initializeCombinedScript(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址