Low-Cost, DIY Ladder Fall Protection

Last week, I had solar panels installed on my roof. The installers secured their ladder to my roof / eaves using the Guardian Fall Protection “Ladder Stability Anchor”.

The way it works is you clamp the clamp onto your eaves, secure the hooks to the hole on the clamp, then wrap the straps around your ladder, securing it using the hook and loop velcro.

It’s a simple idea, but the price is $90. It just went on sale for $56, but that still seems overpriced. Not sure why Harbor Freight hasn’t created a cheaper alternative.

Anyway, you can probably make your own for much less by just using

  1. a C-clamp ($5)
  2. a strap: ratchet strap ($10) or cam buckle strap ($10)
C-clamp
Ratchet strap
Cam buckle strap

For an easy-to-transport ladder, this telescoping ladder ($160) is great.

If you need this for your own home and there is one spot you would always use to get on the roof, and if you want a more secure solution, replace the C-clamp with a large screw eye or eye bolt with nut and use the ratchet straps instead of the cam buckle strap.

screw eye
eye bolt with nut
I just used one cam buckle strap.

Programmatically Migrate a Website From Handlebars to Nunjucks

I’m currently migrating a large website from Handlebars to Nunjucks. Since the website is being updated daily, and because there are too many pages, I can’t convert the Handlebars syntax to Nunjucks syntax manually. To solve this, I started writing a script to convert the syntax programmatically using JavaScript (nodeJS). So far, it’s working very well. Here’s how I’m doing it, and how you can do something similar when confronted with a migration project.

