Skip to content

Instantly share code, notes, and snippets.

@travishorn
Last active July 4, 2025 08:49
Show Gist options
  • Save travishorn/c2b6111a4e63efdbf87a1de84c833ab1 to your computer and use it in GitHub Desktop.
Save travishorn/c2b6111a4e63efdbf87a1de84c833ab1 to your computer and use it in GitHub Desktop.
Saving the images and videos from your ClassDojo storyline

Archived

Please see Patrick330's fork of this script.

ClassDojo changes their site regularly, so a script like this needs regular maintenance. I have decided to archive this project and stop providing updates. Patrick330's fork linked above may be a good alternative.

Original Purpose

ClassDojo is a classroom communication app used to share reports between parents and teachers. Teachers track student behavior and upload photos or videos. The gamification style system teaches developmental skills through real-time feedback.

When your child's teacher shares a photo, it goes on your parent "storyline". Unfortunately, ClassDojo does not provide any means of saving these photos. In fact, the photos are displayed in a <div> using style: background-image('...'); so right-clicking and choosing "Save image" is not an option.

@reloadfast
Copy link

Huge thank you @Loksly!

@chunte
Copy link

chunte commented Jul 5, 2024

Thanks @Loksly for your code. I started the code 3 hours ago, and it's still chugging along.

@neurolizer I tried using Tampermonkey (a pretty cool extension), but somehow it misses many files. For example the download counter goes from to ..., 15, 16, to 405, 406, so on and maybe to 1003, 1004, ...

@vizyonok
Copy link

vizyonok commented Jul 7, 2024

@neurolizer, yes, it looks like the CORS isn't supported anymore, so try switching to "no-cors" instead.

Just change the word cors 2 times in @Loksly's script above to just no-cors. It worked out for me.

@sunghin
Copy link

sunghin commented Jun 17, 2025

Thanks @Loksly for the script!!

I didn't have enough posts on my feed to warrant downloading Tampermonkey, but like others pointed out, the download counter indicated that a lot of images were skipped, even using @Loksly's original script.

I updated his script with a 0.1s delay between downloads to make sure there's no skipped or overlapping downloads:

  • Replaced attachments.reduce(...) with a standard for loop.
  • Added await delay(100) between each download.
  • Added a delay() helper function that returns a Promise that resolves after 100 ms.

My updated script in full:

const FIRST_FEED = "https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&withArchived=false";

function getFeed(url) {
    return fetch(url, {
        "headers": {
            "accept": "*/*",
            "accept-language": "es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7",
            "cache-control": "no-cache",
            "pragma": "no-cache",
            "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Linux\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
            "x-client-identifier": "Web",
            "x-sign-attachment-urls": "true"
        },
        "referrer": "https://home.classdojo.com/",
        "referrerPolicy": "strict-origin-when-cross-origin",
        "body": null,
        "method": "GET",
        "mode": "cors",
        "credentials": "include"
    }).then((response) => response.json());
}

const attachments = [];

async function d() {
    let feed = await getFeed(FIRST_FEED);
    grabFeedAttachments(feed);

    while (feed._links.next && feed._items.length > 0) {
        feed = await getFeed(feed._links.next.href);
        grabFeedAttachments(feed);
    }

    for (let i = 0; i < attachments.length; i++) {
        await downloadAttachment(attachments[i], i);
        await delay(100); // wait 100ms between downloads
    }
}

function grabFeedAttachments(feed) {
    feed._items.forEach((item) => {
        item.contents.attachments?.forEach((attachment) => {
            if (typeof attachment.path === "string") {
                attachments.push({
                    url: attachment.path,
                    time: item.time
                });
            }
        });
    });
}

function downloadAttachment(attachment, counter) {
    return fetch(attachment.url).then((t) => {
        return t.blob().then((b) => {
            let a = document.createElement("a");
            a.href = URL.createObjectURL(b);

            let filename = String(counter) + "_" + attachment.time.split('T')[0];

            a.setAttribute("download", filename);
            a.click();
        });
    });
}

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

d();

@Loksly
Copy link

Loksly commented Jun 17, 2025

Thanks for the info.

Please consider supporting Classdojo.

@brimaster
Copy link

Loksly's script with sunghin's edits worked perfectly! I just had to start a new Chrome session with web security disabled (do at your own risk!)

On Windows, simply create a shortcut with the target: "[path to chrome]\chrome.exe" --disable-web-security --disable-gpu --user-data-dir=%LOCALAPPDATA%\Google\chromeTemp

@MacManas
Copy link

MacManas commented Jul 4, 2025

If you have more than one kid, you can use:
const FIRST_FEED = "https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&studentId=<STUDENT_ID>";
The <STUDENT_ID> is the number that appears in the URL when you select the feed from only one kid.
Also, had to remove withArchived=false since otherwise it tends to mix feeds from the kids for some reason. Since I don't have access to the API docs, I cannot tell why this is the case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment