Posts

DocuTube Shutdown, June 2020

tl;dr: The DocuTube addon will be removed from the Marketplace at the end of June, 2020. If you're a programmer and you want to take over ownership of the project, let me know.


Three years ago, I wrote a proof of concept Google Docs app that found YouTube links and displayed videos embedded in a sidebar. It was fun to do and showed some powerful uses of Apps Script in a real way.

I'd made some incremental updates here and there and then worked on a major update over the fall and winter of 2019. During that time, Google updated the platform the code runs on which has been a little hit or miss in terms of usability.

I've had a love/hate relationship with the addon. For instance, it won't work for someone who doesn't have edit rights to the doc. So, what was meant to help teachers and students work effectively isn't a good solution because of the way addons run.

Second, there have been some very strange errors that I have no control over, mainly within Google's app engine. There are service outages and access errors that I cannot prevent or handle well. This has meant frustration for users and for me.

In the end, it showed that Apps Script on it's own can be a great place to build simple utilities that help add functionality and usability to systems. But the fact of the matter is that I cannot solve many of the main issues with DocuTube and I'm at a point where I can't commit more time to it. Rather than leave a buggy, unmaintained app in the Marketplace, I'll be removing it from the store at the end of June once everyone is out of school.

If you're a programmer or hobbyist and you want to take up maintenance, get in touch because I'd be happy to transfer the codebase over to you and you can republish with all the same assets if you'd like.

If you were a user, thanks. If you wrote a review, thanks. If you've never heard of it, not a big deal.

Comments

ES

Is there any replacement you know of?

Brian Bennett

The closest I’ve seen is more of a workaround than a replacement. You can embed a video in slides, then link the slideshow in the document. You can pull up a playable view of the slides in the doc. Here’s a video from Greg Kulowiec on how to put it together.

Naveed

Hi from Pakistan.

I’m a teacher and in my country mostly students don’t have access to smartphones and YouTube. So i show my class students Youtube educational videos through smart tv. I have been using your DocuTube since a long time to maintain YouTube videos links. As for my students i have to make the doc file of best YouTube videos related to their daily lesson topics.

I would be very thankful to you if you fix your DocuTube and relaunch it as without this app maintaining YouTube links in doc text file is really a painful task.

Please do it for teachers and students, as your app is really a gem for us.

Updating Canvas Notification Preferences

This is a technical post. Read on for code and commentary.


In moving to online, we've tried to streamline all of our communication through Canvas. The goal is to cut down on disconnected email threads and encourage students to use submission comments to keep questions and feedback in context.

The Problem

Many students had already turned off email notifications for most communications in Canvas, preferring not to have any notices, which reduces their responsibility for teacher prompting and revision. Notifications are a user setting and the Canvas admin panel doesn't provide a way to define a default set of notification levels for users. However, with the API, we were able to write a Python program that sets notification prefs by combining the as_user_id query param as an admin that sets user notification preferences.

API Endpoints

  • GET user communication channel IDs: /api/v1/users/:user_id/communication_channels
  • PUT channel preferences for user: api/v1/users/self/communication_channels/{channel_id}/notification_preferences/{msg_type}

Params

  • Int user_id
  • Int channel_id
  • String frequency

Get User IDs

There is no easy way to programmatically get user IDs at the account or subaccount levels without looping each course and pulling enrollments. Instead, we opted to pull a CSV of all enrollments using the Provisioning report through the Admin panel. We configured separate files using the current term as the filter. This CSV included teacher, student, and observer roles. The script limits the notification updates to student enrollments.

Script Details

The full program is available in a GitHub gist. Here is an annotated look at the core functions.

main handles the overall process in a multi-threaded context. We explicitly define a number of workers in the thread pool because the script would hang without a defined number. Five seemed to work consistently and ran 1500 records (a single subaccount) in about 7 minutes.

The CSV includes all enrollments for each student ID, so we created a set to isolate a unique list of student account IDs (lines 9-10 below).

To track progress, we wrapped the set in tqdm. This prints a status bar in the terminal while the process is running which shows the number of processed records out of the total length. This is not part of the standard library, so it needs to be installed from PyPI before you can import it.

