Статьи

Дополнительные примеры API Marvel

Несколько дней назад был выпущен самый лучший API во всем Интернете — Marvel API . Ладно, возможно, сильное — это сильное слово, но я люблю API, я люблю комиксы, и их сочетание — не что иное, как новости уровня Галактуса. (И под Галактусом я подразумеваю гигантского фиолетового парня, а не аморфное гигантское облако из забываемого фильма «Фантастическая четверка».)

API поддерживает получение данных о персонажах, комиксах, создателях, событиях, историях, сериалах и историях. Вы можете попробовать их интерактивные документы для деталей. Сначала вы захотите подписаться на ключ, чтобы увидеть результаты. Документы хорошо сделаны, но в настоящее время есть глупая ошибка CSS, которая не позволяет вам копировать текст из них. (Вы можете исправить это с помощью DevTools — если вы не знаете, как это сделать, просто оставьте мне комментарий в документации.) API пока не поддерживает поиск по тексту, поэтому, если вы хотите найти все «Spider» персонажам тебе не повезло. (Но это также было запрошено.)

Помимо этого, API довольно мощный с большим количеством опций. Вы можете использовать API в клиентских и серверных приложениях с очень простым GET-запросом. API имеет довольно низкий лимит (imo) 1000 звонков в день. Несколько человек попросили более высокий лимит, и люди Marvel сказали, что они рассмотрят его. Они хотели запустить с более низким пределом, просто чтобы быть осторожным, и я могу это понять. Еще одна странность в том, что у них пока нет подходящего форума для обсуждения API. Вместо этого у них есть одна страница с комментариями. Это не будет хорошо масштабироваться. Я надеюсь, что они изменятся на что-то другое довольно скоро, поскольку это уже становится немного грязным. (Черт, даже простая группа Google будет крутой.)

Я работал над простой демонстрацией с использованием Comics API . API позволяет извлекать комические данные и применять несколько разных фильтров. Таким образом, вы можете запросить коллекции или отдельные комиксы — запросить комиксы на определенную дату — или даже найти определенного персонажа. Данные результатов для отдельного комикса очень подробны.

Для моей первой демонстрации я подумал, что было бы интересно провести сравнение дат. Я написал демо, которое будет собирать 100 комиксов в год, вычислять среднюю цену и количество страниц и отображать 5 случайных изображений. Мне было любопытно увидеть, как цены и размеры изменились за эти годы. Давайте посмотрим на код. Во-первых, мой HTML.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="handlebars-v1.3.0.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div id="results"></div>
<div id="status"></div>

