Files

13 KiB
Raw Permalink Blame History

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:

  1. 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.
  2. 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.
  3. 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/toc misses every assignment prompt. See references/d2l-api.md §4.
  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.
  5. 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):

  1. 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.
  2. Videos — by default, skip downloading embedded lecture videos and just record a link to each one in the manifest (see references/content-classification.md bucket 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. Follow references/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.md bucket 5 for why this is non-negotiable rather than just a default.
  3. 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.md for 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:

  1. 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.
  2. 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.
  3. 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 Referer header — no browser needed). Download in parallel batches of 50–100 segments for throughput.
  4. Mux video + audio with ffmpeg -c copy -movflags +faststart.
  5. Clean up intermediate _video.mp4 / _audio.m4a files.

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").