def main():
    """
    Update Canvas user notification preferences as an admin.
    """
    unique = set()
    data = []
    with open('your.csv', 'r') as inp:
        for row in csv.reader(inp):
            if re.search("student", row[4]):
                unique.add(int(row[2]))

    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        with tqdm(total=len(unique)) as progress:
            futures = []
            for student in unique:
                future = executor.submit(process_student_id, student)
                future.add_done_callback(lambda p: progress.update())
                futures.append(future)
            results = [future.result() for future in futures

process_student_id is called by the context manager for each student ID in the set. Canvas breaks communication methods into "channels:" email, push, Twitter, etc (line 3). Each channel has a unique ID for each user, so we needed to call each user's communication channels and then pass the ID for emails to a setter function.

def process_student_id(student):
    # Get their communication channel prefs
    channel_id = get_channel_id(student)

    try:
        # Update the channel prefs and return
        update = update_prefs(student, channel_id)
        return update
    except Exception as e:
        print(e)

GET communication_channels

def get_channel_id(student_id):
    url = f"https://yourURL.instructure.com/api/v1/users/{student_id}/communication_channels"
    resp = requests.request("GET", url, headers=headers)

    for channel in resp.json():
        # find the ID of the email pref
        if channel['type'] == 'email':
            return channel['id']

PUT communication_channels/:channel_id/notification_preferences/:message_type[frequency]

The communication channel can receive several types of communications. We wanted to set the student notifications to "immediately" for new announcements, submission comments, and conversation messages. You can define others as well as their frequencies by modifying the values on lines 3-4.

The communication types are not well documented, so we used our own channel preferences to find the notification strings: GET /users/self/communication_channels/:channel_id/notification_preferences.

The crux of this step is to make the request using the Masquerading query param available to the calling user. Make sure the account which generated the API key can masquerade or else the script will return an unauthorized error.

def update_prefs(student_id, channel_id):
    # loop through different announcement types
    types = ["new_announcement", "submission_comment", "conversation_message"]
    frequency = "immediately"  # 'immediately', 'daily', 'weekly', 'never'
    responses = []

    for msg_type in types:
        url = f"https://elkhart.test.instructure.com/api/v1/users/self/communication_channels/{channel_id}/notification_preferences/{msg_type}?as_user_id={student_id}&notification_preferences[frequency]={frequency}"
        resp = requests.request("PUT", url, headers=headers)

        responses.append(resp)

    return responses

Final Thoughts

Updating a user's personal preferences isn't something I was thrilled about doing, but given our current circumstances, it was preferable to the alternative of continuing to struggle to help students move forward in their coursework. Further improvements would be to call each CSV in the file system incrementally, cutting down on the time someone has to log in and run the script. Hopefully, this only needs to be done once and does not become a recurring task.

Second, there is an endpoint in the API to update multiple communication preferences at once, but it isn't well documented and I wasn't able to get it working reliably. For just one channel and three specific types of messages, the performance improvements probably would have been negligible (at least that's what I'm telling myself).

A Photo API in Python

I started a small project to clean up my personal photo hosting. I'm currently using an app written in PHP, but it's way over my head and does way more than I want or need. So, I'm taking this opportunity to roll my own backend API for photo storage and serving.

I'm building it in Flask and using the Flask-RESTful extension to help with routing, HTTP methods, and overall structure. I'm hoping that if I have a functional backend, I can learn a frontend framework like Vue in a more formal way.

It's nothing fancy, but the bones are starting to grow a little here and there. Right now, I have it returning all references in a database as well as querying by ID directly. Next, I'm going to add some methods to update the photo title and then I'll move on to uploading files directly.

Source code is in GitHub if you want to look. No pretty pictures because all it serves right now is data.

Quince House

I just finished packing up our first house, to be put on the market tomrorow morning after living here for eight years. As I moved from room to room making small repairs this morning, I thought back on the time, sweat, and energy we've poured into it.

We bought the house after ignoring it for months because the outside was...interesting. My father-in-law convinced us to pull over and look into windows. That's the first time we met the neighbor, Dan, who with his wife Vicki, are the best neighbors in the world.

Yes, they're better than your neighbors. I will die on this hill.

All four of our kids were born in this house. My wife's family lived down the road and my parents were able to come and stay with us every couple of months. The neighbors became adopted grandparents by all of the kids. Small group meetings, brothers driving through, visits with cousins and other friends happened here. Our lives were shared in these rooms.

Circa 2017
Sometimes there just isn't enough room on the couch.
Circa 2018
This is about a year before the latest baby was born in June 2019

Since living here, I've learned how to frame, tile, plumb, and do my own drywall and electrical. I've installed a septic system. I've converted the house from electric heat to a gas furnace. We took a large bedroom and made it into two. Both bathrooms have had remakes...and those are just the large projects.

image1
Plumbing an entire bathroom by myself was terrifying. So terrifying that I installed the shower mount upside down to remind myself to let things go. The shower still works.
image2
Repairing a septic field isn't hard, necessarily, but it's a huge task.

I never would have imagined learning how to do all of those things (with my father-in-law's help) and now I can't imaging not having learned those things.

Circa 2013
Lindsey's dad (in grey) is a contractor and taught me 99% of what I've learned in the last eight years.

The spring brought new planing and the summers were full of wild raspberries, vegetables from the garden, and more recently, armloads of cosmos, ranunculus, and roses.

We watched the bats wake up during dusk and wondered how loud the cicadas could get during the day.

These things won't stop. I'm pretty sure cicadas live in Michigan, too.

image3
These guys look way meaner than they actually are.

But all of these things happened for the first time here. At Quince House.


It really hit me that we're leaving when I took the swings out of the tree. I can't bring the house, but some parts of it are coming with us.

2020
This is where the new house will stand in the fall of 2020.

We'll be building our next house on a plot of land adjacent to my brother-in-law's farm. We'll be able to wake up and see the cattle out back along the treeline. We're planning on building a hiking trail from our land to theirs so the cousins can visit whenever they want.

There will still be joy. There will still be disappointment and growing.

Quince House got us started.


The featured photo is of my backyard, early 2020.

Early 2020

Performance Tasks in SBG

I was given a good challenge by our secondary curriculum coordinator a couple weeks back. He wanted to know how we get in front of standards-based grading being reduced to collections of isolated skills. In other words, we're doing well tracking our essential standards over time, but those are more or less in isolation (not taking into account any spiralling or scaffolding happening).

File this under stream-of-consciousness rambles. I have three thoughts percolating:

In terms of how to do this...well, I haven't quite made it there yet. I have a feeling that this would be a good place for a single point rubric (because those are the new ??? right now) because ofo the flexibility they provide.

Another tack would be to write new performance standards which combine the individual standards, but that's another level of organization to add on top of unpacking the current content material. It could work with a larger group together at the district level, but consensus becomes the challenge.

If you're a teacher using SBG, what thoughts do you have? How do you make sure students are forming holistic understanding and not simply accruing a collection of ideas?

Other reading

RedesignU (which I need to investigate more) has a curated list of reading that led me to this helpful policy guide for SBG at a larger level. It includes some guiding questions on performance tasks which were interesting.

CompetencyWorks has a really short article that was thin on material but had a good bulleted list of performance task criteria for the SBG classroom that made me think about what would be included.

Here's another CompetencyWorks article, which is very dated, but raises some good points about interoperability of various SBG tracking systems and the challenges faced when trying to get a wide-angle lens on student growth. Designing performance tasks includes content and helping stuents navigate that process (it's personal, remember?) means we need information to work from. The systems have improved, but it is still difficult to build a full working system on the fly.


Pool Edge by Theen ... is licensed under CC BY-NC-SA

Lemonade Factory

The LMS isn't perfect, but it's what we have.

We've gone through a sea change over the last four years. It started with opening Google Apps for all staff and students and then rolled into using Canvas as an LMS. I have mixed feelings about how heavily we're pouring into people using it, but I think I've landed on, "you have to start somewhere."

Questions I get now were not thinkable three years ago. The more staff get comfortable with the low-level functions (making assignments, using quizzes) the more questions I get that tend to revolve around achieving a goal rather than doing a thing. I have teachers moving toward questions like, "Can my students keep writing journals in Canvas?" (No.)

Discussions are getting better about how to do different things. There is more interest in creating opportunities for students to do more open-ended, meaningful work. I would love to be able to only use the LMS for collating information from other places. I want to make sure teachers are considering where work is done and how it is stored. I want students to be able to keep their own record of what they're doing.

It's a starting point. Canvas, with all of its flaws, has opened up several avenues of discussion that were not possible before we had it in place. I think the main danger is throwing all of the instructional chips into one place. Use the LMS as a launching point, not the end. Keep perspective on what meaningful work actually looks like.

Comments

Tom Woodward

The middle road seems to be a hard path to walk. There’s a weird pleasure in the false simplicity that comes from taking a polarized position. I did/do some mocking of the LMS. I’ve probably stepped over that line at times.

My revised goal is generally to make people some degree happier, save them a bit of time, improve an assignment some amount, and hope that good things will snowball from that.

Unit Testing in GAS Part 5: Testing Objects and Arrays

If you're brand new to unit testing, start with the first post in this series to get caught up.


It's time to dive into deeper equality situations with objects and arrays. Every test we've written so far has used a non-strict comparison. In other words, we've only been checking value but not type. This is particularly important in JavaScript because of how it handles truthy and falsy values.

For instance, if you were to write a test checking the equality of 0 and undefined, what would you expect? They're different, right? If you want to try it yourself, you can write a quick test:

QUnit.test('truthy and falsy values', function() {
    equal(0, undefined, 'both are falsy values, but unequal types') // pass
    deepEqual(0, undefined, 'both are falsy values, and unequal types') // fail
})

QUnit passes the first test because both 0 and undefined are falsy - the values are the same, but the type is different. Using equal as the assertion only checks against the value of the actual and expected arguments. This is where deepEqual helps. Instead of checking values only, deepEqual performs a strict check of both the value and type of the arguments.

Objects and Arrays

We have only looked at simple values - numbers and strings. In this post, we'll look at using deepEqual and propEqual to test objects and arrays. Rather than jumping right into testing our Calcs class, let's start with two simpler examples. Start by adding this to your tests.gs file:

// function calcTests() { ... }

// Create a new test wrapper to keep things neat
function objectTests() {
    QUnit.test('Object and array basics', function() {
        var array = [1,2,3,4];
        deepEqual(array, [1,2,3,4], 'the arrays are equal');
    });
}

This is the first time we've defined a variable inside a Qunit.test instance. Each test is a function, so it can have block-scoped variables used in the test. These variables do not affect other functions in the wrapper. Eventually, we will be retrieving exisitng objects and arrays to test, but for now, we'll define them with each test as necessary.

Because we're defining a new wrapper, you need to go to config.gs and add objectTests() to the tests() wrapper for these new tests to run:

function tests() {
    console = Logger; // Match JS
    calcTests(); // Collection of tests on the Calcs class.
    objectTests(); // new tests for checking objects and arrays
}

This is personal preference, really...there is nothing saying you cannot include these checks in the calcTests wrapper we're using, but I find it helpful to break out tests into similar groups.

Reload the web app and you'll see a new line passing the array deepEqual check we just wrote. Let's do the same thing for an Object:

function objectTests() {
    // ... array check
    deepEqual({a: "hello", b: "world"}, {a: "hello", b: "world"}, 'These objects are equivalent');
}

This test will also pass because the objects have strict equality with one another. deepEqual is recursive, meaning it will check for equality even within nested objects:

function objectTests() {
// ... array check
// ... shallow object check
deepEqual(
    {
        a: "hello",
        b: "world",
        c: {
            aa: "foo",
            bb: "bar"
        }
    }, {
        a: "hello",
        b: "world",
        c: {
            aa: "foo",
            bb: "bar"
        }
    }, 'Nested objects can be tested, too');
}

Checking Constructed Objects

Checking object constructors is complicated. You cannot just define a matching object in the function because deepEqual checks the constructor along with the value. Rather than testing the entire object, it is better to check each part of the object.

This follows with the unit testing philosophy - test the smallest possible pieces of your code. If you want to test the structure of the object, we can assign a variable to an object with the desired properties and test our Calcs object against it with propEqual.

To help with flow control, I've added an init() method Calcs which will return the entire object. It doesn't matter a whole not right now, but it will in future posts.

var Calcs = (function() {
    const init = function() {
        return this;
    }
    // rest of Calcs
})

From now on, when we need to instantiate Calcs, we'll use Calcs.init().

To test obect properties, let's add a variable with a known structure to use as our expected value. Then, we'll call Calcs.init() to get the full object back to compare properties.

function objectTests() {
    // ... array, shallow, and deep object checks
    // Model the structure of the Calcs object
    var testCalcsClass = {
      init: function() {},
      name: "calculation methods",
      about: function() {},
      author: function() {},
      add: function(a, b) {},
      isNumber: function(val) {},
      addArray: function(arr, int) {},
    }
    // .. array, object checks
    propEqual(Calcs.init(), testCalcsClass, 'The constructed object has the expected structure.');
}

propEqual returns true because the properties of both are the same. Calling deepEqual will cause a failure because it checks the properties and the object constructor. Our expected value wasn't created with a constructor like the actual and the test will fail.

Why might this type of check be important?

If your object returns the wrong type of value, propEqual will fail. For example, changing init to a string value in your expected object will fail when compared with Calcs.init() because it's expecting a function, not a string.

Using propEqual on your classes can help prevent type errors down the line by ensuring each property matches the expected type. This kind of check, where you specify an expected structure, is called mocking and we'll look at that in a future post.

Testing Returned Values

What about functions or methods that return structured data? We can use deepEqual to check the returned values. We're going to add a method to Calcs which accepts an array and integer and returns an array with each value increased by the specified amount. Here's the test we'll run:

QUnit.test('Test array calculations', function() {
    equal(Calcs.addArray(2, 2), false, 'Param 1 is not an array');
    equal(Calcs.addArray([1,2,3], 'dog'), false, 'Param 2 is a string');
    deepEqual(Calcs.addArray([1, 2, 3], 2), [3, 4, 5], 'The returned array is correct');
  });

Our test defines three checks that need to pass:

  1. The first parameter is an array,
  2. the second parameter is a number,
  3. and the returned array is equal to the expected value.

Our method needs to accept an array and a number to add to each value in the array. We should get a new array back with the updated values.

const addArray = function(arr, int) {
    // Check the params
    if (!Array.isArray(arr)) { return false }
    if (typeof int !== 'number') { return false }

    var addArr = arr.map(function(val) { return val + int })

    return addArr;
}

return {
    // rest of return
    addArray: addArray
}

If you reload your web app, all tests should pass. This could also be extended with throws to check for custom error messages like we did back in part 4.

Put it into practice

It's easy to get sucked into thinking you need to check for exact data, particularly with Objects and Arrays. With unit testing, remember that you're checking that a piece of your code does what it's designed to do with any data. Running tests on generic structures gives you a clear idea of what any individual part of your application does. Use propEqual to test mocked objects for structure.

Summary

  • equal does a soft comparison (==) and deepEqual uses a strict check (===).
  • deepEqual also checks constructor values for Objects.
  • propEqual compares Object properties (structure) without considering the constructor.

Mutify - A Simple Ad Blocker

We've started using Spotify more around the house with the kids. We only have the free account, which means we hear ads every three or four songs. I don't mind the ads, honestly...they need to make money and I use it without cost to my wallet. That may change someday, but not right now.

The bigger problem with the ads is the fact that they play at 10x volume. Our music is reasonable for the room the speakers are in and then Spotify decides, "Hey, this isn't nearly loud enough. SIGN UP FOR GROCERY DELIVERY! THEN DO THIS SURVEY!"

Kids would cry. Something needed to happen. I told my wife I would write a Chrome extension to block those ads. She laughed and then was confused when I said I was serious. Mutify was born.

Chrome extensions work with three pieces: the manifest, the background script and the content script. Each plays a role in how the extension interacts with the browser and with the pages you visit.

The Manifest

The manifest for this extension is sparse. It loads a couple of icons and, more importantly, defines how the extension can look for ads. To work properly, the extension has permission to interact with tabs and will only run on a URL matching https://*.spotify.com/*. The * is a placeholders, so it will still run if Spotify changes from open.spotify.com to player.spotify.com or even closed.spotify.com. When it sees that URL, the extension will become active.

The Content Script

Content scripts in extensions can see the page you're on but it cannot actually interact with the browser. So, when you're on open.spotify.com a three-line bit of Javascript is run that essentially asks, "Is there an ad playing?" every five seconds. All of the work is done by the background script.

The Background Script

Background scripts are loaded when they're needed and can interact with and change the browser. This is where the work happens. When the background scripts receives the prompt from the content script, it does a quick check of the title of the page.

When an ad plays, the tab title always changes to "Advertisement - Some company" (with the company name changing). So, the background tab just checks for that word in the tab title. If it's there, hey presto, there's an ad playing.

The background script tells Chrome to update the tab status to muted, which cuts the ad out. As soon as the ads are done, the tab title changes back to "Artist - Song" and the extension will unmute the tab.

In all, the code for this little project (not incluing the manifest) was 26 lines of Javascript, including some white space. It isn't published to the Chrome Web Store, so if you want to get a copy, here are the installation instructions.

Two

This little one turns two today.

One week old Mom is the best. Why am I not swinging? Too cool.

Unit Testing in GAS Part 4: Error Handling

If you're brand new to unit testing, start with the first post in this series to get caught up.


Up until now, our Calcs class has handled errors with simple true and false flags. That's not helpful to the user. At this point, we're ready to begin defining and testing custom errors in our functions. In this post, we're going to throw an error when the add() method receives an invalid input. To keep it simple, we're going to call anything other than the number type invalid and return an error.

Get the Source

You can see the completed source for this part on GitHub.

QUnit throws

The ``throws` assertion <https://api.qunitjs.com/assert/throws>`__ is more complex than the ok, equal, notEqual methods we've looked at already. throws will call a function and then can have one of four possible expected params:

  1. An Error object
  2. An Error constructor to use ala errorValue, instanceof, or expectedMatcher
  3. A RegExp that matches (or partially matches) the String representation
  4. A callback function that must return true to pass the assertion check.

With throws, we are able to define not only an error to test, but the kind of error that's returned and even the message received by the test. This is helpful for testing functions that can throw several different types of errors.

We'll start by using the built in Error and TypeError and finish by writing our own CustomError class that you can extend yourself.

Write Failing Tests

To begin, add a new block of tests in tests.gs. Four of these will fail at first and our code will be written to pass each one.

QUnit.test('Checking errors', function() {
    throws(function() { throw "error" }, "throws with an error message only");
    throws(function() { throw new Error }, Error, 'The error was a generic Error');
    throws(function() { throw new CustomError() }, CustomError, 'Creates a new instance of CustomError');
    throws(function() { throw new CustomError("you can't do that!") }, "you can't do that!", "Throws with a specific message");
    throws(function() { throw new CustomError() }, function(err) { return err.toString() === "There was a problem." }, 'When no message is passed, the default message is returned.');
    throws(function() { throw new CustomError("You can't do that.") }, function(err) { return err.toString() === "You can't do that." }, 'Error.toString() matches the expected string.');
  });

When writing your tests, the biggest mistake is that the first parameter must be a function call which throws your error. This is becuase it has to get the returned value to pass to the expected parameter.

When you run your tests by reloading the webapp, the first two assertions will pass because they're handled by the browser. You'll get failures for anything calling CustomError because it doesn't exist yet.

Build the CustomError

We need to create an error called CustomError that does four things:

  1. Raises an instance when called (assertion 3)
  2. Takes a message parameter (assertion 4)
  3. Return the default message if not passed (assertion 5)
  4. Includes a toString method to retrieve the passed message in a callback (assertion 6)

Create a new script file called CustomError and place the following code inside:

var CustomError = function(message) {
    this.message = message || "There was a problem.";
}

CustomError.prototype.toString = function() {
    return this.message;
}

This is scoped globally instead of namespaced (like the Calcs class) because it doesn't access any restricted services in the Apps Script environment. Any class or method can now access and raise this custom error.

If you re-run your test, all assertions should now pass. Now that it is available, we can go back and start using this error in our Calcs class.

Error Handling in the Class

Because the native Error object is always available, we can access those at any point. In calculations.gs, let's not just return false in our function, let's throw a custom TypeError with a message. Our Calcs.add() test block needs to be modified. I'm going to delete a test that no longer applies because we're going to move away from checking with equal. The old line is commented out:

QUnit.test('Checking the `add` method in Calcs', function() {
    ok(Calcs.add(1, 1), 'The method is available and received two variables.');
    equal(Calcs.add(2, 2), 4, 'When 2 and 2 are entered, the function should return 4.');
    // equal(Calcs.add('hello', 2), false, 'When a non-number is added, the function will return false.');
    throws(function() { Calcs.add('foo', 2) }, TypeError, 'When a non-number is passed in the first param, the function will return a TypeError.');
    throws(function() { Calcs.add(2, 'bar') }, CustomError, 'When a non-number is passed in the second param, the function will return a CustomError.');
  });

To pass our tests, we want to update Calcs.add() to throw a TypeError if the first param is not a number and a CustomError if the second is not a number.

Here's a refactored version of the add() method which will pass the test we just wrote:

// ... rest of Calcs
const add = function(a ,b) {
    if(!isNumber(a)) { throw new TypeError }
    if(!isNumber(b)) { throw new CustomError('This deserves a custom message.'); }
    return a + b
}
// ...

This refactor checks a and b independently and throws the specific error to satisfy the assertion statements in the tests. If you run your tests, all assertions should now pass.

The throws method is a powerful tool for testing your exception handling. At a minimum, using the broswer errors can help you give useful information to your users when exceptions occur. throws helps you confidently address each error appropriately before your users run into problems.

Summary

  • throws is an assertion that will raise an error and catch the response for testing.
  • throws expects a function as the first parameter which will raise the error.
  • Define custom errors in the global scope so they can be accessed by all classes and functions.
  • Write a test block for any custom errors you create before adding those raised exceptions to your code.

Unit Testing GAS Part 3: Adding and Testing Functions

If you're brand new to unit testing, start with the first post in this series to get caught up.


We've looked at how to install and configure QUnit and just finished writing some simple tests. In this post, we're going to write a new method, add(a, b) in Calcs which will add the two passed parameters. Then, we'll use testing to check that the params are numbers before returning either the sum or false. We could make this more complex add allow an object (or array), but we'll write another method for that later to compare.

Source

Here is the completed source code for this post.

Write the Test

We know our expected output should be a + b, whatever that happens to be. Let's add a test to tests.gs which will help us write a working function:

function calcTests() {
// ...
    QUnit.test('Checking the `add` method in Calcs', function() {
        ok(Calcs.add(1,1), 'The add method should be available in the module.');
        equal(Cals.add(2, 2), 4, 'When 2 and 2 are entered, the function should return 10.');
    });
}

We added a new QUnit.test() method in the calcTests() wrapper which defines two tests for our new function. ok checks that the function is available (and not accidentally privately scoped) and equal adds 2 and 2 expecting 4 as the result. Running the test now will produce a failure, which is what you would expect because we haven't written the method yet.

Open calculations.gs and add the add method. Don't forget to return it!

// ... rest of code ...
    const add = function(a ,b) {
        return a + b;
    }

    return {
        name: name,
        about: about,
        author: author,
        add: add,
    }
})()

