Serverless Demo - Random Comic Book Character via Comic Vine API

For today's demo, I'm going to be using the Comic Vine API, but let me warn folks that I think it is bad idea to use this API in production. I started looking at it over the weekend and while I was "successful", I found numerous documentation issues and lots of forum posts that have gone unanswered. My gut tells me that this is not something I'd ever use for a "real" app, but since I don't build real apps it doesn't matter, right?

On the flip side, I do want to call out something I think the Comic Vine API does very well. I love the status report of your usage:

Screen shot

Specifically (and I called it out with the arrow) - I love the "Your request rate is fine" comment. It's a small thing, but it's a plain English summary of my status that doesn't make me look at raw numbers and figure out how much I'm abusing the rate limits.

Alright, so with that out of the way - what did I build? I've previously shown how to get a random comic book character from the Marvel API (All My Friends are Superheros) as well as random comic book covers (Building a Twitter bot to display random comic book covers). In both cases, my thinking was that Marvel's history was so rich, I wanted to be surprised by what data existed out there and by how deep the history went. I was a comic book reader growing up, gave up in college, and started reading again about ten years ago.

This was also about the time of Marvel's cinema rise to power and DC's... well, DC's trying for sure. "Wonder Woman" was incredible and I'm a big fan of all their TV shows. The net result of all this new video media is that I've gotten more interested in reading comics. After I caught up with the Flash for example, I picked up a few TPBs. I then did the same for Green Arrow. My appreciation for comic books as a whole is growing and I think that's a great thing.

I thought it would be interesting to recreate what I built with the Marvel API using the Comic Vine API instead. Specifically I was looking at getting a random comic book character. As much as I complained about the API above, once you have an API key it's pretty easy to figure out how to get a random one.

Here is the root URL to fetch all characters, I say "all", but it is paged:

https://www.comicvine.com/api/characters?api_key={{key}}&format=json

The {{key}} part is dynamic of course. Calling this gives you one page of results in no particular order, but the important part is the metadata:

{
    "error": "OK",
    "limit": 1,
    "offset": 0,
    "number_of_page_results": 1,
    "number_of_total_results": 116711,
    "status_code": 1,
    "results": [
        // N characters here
    ],
    "version": "1.0"
}

See the number_of_total_results there? Because I know how many characters exist in their database and because I can both offset and limit my results, getting a random character is actually fairly simple:

const rp = require('request-promise');

let apiUrl = `https://www.comicvine.com/api/characters?api_key=${key}&format=json&field_list=aliases,deck,description,first_appeared_in_issue,image,real_name,name,id,publisher&limit=1&offset=`;

/*
Hard coded but changes on the fly
*/
let totalChars = 100000;

