13 KiB
name, description
| name | description |
|---|---|
| brightspace-course-archiver | Downloads all the files, readings, and assignment/task descriptions from a D2L Brightspace course into a local folder, preserving the course's module structure. Can also download lecture videos from Mediasite (a common university lecture-capture platform) and Zoom cloud recordings (review sessions, office hours) when the user asks. Writes a manifest of everything found. Use this whenever the user wants to archive, back up, download, or export a Brightspace course — phrases like "download my course materials," "archive this class," "save everything from Brightspace," "get all the readings/slides/PDFs from this course," "download the lecture videos," "get the Zoom recording of the review session," or "export my course before it disappears" should all trigger this, even if they don't say "Brightspace" by name as long as a brightspace.* URL or D2L-style course link is involved. Also trigger if they paste a Brightspace course URL and ask you to "grab," "pull," or "get" what's in it. |
Brightspace Course Archiver
Downloads every reachable piece of course content from a D2L Brightspace
course — readings, slides, study guides, assignment instructions — into a
folder on the user's machine, mirroring the course's own module structure,
plus a manifest.md that says exactly what was found, what got downloaded,
and what got deliberately skipped (with a link) and why.
This only works for D2L Brightspace (the LMS at *.brightspace.com or a
school's own Brightspace domain like brightspace.nyu.edu) — it leans on
Brightspace's specific Valence REST API, which Canvas/Moodle/etc. don't
share. If the user's course is on a different LMS, say so plainly rather
than trying to improvise; it won't work.
Why this is a real technical challenge worth a dedicated skill
Five things make "just download my course" harder than it sounds, and this skill exists to handle all of them so you don't have to rediscover them by trial and error on every course:
- Content isn't where it looks. A lot of "File" topics are actually
HTML wrapper pages that embed the real PDF/slide-deck in an iframe — you
have to unwrap them. See
references/content-classification.md. - Large-file downloads silently truncate on some browser bridges.
Fetching a multi-megabyte file in one shot can come back corrupted with
no obvious error. The fix is chunked Range requests. See
scripts/relay_fetch.js. - Assignment instructions usually aren't files at all — they're rich
text sitting inside the Dropbox/Quiz API, not the content tree, so a tool
that only walks
/content/tocmisses every assignment prompt. Seereferences/d2l-api.md§4. - Lecture videos look non-downloadable but often aren't. Mediasite —
one of the most common lecture-capture platforms at universities — has a
PlayerService API that exposes HLS manifests with open CORS. Once you
have the playback ticket from one authenticated API call, the entire
download is plain HTTP that doesn't need the browser at all. See
references/mediasite-video-download.md. - Zoom recordings aren't in the content tree as videos. They arrive as share links buried in a Word file, module description or announcement, or only inside the course's Zoom LTI tool (Cloud Recordings tab). Playing one needs the browser signed in to Zoom itself, not just Brightspace, and course recordings usually have downloads disabled by the host. See
references/zoom-recording-download.md.
Before you start: confirm scope with the user
Don't guess at these — ask (a single AskUserQuestion call is fine for the
first two, and can also bundle the third):
- Which course, and where should it go? Ask for the course's Brightspace
URL if they haven't given one (the org unit ID lives in it — see
references/d2l-api.md§0). Ask where they want the archive saved — a connected folder if you're bridged to their computer, or ask them to name a folder. Don't default to a fixed path; people's setups vary. - Videos — by default, skip downloading embedded lecture videos and
just record a link to each one in the manifest (see
references/content-classification.mdbucket 4). But if the user explicitly asks for videos, check the platform first:- Mediasite: downloadable. Follow the procedure in
references/mediasite-video-download.md. Warn the user about size (~600 MB per 3-hour lecture) and confirm they want all of them before starting. Requires ffmpeg for muxing — check that it's installed before downloading any segments. - Zoom cloud recordings: findable and, once the user is signed in to Zoom in their browser, downloadable as a single MP4 using their Zoom cookies (no ffmpeg needed). Course recordings usually have
disableDownload: true, and the cookie route works around that host setting, so by default link them; download only when the user explicitly asks after being told downloads are disabled. Followreferences/zoom-recording-download.md. - Other platforms (Kaltura, Panopto, YouTube, etc.): link in the manifest instead. These are typically DRM'd or auth-gated in ways that don't expose a clean download path.
- The "link, don't download" rule applies even harder to paid
third-party coursepacks (Harvard Business Publishing and similar) —
see
references/content-classification.mdbucket 5 for why this is non-negotiable rather than just a default.
- Mediasite: downloadable. Follow the procedure in
- Notes/dashboard and task-tracker sync — these are optional add-ons
on top of the file archive, not required for it. Ask after the archive
is done (not before — the answer matters less than getting the files
safely downloaded first). See
references/task-tracker-integration.mdfor the full "ask, discover what's connected, offer to help connect what isn't" pattern. Never assume Obsidian or any specific tracker.
Workflow
1. Confirm the browser session is logged in
Open the course URL in an authenticated browser session — however your environment provides browser automation (CDP connection, relay extension, managed Chromium instance, or any tool that can execute JavaScript in a logged-in tab and return the result). If the page redirects to an SSO/login page, tell the user and wait for them to complete login (including any 2FA) — you cannot and should not attempt to log in yourself. Once you land on the actual course page, you're ready.
2. Discover the API version and fetch the content tree
Read references/d2l-api.md §1–2. Don't hardcode a version number from a
previous course — different tenants run different releases.
3. Classify every topic before downloading anything
Read references/content-classification.md in full. Do this as one fast
pass (fetch + regex-check every topic's URL) across the whole course, then
show the user a quick plan: "Found N modules, ~X files to download, Y
assignment descriptions to render as PDF, Z videos (W downloadable from
Mediasite, U Zoom recordings, V linked instead of downloaded)." This costs almost nothing
compared to the download itself and catches scope surprises (e.g. a
40-video course) before you've sunk time into it.
4. Also pull assignment/quiz instructions
These live in the Dropbox and Quizzes APIs, not the content tree — read
references/d2l-api.md §4–5. Don't skip this step even if the content tree
already produced plenty of files; assignment prompts are frequently the one
thing a student most needs archived and are easy to miss entirely.
5. Download real files
For every bucket-1/bucket-2 item from the classification pass, download it
with the chunked-Range technique in scripts/relay_fetch.js. Test the
"does this bridge truncate large transfers" assumption once at the start
(see the comment at the top of that file) rather than assuming — if it
doesn't truncate in your environment, you can use bigger chunks or skip
chunking and move faster.
Automate this completely. Do not use a download method that triggers a
browser/OS-level download prompt or a save-file dialog per file — with
dozens of files, that means dozens of interruptions asking the user to
approve or name each one, which defeats the entire point of an "archive my
course" tool. The in-memory fetch approach in relay_fetch.js never
touches the browser's download UI at all; that's exactly why it's the right
technique here, not just a workaround for the truncation bug.
Write each file into <output>/<Module Name>/<file name>, using the
module's real name from the TOC (sanitized for the filesystem) — see
references/d2l-api.md §2 on why not to assume a naming convention.
6. Download videos (if requested and on a supported platform)
If the user asked for videos and the classification pass found Mediasite
topics (bucket 4a), download them now. Follow
references/mediasite-video-download.md end to end:
- Check ffmpeg first. If it's not installed, tell the user before downloading any segments — they'll need it for muxing and there's no point fetching hundreds of megabytes of segments without it.
- Navigate to the Mediasite channel page in the authenticated browser (follow the LTI quicklink). Scrape presentation IDs and call the PlayerService API from that page context.
- Download HLS segments outside the browser. Once you have the manifest
URLs with playback tickets, switch to direct HTTP (any runtime that can
set a
Refererheader — no browser needed). Download in parallel batches of 50–100 segments for throughput. - Mux video + audio with
ffmpeg -c copy -movflags +faststart. - Clean up intermediate
_video.mp4/_audio.m4afiles.
For Zoom recordings (bucket 4c), follow references/zoom-recording-download.md: find share links in the content tree and recordings in the Zoom LTI tool's Cloud Recordings tab, confirm the browser is signed in to Zoom (stop and ask if the share link redirects to zoom.us/signin), get a fresh signed MP4 URL from the play page's play/info call, and download it in Range chunks from outside the browser with the user's .zoom.us cookies and a Zoom Referer. Only do this for recordings the user asked for, since downloads are usually disabled by the host.
For bucket-4b videos (other platforms), and Zoom recordings the user didn't ask to download, record the permanent viewer or share link in the manifest and move on.
7. Render bucket-3 content pages and all assignment/quiz instructions to PDF
Use scripts/html_to_pdf.py. If a page has embedded images that need to
survive, use its inline_images() helper while you still have the
authenticated fetch available (see the docstring) — don't call it after the
fact once you've lost the ability to fetch those image URLs.
Name assignment PDFs clearly, e.g. Exercise 4 Instructions.pdf, and put
them in the module folder they belong to (a Dropbox folder's name usually
tells you which module, but if it's ambiguous, put it in a top-level
Assignments/ folder inside the course archive rather than guessing wrong).
8. Write manifest.md
One file at the root of the archive. This is what lets the user (and you, on a future run) know the true state of the archive without opening every subfolder. Include:
- A table per module: file name, size, and status (downloaded / rendered to PDF / linked-not-downloaded / failed with reason)
- A separate "Videos" section. For downloaded Mediasite and Zoom recordings, list the
filename, duration, and file size. For non-downloaded videos (bucket 4b),
list the Brightspace viewer link (
/d2l/le/lessons/{orgUnitId}/topics/{topicId}) and note the platform. - A separate "Assignment & Quiz Instructions" section listing each PDF you
rendered, with its due date (converted to local time per
references/d2l-api.md§9) and weight if you pulled grades - Anything you couldn't resolve at all (broken link, permission error, unrecognized topic type) with enough detail that the user could chase it down themselves
9. Offer the optional add-ons
Only after the archive + manifest exist. Follow
references/task-tracker-integration.md.
10. Summarize
Tell the user, in plain language: how many files downloaded, how many assignment PDFs rendered, how many videos downloaded vs. linked, anything that failed, and where the archive lives. Don't make them go read the manifest to find out whether it worked.
Guardrails
- Never enter the user's LMS credentials yourself — you're relying on their already-authenticated browser session, and if that session isn't logged in, that's a stop-and-ask moment, not a "try to log in for them" moment.
- Never use a download technique that triggers a per-file browser approval — see step 5. If you notice yourself about to click something that would pop a save dialog, stop and switch to the fetch-and-relay approach.
- If a course has an enormous amount of content (hundreds of files, dozens of videos), say so before you start rather than silently churning for a long time — the user may want to scope it down (e.g. "just the PDFs, skip slide decks over 20MB").