Testing for Errors

We've tried, and passed, two tested conditions, both of which follow the expected use of the function. But, what if a user enters something other than a number? We're going to add a helper function to Calcs which will check a value and return true if it is a number, false if otherwise.

Our function will be called isNumber and here are the tests we'll use for this case:

function calcTests() {
    // .. rest of tests ...

    QUnit.test('Checking the `isNumber` method in Calcs', function() {
        equal(Calcs.isNumber(2), true, 'The value entered is a number');
        equal(Calcs.isNumber('foo'), false, 'The value entered is NOT a number.');
        notEqual(Calcs.isNumber('foo'), true, 'The value entered is NOT a number.');
    });
}

In this block, we introduce the notEqual assertion which will pass if the returned value is false. We expect true in notEqual because I expect Calcs.isNumber('foo') to return false, making the assertion true and passing. (It's a little hard to wrap your head around at first.)

Writing tests first means they will fail whenever the web app is loaded. As you write the function to pass the test, you're keeping code concise and focusing on one (and only one) outcome, thereby improving maintainability and clarity of your codebase.

const Calcs = (function() {
    // ... rest of calcs
    const isNumber = function(val) {
        if(typeof(val) === 'number') {
            return true;
        }
        return false;
    }

    return {
        // ... rest of return
        isNumber: isNumber
    }
})

When writing functions to pass tests, first focus on passing. This function could be restructured to use a ternary or some other method of boolean logic, but that doesn't matter right now. We're just focused on satisfying the test conditions. Then we can go back and refactor.

Running your tests should pass all assertions. If not, go back and look at the failures and debug your code.

Handling Private Functions

In certain cases, not all methods need to be exposed to the global namespace. Our isNumber function could certainly be scoped privately because the Javascript core already includes typing (typeof(2) === 'number' // true) which can handle checking.

Testing private methods is tricky and reasons for why you should or shouldn't vary. In applications which compile code with a build process, there are methods for testing private methods. In Apps Script, there is no such build step, so testing private functions becomes more difficult. Here are some considerations:

  • Why is the function private? If it is performing a necessary task within the class, consider exposing it to the user.
  • Keep private functions simple, like our boolean test, and write tests which require the private function to also pass.
  • Use separate helper classes with utility functions that can be tested separately.

In all, the design of your codebase is up to you. Let testing help you make these decisions. Refactoring is much easier because any change you make should still pass the tests you've already written. For clarity, we'll keep isNumber public for now.

Updating Functions

We haven't updated the add() method yet, which is the ultimate goal. Remember, we want to make sure both parameters entered are numbers before trying to add. To start, let's make sure .add() returns false if a non-number is passed into it. Here's our test block:

QUnit.test('Checking the `add` method in Calcs', function() {
    // ... previous tests ...
    equal(Calcs.add('foo', 2), false, 'When a non-number passed in the first param, the function will return false.');
    equal(Calcs.add(2, 'bar'), false, 'When a non-number is passed in the second param, the function will return false.');
    equal(Calcs.add('foo', 'bar'), false, 'When two non-numbers are passed, the function will return false.');
});

All of these tests may seem redundant, but we want to try and cover each scenario of a non-numer entering our function. Again, writing tests first makes you focus on updating functions to pass. Let's make a change to the add() method which will fulfill that function. Here's our updated method:

const add = function(a ,b) {
    if(isNumber(a) && isNumber(b)) {
        return a + b
    } else {
        return false
    }
}

Refactoring

At this point, we have all of our tests passing and our application will function as intended. You can now go back and refactor knowing that your tests will fail if you break a function somewhere.

Summary

  • Writing tests first helps you solve one - and only one - problem at a time.
  • Passing the test is more important (at first) than writing clever code. Once your test passes, you can go back and refactor with confidence.
  • All new functions (or changes to existing functions) get their own, explicit test.
  • Write multiple tests covering all possible scenarios for failure to make sure you are writing robust and maintainable code.
  • Apps Script does not have a build step, so be careful about adding private functions that are difficult to test.

Unit Testing GAS Part 2: Simple Tests

If you're brand new to unit testing, start with the first post in this series to get caught up.


Simple Tests

From part one, unit tests are for single units of code. They test a specific function for a specific result. I found a helpful living guide on writing unit tests that included some very clear expectations:

Unit tests are isolated and independent of each other. Any given behaviour should be specified in one and only one test. The execution/order of execution of one test cannot affect the others.

Let's create a simple class with some properties and methods we can test. We'll use QUnit to write some tests for those methods. Once we've covered the basics, a future post will look at more complex application structures and tests.

Source

The completed source for this part can be found here.

Writing Functions and Tests

Let's start by defining a Calculations class using Bruce Mcpherson's recommended namespacing structure to keep everything neat. If you're following along, create a Script file named calculations.gs in your editor and add the following code.

const Calcs = (function() {
    const name = 'Calculation class';

    const about = function() {
        return 'A class for calculating things';
    }

    return {
        name: name,
        about: about,
    }
})();

A Note on Naming Tests

Following the testing guide, naming tests clearly is important as their messages will be your guides to problem solving. Each test is given a specific message parameter that has a specific action...should...result format. An named action (calling a class parameter or method) should do something and end in a defined result.

In QUnit for GAS, the result is defined as the expected result in assertions that accept that paramter (keep reading below).

Writing Simple Tests

Now it's time to define some tests. The biggest change in my thinking came when I switched to writing tests first to define what I want the outcome to be before diving in and figuring out if my function is giving me the right output or not. Create a new script file called tests.gs and add the following:

function calcTests() {
    QUnit.test('Checking the Calcs class parameters', function() {
        ok(Calcs.name, 'The name parameter should be available in the namespace.');
        equal(Calcs.about(), 'A class of calculation methods', 'The about method should return the Calcs class description.');
        ok(Calcs.author(), 'The author method should return the Calcs class author description.');
    });
}

Breaking this block down:

  • function calcTests() { ... }: a wrapper which contains several tests. The name is arbitrary, but it should describe what you're testing in general.
  • QUnit.test(name, callback): a method requiring two parameters: a name and a callback function. The callback defines specific assertions (or tests) to run.

Inside the test are the specific assertions we're making about the function:

  • ok(state, [message]): The simplest test that evaluates the truthy/falsy state of the input. The message parameter is optional.
  • equal/notEqual(expected, actual, [message]): Comparisons of expected values with actual returned along with an optional message.

Naming and writing good messaging takes practice and I'm still working on a system that works well for me. The great thing is that if a system isn't working well, just rename it or change the messaging!

The last step before we can run tests is to tell QUnit where to look for those tests in the config file we defined in part one. Open your config.gs file and make sure it looks like this (excluding comments):

QUnit.helpers( this );

// Define the tests to run. Each function is a collection of tests.
function tests() {
  console = Logger; // Match JS
  calcTests();   // Our new tests defined in tests.gs
}

// runs inside a web app, results displayed in HTML.
function doGet( e ) {
  QUnit.urlParams( e.parameter );
  QUnit.config({
    title: "QUnit for GAS" // Sets the title of the test page.
  });

  // Pass the tests() wrapper function with our defined
  // tests into QUnit for testing
  QUnit.load( tests );

  // Return the web app HTML
  return QUnit.getHtml();
};

What's happening:

  • calcTests(), our tests function, is included in the tests() wrapper function in the QUnit config (line 8).
  • tests() is loaded into QUnit with QUnit.load(tests) (line 20)

Running Tests

QUnit is run as a web application through apps script. Go to Publish and choose Deploy as web app.... In the popup, set the new version and limit access to yourself.

You'll need to verify the application can have access to your account. Once that is done, you can open your web application link. If you've done your setup correctly, you should see your three test results:

Test results from the QUnit web app.

You just ran your first unit tests!

Failing a Test

There are plenty of ways to write failing tests. They fail either because your code doesn't produce the expected value or because your test is expecting something that isn't happening. Let's make a small change to our Calcs class which will cause a test to fail.

In the class, change the .about method to:

const about = function() {
    return 'A class of calculation method';
  }

Since our test is asserting that this function will return the string, A class of calculation methods, we can expect this test to fail because it will evaluate to false. Run your tests again either by reloading the web app page. Sure enough, we have a failure:

A failed test in QUnit

There are a couple things to note from this result:

  1. The expected result is defined in your test function.
  2. The actual result and the difference are shown so you can identify the point of failure (and yes, your tests can be the point of failure!)

Since the .about() method fails its test, I know I need to go back and fix the bug. Adding an 's' to 'method' solves the bug. Reloading the page will confirm with a passed test.

Stack traces in QUnit for GAS are marginally helpful. This is because the testing happens on Googles servers, not your computer, so there are several steps in the tooling that add layers of trace data. Some ways to make this more readable are to add code references to your tests file or to have function-based naming so you can find what failed. For this example, we don't have to worry too much, but we'll look into more complex applications at a later point.

Changing Your Code

The whole point of unit testing is that you catch breaking changes before your code is released. Let's make a change to our Calcs class and write a test to make sure that nothing is broken. Start by writing a simple test to define what we want that function to do.

// tests.gs
QUnit.test('About Calcs test', function() {
    ...
    ok(Calcs.author(), 'The author method is publicly available');
    ...
})

...and then add the function to Calcs which will pass the test.

// calculations.gs
const Calcs = (function() {
    // ...
    const author = function() {
        return 'This ' + name + 'is authored by Brian.'
    }
    ...
})

Reload your web app page. What happens?

Your test should have failed (if you followed my code above) with the error, Cannot find function author in object [object Object]. But why?

Something is wrong...the test couldn't find the function author() even though I added it to my class. The explanation is that I never exported that function in the return statement! Since it wasn't exported, the test fails. A potential bug in my application has been caught early and is simple to diagnose and repair before it causes user errors later. Update the return statement in the calculations class to:

// calculations.gs
...
return {
    name: name,
    about: about,
    author: author,
}
...

...and run the tests again by reloading the web app to see that everything now passes.

Summary

This is the first glimpse into using QUnit inside an Apps Script project. Once the setup is complete, you can start writing tests for what you expect your code to do, which gives you clarity and insight into actually writing the function while knowing your test will catch bugs.

  • Tests are grouped into wrapper functions, usually by similarity in purpose.
  • Specific tests are run with the QUnit.test() method which takes two parameters:
    1. A title for the tests
    2. A callback function defining each type of test
  • Tests are passed into a tests() wrapper function in the config file.
  • The tests() wrapper is passed into QUnit.load() to run in a web app.
  • ok, equal, and notEqual are simple checks for true/false results when the expected and actual results are compared.

Unit Testing GAS Part 1: QUnit Setup

I'm not good at writing testable code. I'm more of a 'figure it out when it breaks' kind of hobby programmer. The problem with this is that I am constantly making my own bugs and not really finding them until a bad time.

Unit testing is the process of running automated tests against your code to make sure it's working correctly. Each test is for one unit of code - a single function, usually. It expects a value and will pass or fail based on the value received as part of the test.

To get better, I forced myself to write unit tests in Google Apps Script for two reasons:

  1. I've been writing a lot of Apps Script code lately,
  2. There are not many good methods for unit testing in GAS.

This series

The point of this series is to force myself to learn, and use, a unit testing method when writing code and to update the far outdataed unit testing tutorials for Apps Script published online already. I've tried several testing libraries but will be using QUnit as the testing suite.

I'm following Miguel Grinberg's method of posting tutorial code as tagged versions of a GitHub project. Each post will link to a specific tag with the completed source code for that section.

Here's the source for this post

Now, for large projects, you could argue that using clasp and a traditional unit testing library like Mocha or Jasmine is preferable, and you might be right. But, for the purposes of learning, I wanted to keep everything as 'pure' as I could, so all files and tests are written and tests in the online apps script editor.

What is QUint?

It's a testing framework developed and maintained by the jQuery Foundation. It is used in jQuery development to make sure things don't self destruct as the library expands.

QUnit is written for Javascript. Because GAS is based on Javascript, there is a handy library which can be installed in your apps script project.

When testing on your local computer, tests are run by your machine. With Apps Script, everything is run on Google's servers. The QUnit library exposes the framework through a web app that fetches the framework code and executes it when the web app loads.

Install

You can install QUnit for apps script by going to Resources > Libraries in the editor and searching for MxL38OxqIK-B73jyDTvCe-OBao7QLBR4j in the key field. Select v4 and save. Now the QUnit object is available in your project.

Setup

The QUnit library needs some configuration to work with an apps script project. There are three parts to the setup: 1) Declaring QUnit at the global scope, 2) defining tests, and 3) configuring the web application to run the tests.