<script id="reportTemplate" type="text/x-handlebars-template">
<h1>{{year}}</h1>
<p>
Average Price: ${{avgPrice}}<br/>
Low/High Price: ${{minPrice}} / ${{maxPrice}}<br/>
Average Page Count: {{avgPageCount}}<br/>
</p>
{{#each thumbs}}
<img src="{{this}}" class="thumb">
{{/each}}
<br clear="left">
<p/>
</script>
<script src="app.js"></script>

</body>
</html>

Здесь не так много, но есть шаблон Handlebars для обработки результатов. Давайте посмотрим на JavaScript сейчас.

/* global $,console,document,Handlebars */

//default not avail image
var IMAGE_NOT_AVAIL = "http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available";

//my key
var KEY = "mykeyisbetterthanyours";

//credit: http://stackoverflow.com/a/1527820/52160
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getComicData(year) {
var url = "http://gateway.marvel.com/v1/public/comics?limit=100&format=comic&formatType=comic&dateRange="+year+"-01-01%2C"+year+"-12-31&apikey="+KEY;
console.log('getComicData('+year+')');
return $.get(url);
}

$(document).ready(function() {

var $results = $("#results");
var $status = $("#status");

var templateSource = $("#reportTemplate").html();
var template = Handlebars.compile(templateSource);
var start = 2013;
var end = 1950;

var promises = [];

$status.html("<i>Getting comic book data - this will be slow - stand by...</i>");

for(var x=start; x>=end; x--) {
promises.push(getComicData(x));
}

$.when.apply($,promises).done(function() {

var args = Array.prototype.slice.call(arguments, 0);

$status.html("");

for(var x=0; x<args.length; x++) {
var year = start-x;
console.log("displaying year", year);

var stats = {};
stats.year = year;
stats.priceTotal = 0;
stats.priceCount = 0;
stats.minPrice = 999999999;
stats.maxPrice = -999999999;
stats.pageTotal = 0;
stats.pageCount = 0;
stats.pics = [];

var res = args[x][0];

if(res.code === 200) {
for(var i=0;i<res.data.results.length;i++) {
var comic = res.data.results[i];
//just get the first item
if(comic.prices.length && comic.prices[0].price !== 0) {
stats.priceTotal += comic.prices[0].price;
if(comic.prices[0].price > stats.maxPrice) stats.maxPrice = comic.prices[0].price;
if(comic.prices[0].price < stats.minPrice) stats.minPrice = comic.prices[0].price;
stats.priceCount++;
}
if(comic.pageCount > 0) {
stats.pageTotal+=comic.pageCount;
stats.pageCount++;
}
if(comic.thumbnail && comic.thumbnail.path != IMAGE_NOT_AVAIL) stats.pics.push(comic.thumbnail.path + "." + comic.thumbnail.extension);

}
stats.avgPrice = (stats.priceTotal/stats.priceCount).toFixed(2);
stats.avgPageCount = (stats.pageTotal/stats.pageCount).toFixed(2);

//pick 5 thumbnails at random
stats.thumbs = [];
while(stats.pics.length > 0 && stats.thumbs.length < 5) {
var chosen = getRandomInt(0, stats.pics.length);
stats.thumbs.push(stats.pics[chosen]);
stats.pics.splice(chosen, 1);
}

console.dir(stats);
var html = template(stats);
$results.append(html);
}
}
});

});

Там не так много для этого. Я в основном зацикливаюсь на несколько лет и запускаю асинхронные запросы для получения данных. За каждый год я вычисляю свои средние значения, собираю изображения и выбираю 5 случайных. Наконец результаты выводятся на экран. Это приложение работает медленно, так как я ожидаю завершения всех 63 запросов, прежде чем рендерить. Лучшая демонстрация будет отображаться по мере поступления результатов и корректно отображать их в правильном порядке. Результат был … захватывающим.

Я вроде знал, что цены будут расти со временем, и это не удивительно. В 2013 году мои данные показывают в среднем 4,12 доллара по сравнению с десятью центами в 1950 году. Количество страниц немного ниже, но не значительно. Что было действительно эпично, так это обложки. Я имею в виду, я знал, что стили менялись с течением времени, но увидеть все это было здорово! Например, вот те, которые я получил за 2013 год.

Теперь вернемся к 1985 году.

И наконец — 1960.

Из-за ограничения API я не могу поделиться живым приложением, но я взял обработанный вывод и сохранил его. Если вам интересно, вы можете получить динамически генерируемый HTML, просто открыв консоль и выполнив $ (body) .html (). Вы можете просмотреть статический отчет здесь: http://www.raymondcamden.com/demos/2014/jan/31/report.html

Итак, я понял, что самой крутой частью последней демонстрации были обложки. Поэтому я построил второе демо, сфокусированное именно на этом. Я создал приложение Node.js / Express, которое делало одну вещь: выбирал случайный год, выбирал случайный месяц и выбирал случайное покрытие. Затем он отобразил это пользователю вместе с названием / датой публикации в левом нижнем углу. Поскольку это было на стороне сервера, я смог использовать кеширование. Я использовал диапазон от 1960 до 2013, который составляет 756 различных вызовов API. В теории — я должен иметь возможность запускать приложение и никогда не достигать своего предела. Я также встроил код для обработки случаев, когда ограничения API все равно превышаются. Если у меня есть хотя бы 5 месяцев кеширования, я просто буду использовать существующий кеш. Я поделюсь всей базой кода, но вот модуль marvel.js, который приложение использует для возврата обложки.

/* global require,exports, console */
var http = require('http');
var crypto = require('crypto');

var cache = [];

var PRIV_KEY = "iamthegatekeeper";
var API_KEY = "iamthekeymaster";

//default not avail image
var IMAGE_NOT_AVAIL = "http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available";

exports.getCache = function() { return cache; };

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

Object.size = function(obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};

function getCover(cb) {
//first select a random year
var year = getRandomInt(1960, 2013);
//then a month
var month = getRandomInt(1,12);

var cache_key = year + "_" + month;

if(cache_key in cache) {
console.log('had cache for '+cache_key);
var images = cache[cache_key].images;
cache[cache_key].hits++;
cb(images[getRandomInt(0, images.length-1)]);
} else {
var monthStr = month<10?"0"+month:month;
//lame logic for end of month
var eom = month==2?28:30;
var beginDateStr = year + "-" + monthStr + "-01";
var endDateStr = year + "-" + monthStr + "-" + eom;
var url = "http://gateway.marvel.com/v1/public/comics?limit=100&format=comic&formatType=comic&dateRange="+beginDateStr+"%2C"+endDateStr+"&apikey="+API_KEY;
var ts = new Date().getTime();
var hash = crypto.createHash('md5').update(ts + PRIV_KEY + API_KEY).digest('hex');
url += "&ts="+ts+"&hash="+hash;
//TEMP
//var url ="http://127.0.0.1/testingzone/html5tests/marvel/resp.json";

console.log(new Date()+' '+url);

http.get(url, function(res) {
var body = "";

res.on('data', function (chunk) {
body += chunk;
});

res.on('end', function() {
//result.success = true;

var result = JSON.parse(body);
var images;

if(result.code === 200) {
images = [];
console.log('num of comics '+result.data.results.length);
for(var i=0;i<result.data.results.length;i++) {
var comic = result.data.results[i];
//console.dir(comic);
if(comic.thumbnail && comic.thumbnail.path != IMAGE_NOT_AVAIL) {
var image = {};
image.title = comic.title;
for(var x=0; x<comic.dates.length;x++) {
if(comic.dates[x].type === 'onsaleDate') {
image.date = new Date(comic.dates[x].date);
}
}
image.url = comic.thumbnail.path + "." + comic.thumbnail.extension;
images.push(image);
}
}
//console.dir(images);
//now cache it
cache[cache_key] = {hits:1};
cache[cache_key].images = images;
cb(images[getRandomInt(0, images.length-1)]);
} else if(result.code === "RequestThrottled") {
console.log("RequestThrottled Error");
/*
So don't just fail. If we have a good cache, just grab from there
*/
if(Object.size(cache) > 5) {
var keys = [];
for(var k in cache) keys.push(k);
var randomCacheKey = keys[getRandomInt(0,keys.length-1)];
images = cache[randomCacheKey].images;
cache[randomCacheKey].hits++;
cb(images[getRandomInt(0, images.length-1)]);
} else {
cb({error:result.code});
}
} else {
console.log(new Date() + ' Error: '+JSON.stringify(result));
cb({error:result.code});
}
//console.log(data);
});

});
}

}

exports.getCover = getCover;

Вот скриншот:

Вы можете посмотреть это здесь: marvel.raymondcamden.com . Обратите внимание, что я пока не показываю метку атрибуции «Данные по Marvel», и мне нужно добавить ее, чтобы соответствовать правилам API Marvel. (Что совершенно справедливо — я просто еще не хотел перезагружать сервер!)

Я знаю, что я предвзят, но мне нравится моя демонстрация. В первый раз, когда я смотрел его, я увидел названия, о которых я никогда не слышал. Я не знаю, считаю ли я себя «серьезным» коллекционером комиксов, я просто покупаю то, что мне нравится, но, видя глубину истории в линии Marvel, на самом деле побуждает меня выбирать больше комиксов из прошлого. Я включил полный исходный код для обеих моих демонстраций в качестве приложения к этому сообщению в блоге. Я снял ключи, очевидно.