Intro
Today we are going to build an application that will cycle your desktop background to different randomized images from the web, based on a theme and frequency you specify. I've included a level of detail in this tutorial that I hope is suitable for programmers who are new to Node.js.
If you do not need an explanation and just want to download and use the application, you can access the full code on GitHub.
Prerequisites
- I can verify that this will work on Windows 10+ and Mac Ventura+, there seems to be a problem with the wallpaper package on Linux systems.
- You will need to be somewhat familiar with JavaScript. You will have an easier time if you are familiar with ES6 syntax and features, as well as Node and Npm.
- You need to have the latest versions of Node and Npm installed on your system. If you don’t already have Node and Npm installed you can visit the following link for help with this process: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
- You need to have an IDE installed on your system that supports JavaScript and Node.js. I recommend VSCode or Sublime, but any IDE that supports Node.js will do.
- You will also need to sign up for a free SerpApi account at https://serpapi.com/users/sign_up.
Preparation
First you will need to create a package.json file. Open a terminal window, create a directory for the project, and CD into the directory.
mkdir wallpaper-changer
cd wallpaper-changer
Create your package.json file:
npm init
Npm will walk you through the process of creating a package.json file.
After this we need to make a few changes to the package.json file.
We will be using the ES Module system rather than CommonJs, so you need to add the line type="module"
to your package.json.
{
"name": "wallpaper-changer",
"version": "1.0.0",
"description": "Change your wallpaper to a randomized image from the web",
"type": "module",
"main": "index.js",
We will also add a start script for convenience:
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
Next we need to install our dependencies. For this project we will use the new SerpApi for JavaScript/TypeScript Node module. We will also use the image-downloader, wallpaper, write-json-file, node-schedule, and dotenv modules. Most of these have self explanatory names. If you're not familiar with dotenv, it is a module for handling environment variables (like file paths and API keys).
npm install serpapi dotenv wallpaper node-schedule write-json-file image-downloader
If you haven’t already signed up for a free Serpapi account go ahead and do that now by visiting https://serpapi.com/users/sign_up and completing the signup process.
Once you have signed up, verified your email, and selected a plan, navigate to https://serpapi.com/manage-api-key . Click the Clipboard icon to copy your API key.
Then create a new file in your project directory called .env and add the following line:
SERPAPI_KEY = “PASTE_YOUR_API_KEY_HERE”
You will also want to specify the absolute path to a folder where you want to store the images you download:
PATH_TO_IMG = “ABSOLUTE_PATH_TO_DIRECTORY”
Create a file called index.js and add the following:
import { config } from "serpapi";
import * as dotenv from "dotenv";
dotenv.config();
config.api_key = process.env.SERPAPI_KEY;
Scraping Google Images with SerpApi
We want to use SerpApi to search Google Images, and return a list of URLS we can download images from.
First we will need to determine our search parameters. We will want as much as possible to limit our search to only images with resolution and dimensions suitable for desktop wallpaper:
You can then further filter the images by selecting one or more "chips". You can see here I have selected nature, but you can select anything you like.
In order to use these filters or "chips" in our request to SerpApi, we will need to get the value of the chips
parameter in the URL:
These are just suggestions. You can use any query you like, and skip "chips" if you don't need them to get a selection you like. For example instead of "4k desktop wallpaper" you can try searching for "desktop wallpaper nature" or even just "nature photos", or anything you like. Keep in mind that any image you see in the first page of results could be randomly selected as your desktop.
Now that we have selected the image search results page we want to use, we are ready to write the code to scrape it. For this we will use SerpApi. SerpApi is a web-scraping API that streamlines the process of scraping data from search engines. This can also be done manually, but SerpApi provides several distinct advantages. Besides providing real time data in a convenient JSON format, SerpApi covers proxies, captcha solvers and other necessary techniques for avoiding searches being blocked. You can click here for an in depth tutorial to scrape Google Images both manually and with SerpApi, or here for an overview of the techniques you can use to prevent getting blocked when scraping the web.
Create a new file and name it image-search.js. Then add the following:
import { getJson } from "serpapi";
export async function getImageResults(q, chips) {
// The parameters we will include in the the GET request
const params = {
q: q,
location: "Austin, Texas, United States", //your location
google_domain: "google.com", //the google domain for your location
gl: "us", //your two letter country code
hl: "en", //language
chips: chips,
tbm: "isch", //specifies that this an image search
};
// call the API and wait for it to return the data
const data = await getJson("google", params);
return data;
}
If you want to use the same images that came up in your browser, be sure to replace the location
, google_domain
, gl
, and hl
parameters with the correct values for your location. We are using method parameters to set q
and chips
so that these will be easy to change later if we decide we want a different image selection.
Now in your index.js file, import the method we just defined and call it with the q
and chips
parameters you chose for your image search.
index.js should now look like this:
import { config } from "serpapi";
import * as dotenv from "dotenv";
import { getImageResults } from "./image-search.js";
dotenv.config();
config.api_key = process.env.SERPAPI_KEY;
const results = await getImageResults("4k desktop wallpaper", "q:4k+desktop+wallpaper,g_1:nature:Vy6qxRTDEsA%3D");
console.log(results);
You should see something like this in your console:
If you scroll down in your terminal to where it says image_results
you will see that each image has the title
, link
, and original
fields. The link
will take you to the website where the image was found. original
is the URL you can use to download the image. We are going to scrape title
and original
in this tutorial.
We are going to write a method now that will extract these fields from each image in the image_results
, and return an array of objects.
Add the following code to your image_search.js
file:
export async function getImageUrls(q, chips) {
// perform the search and store the results in a variable
const results = await getImageResults(q, chips);
// extract the image urls and titles and store them in an array of Objects
const urls = results.images_results.map((img) => {
return { link: img.original, title: img.title };
});
return urls;
}
Then return to your index.js file and import the new method:
import { getImageUrls } from "./image-search.js";
Modify the code you used to print the search results, so that it prints the image titles and URLs instead:
const results = await getImageUrls("4k desktop wallpaper", "q:4k+desktop+wallpaper,g_1:nature:Vy6qxRTDEsA%3D");
console.log(results);
Check your terminal output and make sure are printing a list of urls and titles.
Downloading the Image
Now that we have URLs for some images, we want to select a random one and download it. In index.js, comment out your console.log
statement for now, and add the code to select a random URL from the list:
const urls = await getImageUrls("4k desktop wallpaper", "q:4k+desktop+wallpaper,g_1:nature:Vy6qxRTDEsA%3D");
// console.log(results);
const randInt = Math.floor(Math.random() * urls.length);
const randImgUrl = urls[randInt]["link"];
Create a new file called image-downloader.js. The code should look like this:
import * as download from "image-downloader";
export async function downloadImg(url) {
//specify a url and the path to where you want to store the image
const options = {
url,
dest: process.env.PATH_TO_IMG,
};
let img;
// download the image
await download
.image(options)
.then(({ filename }) => {
console.log("Saved to", filename);
img = filename.toString();
})
.catch((err) => console.error(err));
// return the filename so we can find the image later
return img;
}
Here we are accepting a URL as a parameter, and attempting to download an image from that URl into the directory we specified in our .env file. We return the file name so that we can find it when we want to set our wallpaper.
Go back to index.js. Import the downloadImg()
function, and call it passing the randomly select URL as the parameter:
import { downloadImg } from "./image-downloader.js";
const urls = await getImageUrls("4k desktop wallpaper", "q:4k+desktop+wallpaper,g_1:nature:Vy6qxRTDEsA%3D");
// console.log(results);
const randInt = Math.floor(Math.random() * urls.length);
const randImgUrl = urls[randInt]["link"];
downloadImg(randImgUrl);
If you run this code, then check the directory you specified for image downloads in your .env file, you should see an image.
Setting the Desktop Wallpaper Programmatically
Create a new file and name it wallpaper-changer.js. Import the functions you wrote for scraping URLs and downloading images, as well as the wallpaper module:
import { getImageUrls } from "./image-search.js";
import { downloadImg } from "./image-downloader.js";
import { getWallpaper, setWallpaper } from "wallpaper";
Create an async function changePaper()
inside wallpaper-changer.js. Copy the code you wrote in index.js in the previous step for selecting and downloading a random image and paste it inside the changePaper()
function. Instead of hardcoding q
and chips
though, lets include them as method parameters:
export async function changePaper(q, chips) {
const urls = await getImageUrls(q, chips);
// select a random url from the array
const randInt = Math.floor(Math.random() * urls.length);
const randImgUrl = urls[randInt]["link"];
// download the image
const img = await downloadImg(randImgUrl);
// set desktop wallpaper
try{
await setWallpaper(img);
console.log("Set wallpaper to: " + urls[randInt]["title"]);
}catch(e){
console.log("e");
console.log("current wallpaper is " + wallpaper);
}
}
Then go back to index.js and import the changePaper()
method. Replace the code you just copied and pasted with a call to changePaper()
, passing in your desired q
and chips
values:
import { config } from "serpapi";
import * as dotenv from "dotenv";
import { changePaper } from "./wallpaper-changer.js";
dotenv.config();
config.api_key = process.env.SERPAPI_KEY;
changePaper(
"4k desktop wallpaper",
"q:4k+desktop+wallpaper,g_1:nature:Vy6qxRTDEsA%3D"
);
If you run npm start
now, you should see your wallpaper change!
Scheduling the Wallpaper Change as a Recurring Task
Now you can download a random image from the web as your wallpaper. But what if you want to do this every Friday at 10 am? Or what if you want a new image every minute, like a slideshow?
The most common way to schedule repeating tasks as a background process is to use something called Cron Jobs. But in order to keep things simple, we are sticking to all JavaScript in this tutorial.
We can accomplish this with JavaScript using node-schedule.
First create a new file, name it scheduler.js and add the following code:
import * as schedule from "node-schedule"
export async function scheduleJob(callback, time){
console.log("scheduling wallpaper change");
// create a RecurrenceRule Object to define when you want the job to repeat
const rule = new schedule.RecurrenceRule();
// copy the properties from the time object to the RecurrenceRule instance
Object.assign(rule, time);
// schedule the job
schedule.scheduleJob(rule, callback);
}
Notice the two parameters for the scheduleJob()
function - callback
and time
.
callback
is the function we want to schedule to run, and time
is an object with properties specifying when we want the function to run.
Go back now to your index.js. We don't want to call our changePaper()
function right away anymore, instead we want to pass it in as the first argument to the scheduleJob()
function. We also need to define a time
object to specify when we want the code to run. Don't forget to add the import statement for the schedule Job
function:
import { config } from "serpapi";
import * as dotenv from "dotenv";
import { changePaper } from "./wallpaper-changer.js";
import { scheduleJob } from "./scheduler.js";
dotenv.config();
config.api_key = process.env.SERPAPI_KEY;
const callback = () =>
changePaper(
"4k desktop wallpaper",
"chips=q:desktop+wallpaper+4k,g_1:nature:uPhOf0Whosk%3D"
);
const time = {
second: 0,
//hour: 0,
//minute: 0,
//dayOfWeek: 0,
//tz: "PST"
};
scheduleJob(callback, time);
The code above will cause the wallpaper to change once every minute. You can change the values in the time variable to anything you want. I suggest starting with every minute through, to make sure it is working.
You can use any of the following key-value pairs in your time object:
second (0-59)
minute (0-59)
hour (0-23)
date (1-31)
month (0-11)
year
dayOfWeek (0-6) Starting with Sunday
tz
You can read more about how to specify different times at https://www.npmjs.com/package/node-schedule.
Now you can run npm start
and watch what happens.
Saving the URLS to a file
You have successfully built an application that periodically downloads a random image from the web and sets it as your desktop wallpaper. You may have noticed there is a part of this process that is incredibly inefficient - we are scraping the image URLS again each time we change the image. It costs a search credit every time we call SerpApi (unless it is an identical call within the hour, in which case the results are fetched from the cache). In any case we want to avoid making unnecessary repeat API calls.
To solve this we will use the write-json-file node module to save the URLs in an external JSON file, instead of holding them in memory and repeating our request over and over again. We will also use the fs
module here for interacting with the file system. Node.js ships with fs
, so you don't need to install it, but we do need to add import statements.
Open your wallpaper-changer.js file again. The code here is a bit more convoluted. We first have to check whether the ".json" file already exists. If it exists we want to read it.
If the file doesn't exist, or if it is empty, we want to call our the getImageUrls()
function to request search results and extract the URLS, then we want to create the ".json" file and write the URLs to it.
import { getImageUrls } from "./image-search.js";
import { downloadImg } from "./image-downloader.js";
import { getWallpaper, setWallpaper } from "wallpaper";
import { writeJsonFile } from "write-json-file";
import { readFile } from "fs/promises";
import { existsSync } from "fs";
export async function changePaper(q, chips) {
let urls;
// check whether the file where we save the urls exists
const fileExists = existsSync("./image-urls.json");
// if the file exists, parse the data from it into the "urls" variable
if (fileExists) {
const json = JSON.parse(
await readFile(new URL("./image-urls.json", import.meta.url))
);
urls = json.urls;
}
// If the file doesn't exist or is empty, download the image urls and create/write the urls to the file
if (!fileExists | !urls) {
urls = await getImageUrls(q, chips); // search google images and extract the image urls
await writeJsonFile("image-urls.json", { urls });
}
// select a random url from the array
const randInt = Math.floor(Math.random() * urls.length);
const randImgUrl = urls[randInt]["link"];
// download the image
const img = await downloadImg(randImgUrl);
// set desktop wallpaper
try{
await setWallpaper(img);
console.log("Set wallpaper to: " + urls[randInt]["title"]);
}catch(e){
console.log("couldn't set wallpaper");
console.log("e");
}
// verify that we've changed the wallpaper, useful for troubleshooting
const wallpaper = await getWallpaper();
console.log("current wallpaper is " + wallpaper);
}
Conclusion
We have created a Node.js application to make our desktop wallpaper cycle through random images from the web at scheduled intervals. You now have a template you can modify in any way you like. You can try out different themes by modifying the values of q
and chips
, and deleting the .json file.
It may have occurred to you that if you keep downloading images indefinitely, eventually they will take up too much space on your hard drive.
Ideally we would want to add code to check how many images are in the directory, and delete them after a specified limit. Or delete them every time we change the background, or after every time the program runs. I haven't covered this here but if you want to implement it, this blog post post explains how to delete files using fs
: https://www.bezkoder.com/node-js-delete-file/#:~:text=To delete a file in,loop because it works asynchronously.
You will also have to keep the terminal window open for the program to run and continue changing your desktop background at scheduled intervals. If you want it to run as a background process, I suggest learning about Cron Jobs and replacing the node-schedule module and our scheduler.js file with a Cron Job.
I hope you found the tutorial informative and easy to follow. If you have any questions, feel free to contact me at ryan@serpapi.com.