1. Instantiate QUnit

Once the library is loaded, it needs to be instantiated at the global level to run. Create a new script file called config.gs to hold all of your QUnit code.

The first line should be:

QUnit.helpers(this);

This exposes all assertion methods in the QUnit library (ok, notEqual, expect, etc.) instead of a pared-down object.

2. Define Tests

Tests are defined within wrapper functions that can be passed into QUnit. This tests function will simply hold a list of tests to run when the web application is loaded. We won't be writing any tests in this post but go ahead and add a wrapper for populate later.

function tests() {
    console = Logger; // Match JS
    // Test definitions will be added here
}

3. Web App Config

TheQUnit.config() object declares settings for the web app, so it gets wrapped in the doGet() function. URL params are used to pass information from the app to the testing library with QUnit.urlParams().

QUnit also has a config object which can set default behaviors. You can see a full config object in the project source. For this simple setup, all I'm going to declare is the web app title. Add this to your config.gs file:

// Updated Feb 2020 to account for V8 runtime
function doGet( e ) {
    var params = JSON.stringify(e);
    return HtmlService.createHtmlOutput(params);
};

Now you're ready to write some code. Running QUnit right now won't do anything; that will come in part 2.

Summary

  • QUnit is a testing library developed by the jQuery foundation.
  • Google Apps Script is Javascript-like, so a JS testing library can be modified to test Apps Script projects.
  • QUnit for Google Apps Script is a library which can be used in the online Apps Script editor.
  • It runs with a web app and is defined by a doGet method and a config object.

