Skip to content

Instantly share code, notes, and snippets.

@travishorn
Last active July 7, 2024 19:44
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.

@gonzalocamarero
Copy link

@mweth @Patrick330 It's needed to declare "postMonth" variable too .

@Patrick330
Copy link

Ok, I've updated this and made it more robust. In particular, I:

  • Made the code robust to variations in the CSS classes assigned (a major source of errors)
  • Corrected an issue with certain date formats
  • Corrected an issue with unusual content formats.

It seems to be working well for me, but let me know if additional enhancements are needed. I'm going to fork it, so you can find my updated code here: https://gist.github.com/Patrick330/aa5d16efaacaee9ffb76e52ba4fb86f6

@elliottbenzle
Copy link

Thank you! Worked pretty well. Better than inspecting each image.

@RahulSDeshpande
Copy link

When I run this code in my Mac Chrome's console, the fails with the error Undefined.
I really dont know what is going wrong bcoz I know nothing about JS.
I am using this https://gist.github.com/Patrick330/aa5d16efaacaee9ffb76e52ba4fb86f6.

Can @Patrick330, @travishorn or anyone else help me here??

@pedralmeida
Copy link

pedralmeida commented Jun 27, 2023

When I run this code in my Mac Chrome's console, the fails with the error Undefined. I really dont know what is going wrong bcoz I know nothing about JS. I am using this https://gist.github.com/Patrick330/aa5d16efaacaee9ffb76e52ba4fb86f6.

Can @Patrick330, @travishorn or anyone else help me here??

The script isn't working because fields changed names again. "data-test-name" is now only "data-name", for instance. Even then it takes a few more tweaks because of the dates, I personally gave up.

if you have access to a PC, what I suggest, for a layman like you, is to use the ChromeCacheView app. it will allow you to bulk download every content loaded by the browser into its cache, in a very simple way.

@vizyonok
Copy link

vizyonok commented Jul 26, 2023

Guys, if somebody has created a working version of the script, please share it or let us know here.
Thanks in advance! 🤗

@Loksly
Copy link

Loksly commented Aug 1, 2023

Use the API instead of scrap:

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);
    }

    attachments.reduce((p, c, i) => {
        return p.then(() => downloadAttachment(c, i));
    }, Promise.resolve());
}

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();
            counter++;
        }
        );
    });
}

d();

Note: you'd better configure your browser so not to ask for every downloaded file where should be stored.

Please consider supporting Classdojo.

@vizyonok
Copy link

vizyonok commented Aug 1, 2023

@Loksly amazing! Thanks a ton! It works just flawlessly 👌❤️🙏

@joaoubaldo
Copy link

Here's the python script I've been using so far
https://gist.github.com/joaoubaldo/ce75db527bb9b865bd8cc464160be19d

  1. Save each response from https://home.classdojo.com/api/storyFeed (scroll down on the classdojo story feed) as a .json file in the same directory as this script
  2. Run script. Media files will be downloaded in parallel.

@neurolizer
Copy link

Is anyone else getting a CORS policy error with the wonderful API provided by @Loksly ? My feed only has one post, but the post has 7 videos in it. The API script downloads the first 4 videos great, but then throws this CORS error and stops. I tried switching to no-cors, which allows it to download the first 5 videos, but then throws the same error:

Access to fetch at 'https://svideos.classdojo.com/0e5...003/2023-08-17/590...a2c/436...4ef.mp4?Policy=eyJ...dfQ__&Key-Pair-Id=KS4...06Z&Signature=HUU...g__' from origin 'https://home.classdojo.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
GET https://svideos.classdojo.com/0e5...003/2023-08-17/590...a2c/436...4ef.mp4?Policy=eyJ...Q__&Key-Pair-Id=KS4...06Z&Signature=HUU...g__ net::ERR_FAILED 200
Uncaught (in promise) TypeError: Failed to fetch
at downloadAttachment (:58:12)
at :39:29

Also, does anyone know how to turn the API script into something we could use in Tampermonkey that would give us a download all button when we are on the home story page?

@neurolizer
Copy link

neurolizer commented Aug 21, 2023

I was able to get Loksly's API code working in Tampermonkey so it displays a Download All button in the top left when you are on home.classdojo.com. Conveniently, when the script runs via Tampermonkey it is able to continue past the CORS error to download the last video and the image on the default new class post. Strangely the one video I actually want is the one that errors and doesn't download, but I was able to get it manually. Hopefully it will work better for future posts.

// ==UserScript==
// @name         ClassDojo Story Feed Download All Button
// @namespace    https://gist.github.com/travishorn/c2b6111a4e63efdbf87a1de84c833ab1?permalink_comment_id=4647516
// @version      0.1
// @description  Download attachments from ClassDojo feed with a "Download All" button in the top left
// @author       Loksly
// @match        https://home.classdojo.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

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

    function getFeed(url) {
        return fetch(url, { // Headers and options here...
        //      "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 downloadAttachments() {
        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);
        }

        await Promise.all(attachments.map((attachment, i) => downloadAttachment(attachment, i)));
    }

    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+1) + "_" + attachment.time.split('T')[0];

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

    function createDownloadAllButton() {
        const downloadAllButton = document.createElement("button");
        downloadAllButton.textContent = "Download All";
        downloadAllButton.style.position = "fixed";
        downloadAllButton.style.top = "8.3%"; // Adjust Download All button location with style.top and left percentages
        downloadAllButton.style.left = "6%";
        downloadAllButton.style.zIndex = "9999";
        downloadAllButton.addEventListener("click", () => {
            downloadAttachments();
        });

        document.body.appendChild(downloadAllButton);
    }

    // Call the function to create the "Download All" button
    createDownloadAllButton();
})();

@MrMPM
Copy link

MrMPM commented Sep 4, 2023

When I go on Class Dojo website, the address is: https://home.classdojo.com/#/story
Should I use this or as per scripts above, /api/storyFeed? Anyway, neither works for me... @Loksly script does something, but returns error, other scripts return "undefined" immediately.

@neurolizer
Copy link

https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&withArchived=false is still the correct URL. If you go there directly, you can see the json (or whatever) formatted data that Loksly's script parses. If you just need a few pictures/videos, you can find the links in the api/storyFeed data and download them manually.
The code I posted is designed to be used inside the Tampermonkey extension for Chrome. It errors a lot for me too though, probably downloads less than half the pictures/videos because of CORS errors that I haven't found a way to fix.

@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.

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