function getRandomInt (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function main(args) {

    return new Promise( (resolve, reject) => {
        
        console.log('current max is '+totalChars);

        var options = {
            uri: 'https://www.comicvine.com/api/characters',
            qs: {
                api_key: key,
                format:'json',
                field_list:'aliases,deck,description,first_appeared_in_issue,image,real_name,name,id,publisher',
                limit:1,
                offset:getRandomInt(0,totalChars)
            },
            headers: {
                'User-Agent': 'Request-Promise'
            },
            json: true 
        };

        rp(options)
        .then(function(json) {
            //update totalChars
            totalChars = json.number_of_total_results;
            resolve({result:json.results[0]});
        })
        .catch(function(err) {
            console.log('error in rp');
            reject({error:err});
        });
            
    });
}

Let's start in the main function. I begin by making a note of totalChars. You'll notice I've hard coded it to 100k. Even though I know Comic Vine has over that number, I made my system just use that as an upper hit on the first call to the action. When I get my result back, I store that "real" number. There's multiple issues with this.

First - the number will only persist as long as my action does, and if no one is using it, it won't persist at all. Second, it is possible that the total number of characters in the Comic Vine database could shrink dramatically.

The "best" solution would be to make one call and ignore the character results but make note of the total and then make my random call again. That would double the HTTP calls and make my code more complex. So I made what I thought was a good compromise.

I get a random number, use that as the offset, and then just make my call. I use field_list to reduce the amount of data a bit which was a somewhat arbitrary decision. I then just resolve the result when done.

This worked well! So of course I decided to kick it up a notch. As I started looking at my data, I noticed a few things. There is another API for the detail for the character that returns a bit more information. I thought - why not return that data as well. I decided to add that second call like so:

var options = {
    uri: 'https://www.comicvine.com/api/characters',
    qs: {
        api_key: key,
        format:'json',
        field_list:'aliases,deck,description,first_appeared_in_issue,image,real_name,name,id,publisher,api_detail_url',
        limit:1,
        offset:getRandomInt(0,totalChars)
    },
    headers: {
        'User-Agent': 'Request-Promise'
    },
    json: true 
};

let character;

rp(options)
.then(function(json) {
    //update totalChars
    totalChars = json.number_of_total_results;
    //look up details now
    character = json.results[0];
    return rp({
        uri:character.api_detail_url,
        qs:{
            api_key:key,
            format:'json',
            field_list:'birth,character_enemies,character_friends,creators,powers,teams'
        },
        headers: {
            'User-Agent': 'Request-Promise'
        },
        json: true
    });
})
.then(function(json) {
    character.detail = json.results;
    resolve({character:character});
})
.catch(function(err) {
    console.log('error in rp');
    reject({error:err});
});

Notice I'm returning a promise in my then block so I can use yet another then to chain it. You can also see in the field list the kinds of interesting data you get in the result. I just append this data to a detail key in the result and resolve it.

The next thing I wanted was the name of the first comic book that this character appeared in. Here's where the API failed me. The initial character result contains an API URL that should point to that end point, but it didn't work for me. So instead I use the issues API to fetch it. This was yet another promise chained in resulting in a grand total of 3 HTTP calls to get one random character. Here's the entirety of the action now:

const rp = require('request-promise');

/*
Hard coded but changes on the fly
*/
let totalChars = 100000;

function getRandomInt (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function main(args) {

    return new Promise( (resolve, reject) => {
        
        let character;

        var options = {
            uri: 'https://www.comicvine.com/api/characters',
            qs: {
                api_key: args.key,
                format:'json',
                field_list:'aliases,deck,description,first_appeared_in_issue,image,real_name,name,id,publisher,api_detail_url,site_detail_url,gender',
                limit:1,
                offset:getRandomInt(0,totalChars)
            },
            headers: {
                'User-Agent': 'Request-Promise'
            },
            json: true 
        };

        rp(options)
        .then(function(json) {
            //update totalChars
            totalChars = json.number_of_total_results;
            //look up details now
            character = json.results[0];
            return rp({
                uri:character.api_detail_url,
                qs:{
                    api_key:args.key,
                    format:'json',
                    field_list:'birth,character_enemies,character_friends,creators,powers,teams'
                },
                headers: {
                    'User-Agent': 'Request-Promise'
                },
                json: true
            });
        })
        .then(function(json) {
            let detail = json.results;
            for(let key in detail) {
                character[key] = detail[key];
            }
            /*
            craft a url for the issue based on first_appeared_in_issue.
            note it includes an api_detail_url but it doesn't work
            */
            return rp({
                uri:'https://www.comicvine.com/api/issues',
                qs:{
                    api_key:args.key,
                    format:'json',
                    filter:'id:'+character.first_appeared_in_issue.id
                },
                headers: {
                    'User-Agent': 'Request-Promise'
                },
                json: true
            });
        })
        .then(function(json) {
            character.first_issue = json.results[0];
            resolve({character:character});
        })
        .catch(function(err) {
            console.log('error in rp',err);
            reject({error:err});
        });
            
    });
}

By the way, while I'm pretty comfortable with Promises now, I want to share a Gist snippet that my friend Shannon Hicks shared with me. It's based on an earlier version of the code, but it uses the new await functionality from ES6 and frankly - it's freaking awesome. I decided against using it for now because - as I said - I'm not really familiar with it yet, but damn, I think I will the next time I build a sample.

Alright - so far so good. Here's a sample of calling the action from the CLI:

https://gist.github.com/cfjedimaster/a8544fb2897ebc35bf5ddf8239bf6d53

I used a Gist since the JSON was rather large. If you don't want to read a large blob of data, just know the result was Sunset Shimmer.

Yep - cool.

Alright, so how do I want to actually use this? I decided to build a simple web page that would render the results. Now - unfortunately - I can't share this web page publicly since it will blow away my key usage pretty quickly if folks actually used the demo, but I'll share some screenshots of the results.

First is General Tuzik from DC:

Result

His "power" is "Leadership", awesome. Next up is Szothstromael from Dark Horse Comics:

Result

Ah, good old Szothstromael. A name that just rolls off the tongue. No powers are listed but obviously his power is to never actually be called by his name. (Ok, now to nerd out a bit - apparently this is a demon, and if demons can be controlled by their name, having a name that's near impossible to say would come in handy.)

One more for good measure - here is the anthropomorphic badger Inspector LeBrock:

Result

Nothing says "comic book" like anthropomorphic badger. So the code behind this is 99% rendering into the DOM. I spent 2 seconds turning my action into an anonymous API using OpenWhisk and Bluemix Native API Management (more on that later this week), and then I was able to call it via a simple URL. I decided against removing the URL below - if you want to hit it and see results, be my guest, just know I'll probably hit the rate limit.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
        <link href="https://fonts.googleapis.com/css?family=Bangers" rel="stylesheet">
        <style>
        body {
            font-family: 'Banger', cursive;
            background-color: #ffeb3b;
        }

        a {
            color: black;   
        }

        img.heroImage {
            float: right;
            max-width: 500px;
        }
        </style>

    </head>
    <body>

    <div id="result"></div>

    <p>
        All data from <a href="https://comicvine.gamespot.com" target="_new">ComicVine</a>.
    </p>

    <script src="https://code.jquery.com/jquery-3.1.1.min.js"   integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
    <script>
    let api = 'https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/37871051d18d0b2115da90f292458913e22e5d182c8a965fadcfbf6b5fcc96c6/comicvine/randomCharacter';


    $(document).ready(() => {

        $("#result").html('<i>Loading Random Character...</i>');

        $.get(api).then((res) => {
            console.log(res);
            if(res.error) {
                alert('Error! (check console)');
                return;
            }
            let char = res.character;
            let friendsTemplate = '';
            let enemiesTemplate = '';
            let powersTemplate = '';
            let teamsTemplate = '';
            let creatorsTemplate = '';

            //need to find female
            let defaultMaleImage = 'https://comicvine.gamespot.com/api/image/scale_large/1-male-good-large.jpg';
            let image = '';
            if(!char.image) {
                image = defaultMaleImage;
            } else if(char.image && !char.image.super_url) {
                image = defaultMaleImage;
            } else {
                image = char.image.super_url;
            }

            let publisher = 'None';
            if(char.publisher && char.publisher.name) publisher = char.publisher.name;

            /*
            If no description, copy deck over. deck can be blank too though
            also sometimes its <br/>, sometimes <p>.</p>
            */
            if(char.description && (char.description === '<br/>' || char.description === '<p>.</p>')) delete char.description;

            if(!char.description && !char.deck) {
                char.description = 'No description.';
            } else if(!char.description) {
                char.description = char.deck;
            }

            if(char.character_friends.length) {
                friendsTemplate = '<h2>Friends</h2><ul>';
                char.character_friends.forEach((friend) => {
                    friendsTemplate += `<li><a href="${friend.site_detail_url}">${friend.name}</a></li>`;
                });
                friendsTemplate += '</ul>';
            } 

            if(char.character_enemies.length) {
                enemiesTemplate = '<h2>Enemies</h2><ul>';
                char.character_enemies.forEach((enemy) => {
                    enemiesTemplate += `<li><a href="${enemy.site_detail_url}" target="_new">${enemy.name}</a></li>`;
                });
                enemiesTemplate += '</ul>';
            } 

            if(char.powers.length) {
                powersTemplate = '<h2>Powers</h2><ul>';
                char.powers.forEach((power) => {
                    powersTemplate += `<li>${power.name}</li>`;
                });
                powersTemplate += '</ul>';
            } 

            if(char.teams.length) {
                teamsTemplate = '<h2>Teams</h2><ul>';
                char.teams.forEach((team) => {
                    teamsTemplate += `<li><a href="${team.site_detail_url}" target="_new">${team.name}</a></li>`;
                });
                teamsTemplate += '</ul>';
            } 

            if(char.creators.length) {
                creatorsTemplate = '<h2>Creators</h2><ul>';
                char.creators.forEach((creator) => {
                    creatorsTemplate += `<li><a href="${creator.site_detail_url}" target="_new">${creator.name}</a></li>`;
                });
                creatorsTemplate += '</ul>';
            } 

            let mainTemplate = `
            <h1>${char.name}</h1>
            <p>
                <strong>Publisher:</strong> ${publisher}<br/>
                <strong>First Issue:</strong> <a href="${char.first_issue.site_detail_url}" target="_new">${char.first_issue.volume.name} ${char.first_issue.issue_number} (${char.first_issue.cover_date})</a><br/>
            </p>

            <a href="${char.site_detail_url}" target="_new"><img class="heroImage" src="${image}"></a>
            <p>${char.description}</p>

            ${creatorsTemplate}
            ${powersTemplate}
            ${teamsTemplate}
            ${friendsTemplate}
            ${enemiesTemplate}
            `;

            $('#result').html(mainTemplate);
        });

    });

    </script>
    </body>
</html>

There isn't anything particularly special about any of this. You can see a few hacks in there for when the data returned was a bit sub-optimal (or just plain broken). Finally, you can find the source code for the action and the demo here: https://github.com/cfjedimaster/Serverless-Examples/tree/master/comicvine

What do you think?

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

Want to read more like this?