Basically, the way it works is

  1. it recursively finds all files in a folder called “temp”
  2. if the file path ends with “hbs” – indicating it is a Handlebars file – then for each file, it executes a series of regex search and replace commands, e.g.
    • replace {{#if class}} with {% if class %}
    • replace {{/if}} with {% endif %}
    • and so on.

Those are simple search-and-replace situations. There may be a situation where you’ll need an advanced search and replace, e.g. when replacing

{{> social-list 
    dark="true" 
    centered="true"}} 

with

{% set dark="true" %}
{% set centered="true" %}
{% include social-list.njk %}

In this case, you can use a “replacer” function, which allows you to do much more to manipulate the output.

If you need to test regular expressions, https://regexr.com/ may come in handy.

When you’re all done and you’ve built the HTML files from both the handlebars templates and the nunjucks templates, you can write a script that recursively reads all HTML files in the build output folder and lists each HTML file path generated from each handlebars and nunjucks template along with their respective file size. The file sizes should be the same or almost the same. If some are not, then the migration script didn’t convert those templates correctly. Maybe something like:

File PathFile Size (KB) (hbs)File size (KB) (njk)Difference (KB)
/index.html181.3180.70.6
/about/index.html153.223.5129.7
/products/index.html353.6350.23.4

Dutch Hagelslag: Low-Calorie DIY Donut

The average sprinkle donut has 260 calories.

For a similar low-calorie alternative, replace that with Dutch Hagelslag. The recipe below contains only 120 calories!

Ingredients

Instructions

  1. Spread butter on one slice of bread
  2. Top with sprinkles
  3. Eat

Compare

If you must have the shape be like a donut, you can use a large and small cookie cutter to turn the bread slice into a donut.

US Federal Taxation

This post explains at a high level how taxation in the US works. This article only talks about federal taxes, not state taxes.

Income

There are 2 types of income

  1. Earned income, e.g.
    • salary, wages and compensation
    • appreciated assets that have been sold during the tax year
  2. Unearned income, e.g.
    • canceled debts,
    • government benefits (such as unemployment benefits and disability payments),
    • strike benefits,
    • lottery payments,
    • interest,
    • dividends,
    • rents,
    • capital gains

Filing Status

When you file your taxes, you can choose from one of the following, depending on your situation:

  • single
  • head of household
  • married filing jointly
  • married filing separately

Ordinary Income Tax Rates By Income (2023)

Filing status = single

IncomeMarginal Tax Rate / Bracket
Up to $11,00010%
$11,000+ to $44,72512%
$44,725+ to $95,37522%
$95,375+ to $182,10024%
$182,100+ to $231,25032%
$231,250+ to $578,12535%
Over $578,12537%

Ordinary Income Tax Rates By Income (2023)

Filing status = married filing jointly

IncomeMarginal Tax Rate / Bracket
Up to $22,00010%
$22,000+ to $89,45012%
$89,450+ to $190,75022%
$190,750+ to $364,20024%
$364,200+ to $462,50032%
$462,500+ to $693,75035%
Over $693,75037%

For brevity, I omitted other tax filing statuses, including

  • head of household
  • married filing separately

The tax rates are also called “tax brackets” and “marginal tax” rates.

Historical US income tax rate brackets

Income from Capital Gains

  • Capital gains is the profit you make from selling a capital asset.
  • Capital assets include stocks, bonds, precious metals, jewelry, art, and real estate.
  • Selling a capital asset after owning it for one year or less results in a short-term capital gain.
  • Selling a capital asset after owning it for more than one year results in a long-term capital gain.
  • Net capital gains are calculated based on your adjusted basis in an asset. This is the amount that you paid to acquire the asset, less depreciation, plus any costs that you incurred during the sale of the asset and the costs of any improvements that you made. 
  • Short-term capital gains are taxed as ordinary income from your salary or wages.
  • Long-term capital gains are subject to a tax of 0%, 15%, or 20% (depending on your income).

Short-Term Capital Gains Tax Rates By Income (2023)

Filing status = single

IncomeShort-Term Tax Rate
(same as ordinary income tax rate)
Up to $11,00010%
$11,000+ to $44,72512%
$44,725+ to $95,37522%
$95,375+ to $182,10024%
$182,100+ to $231,25032%
$231,250+ to $578,12535%
Over $578,12537%

Filing status = married filing jointly

IncomeShort-Term Tax Rate
(same as ordinary income tax rate)
Up to $22,00010%
$22,000+ to $89,45012%
$89,450+ to $190,75022%
$190,750+ to $364,20024%
$364,200+ to $462,50032%
$462,500+ to $693,75035%
Over $693,75037%

If you have $90,000 in taxable income from your salary and $10,000 from short-term investments, then your total taxable income is $100,000.

Long-Term Capital Gains Tax Rates By Income (2023)

Filing status = single

IncomeLong-Term Tax Rate
Up to $44,6250%
$44,626 to $492,30015%
Over $492,30020%

Filing status = married filing jointly

IncomeLong-Term Tax Rate
Up to $89,2500%
$89,251 to $553,85015%
Over $553,85020%

Taxable Income

  • Taxable income is the portion of your gross income that the IRS deems subject to taxes.
  • It consists of both earned income and unearned income.
  • Taxable income is generally less than adjusted gross income because of deductions that reduce it.
  • Taxable income = Gross income – deductions
  • For a business, revenue – business expenses = profit. Profit – deductions = taxable income.

Deductions

The IRS offers individual tax filers the option to claim the standard deduction or a list of itemized deductions.

Standard Deduction

The standard deduction is a set amount that tax filers can claim if they don’t have enough itemized deductions to claim. For the 2022 tax year, individual tax filers can claim a $12,950 standard deduction ($13,850 for 2023). If you are married filing jointly, the standard deduction is $25,900 ($27,700 for 2023).

Itemized Deductions

If you plan to itemize deductions rather than take the standard deduction, these are the records most commonly needed:

  • Property taxes and mortgage interest paid (form 1098)
  • State and local taxes paid (this is on form W-2 if you work for an employer)
  • Charitable donations
  • Educational expenses
  • Unreimbursed medical bills
  • Documents related to operating a rental property, such as receipts for repairs, advertising, etc. Learn more.

Learn more about deductions

Tax Credits

A tax credit will lower your tax liability (any taxes you owe). If you don’t owe any taxes, then you may or may not get a refund, depending on the tax credit details. Some tax credit examples are

  • 30% credit off the total cost of a solar panel installation
  • $7,500 tax credit when you buy a qualifying electric vehicle

Learn more about tax credits

Refundable vs. Non-refundable Tax Credits

Refundable tax credit: If your tax credit is refundable, then even if you have no tax liability, e.g. if you are retired, then you will still get a refund for the entire tax credit amount.

Non-refundable tax credit: If your tax credit is non-refundable, then you will only get the full credit if your tax liability is at least as much as the tax credit. For example, if your tax credit is $1000 and your total tax liability before applying the credit is $1500, then your updated tax liability is reduced to $500. However, if your total tax liability before applying the credit is $800, then your updated tax liability is reduced to $0 and you will NOT get a refund for $200. For that reason, you should ensure your tax liability is at least as much as the tax credit you want to apply.

How to Calculate Taxable Income

  1. Determine Your Filing Status
    • single,
    • married filing jointly,
    • etc
  2. Gather Documents for all Sources of Income
    • form W-2 for earned compensation
    • form 1099-INT for interest income
    • etc
  3. Calculate Your Adjusted Gross Income (AGI)
    Your AGI is the result of taking certain “above-the-line” adjustments to your gross income, such as contributions to a qualifying individual retirement account (IRA), student loan interest, and certain education expenses. These items are referred to as “above the line” because they reduce your income before taking any allowable itemized deductions or standard deductions.
  4. Calculate Your Deductions (Standard or Itemized)
  5. Calculate Taxable Income
    Taxable income = AGI – deductions

Marginal vs. Effective Tax Rates

The US has a progressive tax system, so your income is taxed at different rates. If your total annual income is $125,000 and your taxable income is $100,000 (income minus deductions and credits) and you are filing single, your tax liability would not be 24% of the entire $100,000. Instead, it would be $17,400. In this case, your “effective” or “average” tax rate is $17,400/$125,000 = 13.9%, which is much lower than 24%.

Short-Term IncomeMarginal Tax RateTax Liability ($)
Up to $11,00010%$11,000 x 10% = $1,100
$11,000+ to $44,72512%($44,725 – $11,000) x 12% = $4,047
$44,725+ to $95,37522%($95,375 – $44,725) x 22% = $11,143
$95,375+ to $182,10024%($100,000 – $95,375) x 24% = $1,110
$182,100+ to $231,25032%N/A
$231,250+ to $578,12535%N/A
Over $578,12537%N/A
Total$17,400

How to Clean Upholstery

If your car seats, floor mats and furniture are made of upholstery, they will eventually get dirty. Fortunately, cleaning them isn’t too hard if you use the right tools and technique. I recently cleaned an old car seat and floor mats. The process was simpler than I thought. Here’s a before and after pic.

And here’s how to do it.

Equipment

I have the Bissel SpotClean Pro.

I also rented the Karcher Carpet Cleaner and Detailer from Home Depot to compare it to the Bissel.

Both of these tools have 2 functions;

  1. Squirter: to squirt water or cleaning solution
  2. Extractor: to suck water from upholstery

You could also just use a spray bottle to squirt the cleaning solution.

You can also use a wet shop vac, but it’s helpful to have a hose head that is transparent so you can see if any water is left in the upholstery. Here’s a transparent extractor nozzle designed to fit all shop vacs.

The Bissel nozzle includes a brush, but it’s better to use a drill brush. For the drill brush, I went with this one on Amazon. Make sure to choose brushes designed for upholstery.

This one includes 4 items:

  • 2″ brush
  • 3.5″ brush
  • 4″ brush
  • Drill extension

Instructions

  1. Add the cleaning solution to the carpet cleaner tool.
  2. Spray the cleaning solution onto the item to be cleaned.
  3. Use a drill brush to agitate the soiled upholstery. I find that using the 3.5″ brush sideways does a better job at agitating upholstery.
  4. Suck any moisture using the carpet cleaner tool.
I added a cleaning solution to the carpet cleaner tool to spray the solution onto the upholstery.
I also bought and applied a upholstery cleaning foam.
The 4″ brush is also effective in agitating the soiled fabric.
Here’s a stain that was actually easy to remove.
This time, I use the 3.5″ brush sideways.
After sucking the moisture, the stains were gone.

When cleaning upholstery in this way, not a lot of water is needed. After sucking the water, very little is left, so it doesn’t take long for the material to dry.

This is the nozzle on the Bissel.
This is the nozzle on the Karcher.

Should You Sell Your House & Buy a New One Within 10 Years?

According to RocketMortgage, the average mortgage term is 30 years and the average length is under 10 years. This is because homeowners will either refinance their home, like I did to get a much lower interest rate, or because they want to move. Refinancing your mortgage to get a lower interest rate makes sense if the new rate will be much lower such that you’ll end up saving money. You’ll just have to keep in mind that if you’ve had your mortgage for 10 years, for example, and you refinance, the clock resets and you’ll have 30 years to pay off your mortgage instead of 20. For this reason, I personally continue to pay the same monthly payments at the higher interest rate so that I pay less interest and the mortgage is paid off sooner.

But what if you sell your home to buy a new one within 10 years? What many people may not realize is that by doing this, they will lose a lot of money because their home loan is an amortized loan rather than a simple interest loan. Amortized loans favor lenders, like banks, instead of borrowers. Unlike a simple interest loan, where you’re paying the same amount towards principal and interest each month, when you get a mortgage, most of your monthly payments go towards interest in the beginning and less near the end of the 30-year term.

Consider an amortization schedule for this loan:

Loan Amount$400,000
Interest rate6.731%
Loan Term30 years
Total principal payments$400,000
Total interest payments$532,165

Note that your total interest payments over 30 years is more than the loan amount.

Let’s take a few points in time and compare how much of your monthly payment goes towards principal and interest.

PrincipalInterestPercent Towards Interest
First mortgage payment$346$224486.64%
Mortgage payment at month 100 (8.3 years)$605$198576.64%
Mortgage payment at month 200 (16.7 years)$1058$153159.13%
Mortgage payment at month 236 (19.7 years)$1294$129550%
Mortgage payment at month 300 (25 years)$1851$73828.51%
Mortgage payment at month 360 (30 years)$2578$140.54%

As you can see, in the first 10 years of your mortgage, the bulk of your monthly payments goes towards paying interest. Your equity from paying down the principal is very little. Therefore, if you sell your house within the first 10 years and buy a new one, you’ll have little equity from your mortgage payments and, when you get a new mortgage for your new home, you’ll start over from month 1, when most of your new monthly payments will go towards interest again.

Of course, your house could appreciate significantly in 10 years, in which case the equity you gain from appreciation could outweigh the equity from paying off the principal. However, that is not always the case.

If you’re planning on selling your home within 10 years and buying a new one, it may not be worth it since you may lose a lot of money from having mostly just paid interest.

Santa Cruz, Monterey, Pebble Beach, Carmel-by-the-Sea, Hearst Castle

A collection of points of interest south of San Francisco

*San Jose – Santa Cruz Highway 17 (Scenic)
*Monterey / Fisherman’s Wharf / Cannery Row
Cannery Row
*El Torito (Mexican)

Google Maps

*Crepes on the Row

Google Maps

*Pebble Beach / 17-Mile Drive
*Carmel-by-the-Sea
*California Highway 1 Scenic Route / Pacific / Gold Coast

WARNING: Check for road closures

*Hearst Castle

San Francisco Sightseeing Itinerary

This itinerary assumes a starting point in Foster City, California near Qualys headquarters. It does not include every single tourist destination, but it does include many popular ones. This particular itinerary includes brunch and dinner at restaurants based on personal preference. The itinerary was designed to make a loop to avoid backtracking as much as possible.

*Twin Peaks
*Twin Peaks to Painted Ladies

Google Maps Directions

*Alamo Square / Painted Ladies
*Alamo Square / Painted Ladies to Haight-Ashbury

Google Maps Directions

*Haight-Ashbury: Former 1960s Hippie Neighborhood
Four Seasons houses1315 Waller Street
Victorian houses
1660 Haight Street
*1428 HAIGHT Patio Cafe & Crepery
Eggs Benedict: Grilled Canadian Bacon, Two Poached Eggs, Crispy English muffins & House-made Tomatillo Hollandaise Sauce. Served with Rosemary Garlic Potatoes or Fresh Fruit
Eggs Florentine: Sautéed Spinach, Roasted Tomatoes, Crispy English muffins, Two Poached Eggs, Topped With House-made Tomatillo Hollandaise Sauce. Served with Rosemary Garlic Potatoes or Fresh Fruit
Hash on Haight: Corned Beef brisket, sauteed peppers, onions, Rosemary Garlic Potatoes & 2 eggs any style topped with house-made tomatillo hollandaise sauce. Served with a side of Fresh Fruit
Philly Cheese Steak
Funky Monkey Crepe: Bananas, brown sugar, Nutella, ice cream
Berrylicious Crepe

Tiramisu Crepe

Basic Dessert Crepe
*Haight-Ashbury to Golden Gate Park

Google Maps Directions

*Golden Gate Park

The equivalent of Central Park in New York

Conservatory of Flowers
*Conservatory of Flowers to Murphy Windmill

Google Maps Directions

*Ocean Beach
*Ocean Beach / Murphy Windmill to Lands End Lookout

Google Maps Directions

*Lands End Lookout
*Lands End Lookout to Legion of Honor

Google Maps Directions

*Legion of Honor
*Legion of Honor to Golden Gate Bridge (South Viewpoint)

Google Maps Directions

*Golden Gate Bridge
*Golden Gate Bridge (South Viewpoint) to H. Dana Bowers Rest Area & Vista Point

Google Maps Directions

*H. Dana Bowers Rest Area & Vista Point to Golden Gate View Point

Google Maps Directions

*Golden Gate View Point to Palace of Fine Arts

Google Maps Directions

*Palace of Fine Arts
*Palace of Fine Arts to Lombard Street Along the Marina

Google Maps Directions

The Marina
The Marina
*Lombard Street

*Lombard Street to Fisherman’s Wharf

Google Maps Directions

*Fisherman’s Wharf
*Pier 39
*Pier 39 to the Ferry Building

Google Maps Directions

*Ferry Building
*Ferry Building to Chinatown

Google Maps Directions

*Chinatown
*Chinatown to Union Square (Drive by Transamerica Building)

Google Maps Directions

*Union Square
*Cable Cars
*Union Square to Shalimar Restaurant

Google Maps Directions

*Shalimar Restaurant
Tandoori Chicken
Garlic Naan
Chicken Tikka Masala
Daal Masala
*Shalimar Restaurant to City Hall

Google Maps Directions

*City Hall
*City Hall to Treasure Island via Bay Bridge

Google Maps Directions

*Treasure Island & Bay Bridge
*Treasure Island to El Torito

Google Maps Directions

*El Torito
*El Torito to Foster City

Google Maps Directions

*San Mateo-Hayward Bridge

Moom: MacOS Window Management with Instant Arrangements

With so many people working both from home and at the office, it can become annoying to have to rearrange your application windows when you move between the two locations. This is especially true for people like me who need multiple monitors, two of which are 32″ 4K ones as shown below, which I need to display multiple windows on each screen.

Though I have a similar setup at home, my application windows always get jumbled up when I move between locations, possibly because the standalone monitors are not all the same brand with the same exact resolution.

Most window management apps allow you to move and resize windows in a grid, e.g.

  • left 50% of screen,
  • bottom 50% of screen,
  • right 33% of screen,
  • top 50%, left 50% of screen,
  • etc

These are fine if you aren’t going to move locations often and don’t have too many windows. If you want the same layout spanning multiple monitors and the ability to instantly move and resize all windows to that layout, then I recommend Moom. Here’s how to use Moom to save layouts for multiple monitor configurations.

  1. At location 1, e.g. work, open your applications and arrange them how you like
  2. Open Moom and create a custom preset with the following settings
    • Type: Arrange Windows
    • Name: I put “3 Monitors – Work”
    • Uncheck all checkboxes
  3. Click “Update Snapshot”

This saves the layout as a preset. To test it, resize and move all your windows around. Then, hover over the green dot in any one window and click on the preset. All windows will instantly move to how you had them.

When you’re at home, you can create another preset and call it something like “3 Monitors – Home”. Now, you no longer have to mess around with moving windows around. Just click on a preset from any open window and get back to business.

Moom has a one-time cost of $10, but it’s obviously worth it.

Make Multiple REST API Calls in Serial and Parallel Using Node.js

In this tutorial, I will explain how we can fetch remote paginated JSON data synchronously (in serial) and asynchronously (in parallel).

Data

You can get test data to fetch from RapidAPI, but I’m going to fetch video data from Vimeo using the Vimeo API.

Fetch Method

There are many ways you can fetch remote data. The RapidAPI website provides code snippets for various languages and fetch methods. For example, for Node.js, there’s HTTP, Request, Unirest, Axios, and Fetch.

Some services like Vimeo provide libraries and SDKs in a few languages like PHP, Python and Node.js. You can use those as well if you’d like.

I’m actually going to use the Got library [GitHub], which is a very popular library.

CommonJS vs ESM

Many of the latest Node packages are now native ESM instead of CommonJS. Therefore, you can’t require modules like this

const got = require('got');

Instead, you must import modules like this

import got from 'got';

According to this page, you can convert your project to ESM or use an older version of the got package that uses CommonJS.

If using ESM, you need to put "type": "module" in your package.json.

Authentication

Many services like Vimeo require authentication in order to use their API. This often involves creating an access token and passing it in the header of the API call like this

In cURL:

curl https://api.vimeo.com/tutorial -H "Authorization: bearer {access_token}"

As HTTP:

GET /tutorial HTTP/1.1
  Host: api.vimeo.com
  Authorization: bearer {access_token}

Setup

Let’s set up our project. Do the following:

  1. Create a new folder, e.g. test
  2. Open the folder in a code editor (I’m using VisualStudio Code)
  3. Open a terminal (I’m doing it in VS Code)
  4. Initialize a Node project by running npm init -y

This will generate a package.json file in the folder.

Since we’re using ESM and will import modules rather than require them, add the following to the package.json file.

"type": "module"

Call the Vimeo API

Let’s start by calling the Vimeo API just once. Create a new file called get-data-one.js and copy the following contents into it. Replace {user_id} with your Vimeo user ID and {access_token} with your Vimeo access token.

import got from 'got';

let page = 1;
let per_page = 3;
let fields = "privacy,link,release_time,tags,name,description,download";

const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
const options = {
  method: 'GET',
  headers: {
    'Authorization': 'bearer {access_token}'
  }
};

let data = await got(url, options).json();
console.log(data);

We’re importing the got library. For this to work, we need to install the got package. Run the following command.

npm install got

This will download the got package and its dependencies into the node_modules folder.

In the code, the Vimeo endpoint we’re calling is /users/{user_id}/videos, which returns all videos that a user has uploaded. According to the API docs, we can

  • Specify the page number of the results to show using page
  • Specify the number of items to show on each page of results, up to a maximum of 100, using per_page
  • Specify which fields to return using fields

These parameters can be added to the endpoint URL in the query string, which is what we’ve done. However, for this test, we’ll just call one page and return the records (videos). We then call the API using the got library and then dump the results to the console. Let’s run the script and check the output. Run the following command.

node get-data-one.js

As expected, here’s the output.

The output starts with pagination info and the total number of available records (videos) followed by the actual data in the form of an array of video objects. In this case, we see 3 objects because we set per_page to 3.

Let’s update our code to write the output to a file. That will make it easier to read when there’s a lot of data. Add the following code snippets

import fs from "fs";
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
    stream.once('open', function(fd) {
    stream.write(JSON.stringify(data)+"\n");
    stream.end();
});

so the code looks like this:

import fs from "fs";
import got from 'got';

let page = 1;
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";

const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
const options = {
  method: 'GET',
  headers: {
    'Authorization': 'bearer {access_token}'
  }
};

let data = await got(url, options).json();
console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
    stream.once('open', function(fd) {
    stream.write(JSON.stringify(data)+"\n");
    stream.end();
});

We don’t need to install the fs package because that’s included in Node by default. The stream will write data to a file we’ll call video-data.json and we pass it the “w” flag to overwrite any existing contents of the file.

When we rerun the script, we see the file is created. We can format (prettify) it so it’s easy to read.

Call the Vimeo API Multiple Times in Serial with Pagination

Now, let’s say we want to fetch more data, but the API limits how many records are returned in a single call. In this case, we need to call the API in a loop passing a different page number. Let’s create a new file called get-data-serial.js with the following code.

import fs from "fs";
import got from 'got';

let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";
const options = {
    method: 'GET',
    headers: {
      'Authorization': 'bearer {access_token}'
    }
}

for(let page = 1; page <= 3; page++) {
    const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;

    let somedata = await got(url, options).json();
    data.push(somedata);
    console.log(page);
};

console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
    stream.once('open', function(fd) {
    stream.write(JSON.stringify(data)+"\n");
    stream.end();
});

Here, I’m using a simple for loop to loop through 3 pages. I also created a data variable as an empty array. With each loop iteration, I push the page’s returned data to the data array. When all is done, I write the data array to a file, which looks like this.

I collapsed the “data” array so we can see that 3 pages of data were returned. We ran this in serial so the order of the output is page 1, page 2, and page 3.

Call the Vimeo API Multiple Times in Parallel with Pagination

Now, let’s do the same thing, but asynchronously (in parallel). Create a new file called get-data-parallel.js with the following code.

import fs from "fs";
import got from 'got';

const options = {
    method: 'GET',
    headers: {
    'Authorization': 'bearer {access_token}'
    }
};

let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";
let pages = [1,2,3];

await Promise.all(pages.map(async (page) => {
    const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=2&fields=privacy,link,release_time,tags,name,description,download`;

    let somedata = await got(url, options).json();
    data.push(somedata);
    console.log(page);
}));

console.log(data);
var stream = fs.createWriteStream("video-data-parallel.json",{flags:'w'});
    stream.once('open', function(fd) {
    stream.write(JSON.stringify(data)+"\n");
    stream.end();
});

In this case, instead of a for loop, we’re using Promise.all and passing to it an array of page numbers that we loop over using the map function. When we run the script, we get output like the following:

You’ll notice 2 things:

  1. the script runs faster because the API calls are done simultaneously in parallel (asynchronously) rather than one after the other in serial (synchronously).
  2. the order of the output is no longer consecutive by page number. In this example, it was page 1, page 3, page 2.

Modifying the JSON Output Structure

As shown in the previous screenshot, the API call returns an object containing pagination info followed by a data array – an array of objects containing video info.

What if we just want the data objects and not the pagination info. We can do that by modifying the structure of the JSON output. We can replace

data.push(somedata);

with

data.push(somedata.data);

but then the output becomes an array of arrays.

To fix this, let’s flatten the array by adding the following code:

data = data.flat(1);

right before we console it out and write to file.

Now, the output file looks like this (each record is collapsed for visibility).

Filtering Out Certain Records

What if we want to filter out certain records, e.g. we want to filter out all videos that are not public, i.e. we only want videos where privacy.view = “anybody”. We can use the filter function to do that, like this:

somedata = somedata.filter(video => video.privacy.view === "anybody" );

Now, we only get 5 records instead of 6 because one of them had privacy.view = “unlisted”.

What if we want to exclude videos in the “Educational” category. We can do so like this:

somedata = somedata.filter(function (video, index, arr) {
    let isEducational = false;
    video.categories.filter(function (category, index, arr) {
        if (category.name === "Educational") {
            isEducational = true;
        }
    });

    if (isEducational === false) {
        return video;
    }
});

Now, the output is only one record.

Removing Fields From the JSON Output

Each video record can contain a lot of information, including information we don’t need. For example, the privacy object contains 5 keys.

If we want to return just one privacy key, say “view”, then we can do so using the map function as follows:

// simplify privacy object to just privacy.view
somedata = somedata.map(function (video) {
    video.privacy = video.privacy.view;
    return video;
});

For each video record, the “download” field is an array of objects, one for each available rendition (resolution), e.g.

If we only want to, say, return “hd” videos and only the download links, we can use two map functions like this:

// only include videos that are HD and only return HD video download links
somedata = somedata.map(function (video) {
    let download = [];

    video.download.map(function (size) {
        if (size.quality === "hd") {
            download.push({
                rendition: size.rendition,
                link: size.link
            })
        }
    });

    if (download.length !== 0) {
        video.download = download;
        return video;
    }
});

Now, the downloads array is simplified, like this:

The “categories” field is an array of objects with a lot of data, including objects and arrays of objects.

What if we want to simplify that to just a comma-delimited list of category names. We can do that like this:

somedata = somedata.map(function (video) {
    let categories = [];

    if (video !== undefined) {
        video.categories.map(function (category) {
            categories.push(category.name);
        });

        video.categories = categories;
        return video;
    }
});

Now, the “categories” field is much simpler.

Complete Code

For reference, here’s the complete code for get-data-serial.js. The page limit and per_page values can be updated depending on how many results you want.

import fs from "fs";
import got from 'got';

let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download,categories";
const options = {
    method: 'GET',
    headers: {
      'Authorization': 'bearer {access_token}'
    }
}

for(let page = 1; page <= 3; page++) {
    const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;

    let somedata = await got(url, options).json();

    somedata = somedata.data;

    // only include videos that are public
    somedata = somedata.filter(video => video.privacy.view === "anybody" );

    // only include videos that aren't in the "Educational" category
    somedata = somedata.filter(function (video, index, arr) {
        let isEducational = false;
        video.categories.filter(function (category, index, arr) {
            if (category.name === "Educational") {
                isEducational = true;
            }
        });

        if (isEducational === false) {
            return video;
        }
    });

    // simplify privacy object to just privacy.view
    somedata = somedata.map(function (video) {
        video.privacy = video.privacy.view;
        return video;
    });

    // only include videos that are HD and only return HD video download links
    somedata = somedata.map(function (video) {
        let download = [];

        video.download.map(function (size) {
            if (size.quality === "hd") {
                download.push({
                    rendition: size.rendition,
                    link: size.link
                })
            }
        });

        if (download.length !== 0) {
            video.download = download;
            return video;
        }
    });

    // simplify categories array of objects to just an array of category names
    somedata = somedata.map(function (video) {
        let categories = [];

        if (video !== undefined) {
            video.categories.map(function (category) {
                categories.push(category.name);
            });

            video.categories = categories;
            return video;
        }
    });

    data.push(somedata);
    console.log(page);
};

data = data.flat(1);
console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
    stream.once('open', function(fd) {
    stream.write(JSON.stringify(data)+"\n");
    stream.end();
});

Learn more about async/await.