Comments

Four

Our second daughter turns four today.

New baby sister About 5 months old. With her mom camping in the UP. Out on a walk

Re-writing DocuTube

tl;dr I have a Google Apps Script project getting a major overhaul. If you want to look at the code and contribute, it's on GitHub.

A couple years back, I published a little addon which would scan a Google Doc for linked YouTube videos and allow you to watch them in a popup or sidebar. I called it DocuTube and published without much more thought.

Since writing that app, I've learned a ton more and decided to give it a major overhaul. It was mediocre on the web store with some valid complaints about a lack of clarity and functionality.

As I added functions, I took my time to figure out better ways to structure my code. I followed Bruce McPherson's wonderful advice to add namespacing (isolating functions from one another) to help keep everything tidy. It bent my brain into pretzels, but it was so good to wrestle though. I now have an application that is more manageable and extensible because separate parts are sequestered from one another.

This month, I published an update which adds search, video previews, automatic embedding, and cleans up video playback.

Video Previews

Since searching was included, I wanted to be able to provide a way to actually check and make sure the video clicked was the one the user wanted to actually embed.

https://blog.ohheybrian.com/wp-content/uploads/2019/11/docutubeSearch2.png

Clicking on a video gives a playable iframe for a preview. The user can then choose how to attach the video to the document: copy the link to the clipboard (manual paste), insert the thumbnail, or add some custom text. All of this is done with the cached resource so the API isn't hit again to get the link or thumbnail.

