This document explains how each script exploits the MyCourseville platform and outlines the underlying website architecture for future improvement.
MyCourseville's classic platform is built on Drupal and exposes server-side actions through query-string-routed AJAX endpoints.
| Layer | Detail |
|---|---|
| Routing | URL query string: ?q=<module>/<sub-path> (Drupal's q parameter) |
| Video Player | Embedded Kaltura player (<div id="player" data-kaltura-entryid="...">) |
| Frontend | jQuery ($), standard DOM APIs |
| Session | Server-side sessions / cookies (no explicit token in JS) |
| Progress API | ?q=cvdlit/ajax/recordbar — accepts POST with video ID and watched-interval list |
| Quiz API | ?q=cvocp/ajax/submitquizanswer — accepts POST with quiz node ID, session token, and answer map |
Key DOM contracts relied on by the scripts:
cvdlit_getVideoID()— global JS function injected by the page for video ID retrieval (falls back todata-kaltura-entryidattribute).#cvocp-quiz-header[data-nid]— quiz node ID.#cvocp-quiz-session— hidden<input>holding the server-side session token for the current quiz attempt..cvocp-quiz-item.cvocp-quiz-course-theme— container per question.div[data-part='choice-item']— each selectable answer choice inside a question.
The Alpha platform is a React SPA backed by a proper REST API hosted at api-v2.alpha.mycourseville.com/v2/.
| Layer | Detail |
|---|---|
| Routing | Clean REST paths: /v2/student/courses/:courseId/playlists/:playlistId |
| Authentication | Short-lived JWT access token + refresh token stored in localStorage (refresh_token) |
| Video Player | YouTube embeds; video IDs scraped from thumbnail src (vi/<videoId>/) |
| Progress API | POST /v2/student/playlists/:playlistId/videos/:videoId — Authorization: Bearer <token> |
| Status API | GET /v2/student/courses/:courseId/playlists/:playlistId — returns per-video watchedPercent |
| Token Refresh | POST /v2/auth/refresh — exchanges old refresh_token for a new access+refresh pair |
Target: Classic MyCourseville video progress tracking.
How it works:
-
Identify the video ID — The script first tries the page-global helper
cvdlit_getVideoID(). If that is not defined (some pages omit it), it falls back to readingdata-kaltura-entryiddirectly from the Kaltura player<div>. -
Fake a full watch — Kaltura progress is reported to the server as a list of "watched intervals" (integer indices). The script constructs a
URLSearchParamspayload with:v— the video IDtype— hardcoded"kaltura"n—400(number of intervals)i— a JSON-encoded array[0, 1, 2, …, 399]representing every interval out of 400
-
POST to the progress endpoint —
fetch("?q=cvdlit/ajax/recordbar", { method: "POST", body: data }). Because the request is made from within the same origin (the browser already has a valid session cookie), it is authenticated automatically. -
Retry once — The loop runs at most twice. If the server returns
status == 1on either attempt, an "success" alert is shown and the function returns. If both attempts fail, "failed, try again" is shown.
Why it works:
The server accepts the interval list without verifying it was produced by real playback events. Submitting all 400 intervals at once fools the server into believing the video was watched in its entirety.
Target: Classic MyCourseville multiple-choice quizzes.
How it works:
-
Discover all questions —
document.querySelectorAll(".cvocp-quiz-item.cvocp-quiz-course-theme")returns every question block on the page. -
Build an answer index — For each question (keyed by
dataset.qstnNid):- Collect the
valueattribute of everychoice-qstn-<nid>-<index>input element intoanswers[nid]. - Initialize the "current guess index"
o[nid] = 0.
- Collect the
-
Submit and evaluate (brute-force loop):
- The inner async function
l(guesses)builds a POST payload for?q=cvocp/ajax/submitquizanswercontaining:nid— quiz node ID from#cvocp-quiz-headersid— current session token from#cvocp-quiz-sessionanswer_<nid>— the value of the currently guessed choice for each question
- It also
.click()s the selected radio/checkbox in the DOM to keep the UI in sync. - The server response includes a
resultmap ({ <nid>: "0" | "1" }) and the currentscorevsscoretotal.
- The inner async function
-
Advance wrong answers — For each question where the result is
"0"(wrong), the guess indexo[nid]is incremented, so the next submission tries the next choice. -
Stop when perfect — The outer loop runs up to
ttimes (wheretis the number of choices per question). Ifscore == scoretotal, the UI is updated (crosses hidden, checks shown) and an "done" alert fires.
Why it works:
The quiz submission endpoint does not limit the number of attempts or add a delay between submissions, and it reveals per-question correctness in the response. This allows systematic elimination: at worst, the script needs one submission per wrong answer per question (i.e., choices - 1 rounds in the worst case).
Target: Alpha MyCourseville playlist video completion.
How it works:
-
Extract IDs from the URL — The course ID and playlist ID are parsed from
window.location.pathnameby looking for segments after"course"and"playlists"respectively. -
Scrape the video list from the DOM — All
<a>tags whosehrefcontains/playlists/and that contain an<img>child are collected. From each:id— last path segment of the linkhref(internal playlist-item ID)vid— YouTube video ID extracted from the thumbnailsrc(vi/<videoId>/)title— visible text of the link
-
Process each video in sequence:
A. Token rotation — Before every request, a fresh access token is obtained by
POSTing the currentlocalStorage.refresh_tokento/v2/auth/refresh. The newrefresh_tokenis stored back tolocalStorage. This avoids token expiry mid-loop.B. Fake progress submission — A
POSTis sent to/v2/student/playlists/:playlistId/videos/:vidwith:i— array[1, 2, …, 400](all 400 intervals)n—400videoType—"YOUTUBE"Authorization: Bearer <accessToken>header
C. Verification — A
GETto the playlist status endpoint confirmswatchedPercent === 100for the video. A warning is logged if the backend hasn't updated yet. -
Rate-limit safety — A 600 ms
setTimeoutpause is inserted between videos to avoid flooding the API.
Why it works:
Like the classic video finisher, the Alpha API accepts an arbitrary interval array without validating it against real playback telemetry. Providing all 400 intervals in one POST marks the video as 100% watched. The per-request token refresh ensures the short-lived JWT never expires during a long playlist.
| Feature | Classic MCV | Alpha MCV |
|---|---|---|
| Base URL | mycourseville.com |
alpha.mycourseville.com |
| Backend framework | Drupal (PHP) | Node.js / REST API |
| API style | Drupal ?q= routing |
REST (/v2/...) |
| Auth mechanism | Server-side session cookie | JWT (access + refresh token) |
| Token storage | Browser cookie (implicit) | localStorage |
| Video platform | Kaltura | YouTube |
| Progress payload | Form-encoded (URLSearchParams) |
JSON body |
| Quiz result in response | Per-question result map |
N/A (no quiz API yet) |
Create a single bookmarklet / browser extension that auto-detects whether the current page is Classic MCV or Alpha MCV (by hostname) and dispatches to the appropriate script.
Package the scripts as a Manifest V3 Chrome/Firefox extension with:
- A popup UI for triggering each tool.
- Persistent storage for tokens instead of relying on
localStorage. - Background service worker for token refresh without page reload.
The classic video finisher only operates on a single open video page. A playlist annihilator equivalent could:
- Enumerate all video nodes on a course page.
- Fetch each video's Kaltura ID via the existing DOM or the Drupal API.
- Call
recordbarfor each video sequentially (with rate-limit delays).
If the Alpha platform ever introduces quizzes, the same brute-force strategy should work against its REST quiz endpoint — substitute the Drupal submitquizanswer POST with the corresponding /v2/student/.../quiz endpoint and re-use the JWT header pattern from the playlist annihilator.
Future platform changes may add:
- Interval validation — checking that reported intervals are dense, contiguous, and match the actual video duration. Mitigation: compute intervals dynamically from a real-duration API call instead of hardcoding 400.
- Rate limiting / CAPTCHA — add randomized delays and human-like submission timing.
- Attempt limits on quizzes — store seen wrong answers locally and skip already-failed choices on re-run.
The current scripts surface errors only via alert() or console.error(). A shared utility module could provide:
- Structured error codes.
- A DOM-injected status overlay (avoiding reliance on
alert()). - Per-video / per-question retry logic with exponential back-off.