Embeds

Google Docs doesn't include an "embed" in the traditional sense. When I use the term embed in the context of DocuTube, I mean it handles the link for you. Inserting a video as a thumbnail grabs the title image, throws it in the document, and then adds the link. You can certainly do the same thing manually with several tabs and clicks.

I think "embed" is an okay term because it leads into the other function of DocuTube: watching videos.

Watch

This hasn't changed a whole lot. The major update from v0.7 (the current version) to v1.0 is that video watching is inclusive. Prior to 1.0, you needed to choose where to pull links from: the document or the comments.

With the 1.0 update, all videos linked in the document, regardless of location, are added to the Watch sidebar. This also removes the option of watching videos in a popup because the whole point of including the video as an embedded item is not leaving the document. If videos are loaded in a popup that takes up the entire editor, I've essentially kicked you out of the document.

https://blog.ohheybrian.com/wp-content/uploads/2019/11/docutubeWatch1.png

If you want to give it a try, you can search from the Docs Addons menu or install it from the GSuite Marketplace. Issues can be sent to brian@ohheybrian.com, posted as an issue on the code itself.

Auto-select form checkbox with querystrings

I maintain a website at school where teachers can register for professional development. They can see all offerings and save sessions to their profile for remidners, etc.

The bulk of the data comes through Javascript, which populates the main page in a fancily-styled form. Each course is a form element with a unique ID that is passed to the backend when they register. The database stores the user ID and the course ID in a couple different places to manage all the reminders.

https://blog.ohheybrian.com/wp-content/uploads/2019/11/2019-11-18_12-55-13.png

Because each course is a JSON object, you cannot just send a link for a specific course, which is a problem when you sometimes have dozens on the screen. That means they're either having to remember to search (which matches titles) or scroll until they find the right session. Coordinating a group of people becomes difficult.

Since each of my form elements has a specific ID, I decided to use a URL query to automatically select the correct element. I can also build a specific URL for each session and add it to the element when it's built on the page. Double win. Here's how it works.

The URL

You can call window.location.search to pull any query parameters from a URL (anything after a ? character). The browser also has a handy URLSearchParams function that allows you use methods like .has() and .get() to make them more accessible in the code. With some Javascript, I can pull the keys from the URL when the page loads and then kick off another action.

I want to pass a specific course ID to the querystring for the user. So, my URL becomes:

https://mysite.com/?course=abc123

Acting on the query

The normal page load scripts didn't need to change much. I added a quick conditional to check if the URL included a query string with the .has() method I mentioned above. I can specify which key to look for, which makes extending this function easier in the future.

if (urlParams.has('course')) {
        // If the query matches the course being built, select the input
        if (urlParams.get('course') === course.key) {
            document.querySelector(`input[name='course'][value='${course.key}']`).checked = true;

            // The page can get long, so I focus the window on the specific course.
            window.location.hash = `#${course.key}`;

            // set the submit badge quantity
            loadSubmitBadge();

        }
    }

If a query is passed in the URL, that course gets a checkbox pre-selected and focused on the screen for the user. They're free to either select more courses or just hit sbumit and be on their way.

The Bennett Family Sing Along

A few times a week, we sing hymns with the kids before bed. I get the guitar out and we practice their favorites. The lyrics are Bible-based truth and my kids have always done great with song as a memory tool. They can probably sing more of a hymn from memory than I can at this point, to be honest. (If you're interested, here's a great list to start with from The Gospel Coalition.)

Part of that time is "silly time" where they make up a situation and I think up a song on the spot. Most situations have to do with an unfortuntate encounter between two incompatible animal species (the alligator climbed the tree and ate the raccoon, for instance). Other times, it's about the dog (Jo's Song) or the baby (I get back to my metal days and we all scream).

But there's also The Monkey Took His Banana to the Water, which is a regular in the rotation. It's got a Johnny Karate vibe and will stick in your ear for days.

The Monkey Took His Banana to the Water

195 BPM

Verse

Oh the monkey went down to the water
And he took his banana with him to the water
When he came out of the water, his banana was wet

Chorus

It was wet wet wet
It was wet wet wet
It was wet wet wet
It was wet wet wet

Repeat V1

Instant classic.


The featured image is my own from 2013. I only had one daughter at that time and my wife's cousins would come over to play guitar together. This living room has had a lot of music in it since we moved in.

Mask Text in Keynote on the iPad

Keynote has image masking built in. Masking allows you to more or less shape a picture in a frame. A simple example would be showing a portion of a photo in a circle rather than as a square.

This is easy to do and can help make a presentation look a little more polished.

A more advanced version is masking an image with text. Here's a great example of this technique:

A promotion poster for Solo: A Star Wars Story

(Fun side note: Disney was hit with a copyright suit for this string of promo posters.)

You can't do this in Keynote on iOS, though. It's not part of the text formatting settings you would need.

Mask text in Keynote

You can't mask text natively in Keynote. But, you can use an image of text whipped up in Pages (or similar) to create the same effect. Here's the final result:

The word 'loud' with a boy yelling into the microphone showing through.

First, make some big, bold text. I did this in Pages because the font choices are easier to use. When you have your word, take a screen shot and crop it down.

The word 'loud' in block letters.

Add your base image and the text to the Keynote slide with the text on top. The, select the text screenshot and use Instant Alpha in the format menu to remove the inside of the letters.

A screenshot of Keynote on the iPad. Tap the Format menu and use instant alpha to remove the color from the text.

After removing your text, you should be able to see your image in the empty space. Crop the image down (double-tap) so it's the same size as the text layer.

Finally, it's a good idea to lock the text layer and the image together in a Group so they can be positioned as a single object. Tap your text and while you're holding, tap the image in the transparent area. This will select both objects and bring up a menu. Select Group to lock them together.

A screenshot of Keynote on iOS. Tap both images at the same time to group them together.

Hey presto, you now have a masked image.

Just don't land in hot water like Disney.


The original image is "Boy Singing on Microphone" by Jason Rosewell on Unsplash.

Comments

Change Document Ownership with Apps Script

My Google account is managing a lot of document creation at school. I set up a lot of autoCrat projects to create docs alongside several utility scripts that pump out Docs on a regular basis.

The problem is, I don't want to be the owner of all of these documents. Using some Apps Script, I can set a trigger to automatically set a new document owner so the files are out of my account.

Notes

We use autoCrat for document creation, which has a consistent spreadsheet structure. When setting up the form to create documents, make sure you do the following:

  1. When autoCrat is set up, set the Merged Doc ID column in line 6 of the script.
  2. Include the user email address in the form response. Set the column ID with the email in line 7 of the script.

There's a check in the loop that makes sure the emailCol actually gives an email address. If it's not a valid email address, the row will be skipped. This shouldn't cause some rows to complete and others to fail because the entire column is checked.

You can run the script manually from the script editor and it will loop the sheet, setting the doc owner as the person who submitted the form. I set it to run daily with a trigger so I don't have to manage these long-running tasks.

This doesn't have to be used with autoCrat, either. All it needs is the ID of a document and the email address of the person to set as the owner. As long as you have that information, you can set this to run and help keep your Drive from becoming everyone's filing cabinet.

The Great YouTube Migration

It's begun.

This summer, YouTube deactivated brand accounts for GSuite Education domains. A brand account is essentially a shared account for teams. There's no single Google account associated and it can be used for branded content. We set up a branded page when our team first started so we could each upload to the channel without having a shared Google account.

Well, those days are gone. While I understand the reasoning (and I actually agree with the reasoning), Google really borked the process by not providing a migration strategy. There is no way to take videos associated with a brand channel and automatically associate them with another. We were able to get our channel activated, but we cannot easily move videos to a new, shared Google account for our team.

Download with youtube-dl

This is where youtube-dl comes into play. It's a command line utility that downloads YouTube videos based on specific video IDs, playlist IDs, or channel URLs. It's awesome.

I'm not the only one who has looked for easy ways to download entire channels. Ask Ubuntu has a great answer for how to download videos for an entire channel. It's a one-liner that just runs in the background, writing files to a folder. Adding the --write-description flag to the command also automatically creates a file for the video description to make the copy/paste easier later.

Reuploading - this is a nightmare

Downloading is easy, but with no thanks to Google on that solution. Uploading is even harder.

YouTube does have an API that would allow me to write a little loop to upload videos in the background. But, it is tied to a quota for the user. The standard is 10,000 units/day, which sounds like a lot, until you look at the cost for each operation.

According to the quota calculator provided, each upload costs ~1600 units. That's an upload and description written via the API, which is bare minimum. We have 102 videos, which means it would take me 17 days to automate the uploads.

17 days.

The alternative is to manually sit and upload each video to the new channel. It isn't 17 days, but it's a wasted day, for sure.

Here's where we gripe about Google killing things without providing viable alternatives for users.

But remember, we're not users to Google. We're products.

Products don't get a say in how we're used by the corporation.

See you in 17 days.