BIN
chara.png
Before Width: | Height: | Size: 821 KiB |
BIN
chara_n.png
Before Width: | Height: | Size: 74 KiB |
BIN
chara_top2.png
Before Width: | Height: | Size: 606 KiB |
906
create-card.js
33
create-ilvl-filter.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const itemUICategory = [
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||||
|
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
41, 42, 43, 84, 87, 88, 89, 96, 97, 98, 99, 105, 106, 107,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Returns a list of item IDs that, when equipped, increase the iLvl by 1
|
||||||
|
async function createIlvlFilter(xivApiKey = undefined) {
|
||||||
|
const itemIds = [];
|
||||||
|
|
||||||
|
// Fetch for each category one after another to prevent rate limiting
|
||||||
|
// This should only by needed on startup, so a longer caching time is ok
|
||||||
|
for (const category of itemUICategory) {
|
||||||
|
const url = new URL('https://xivapi.com/search');
|
||||||
|
url.searchParams.set('indexes', 'item');
|
||||||
|
url.searchParams.set('filters', `IsIndisposable=1,ItemUICategoryTargetID=${category}`);
|
||||||
|
if (typeof xivApiKey === 'string' && xivApiKey !== '') url.searchParams.set('private_key', xivApiKey);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data != null && Array.isArray(data.Results)) {
|
||||||
|
for (const item of data.Results) {
|
||||||
|
itemIds.push(item.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createIlvlFilter
|
|
@ -1,27 +0,0 @@
|
||||||
const fetch = require("node-fetch");
|
|
||||||
|
|
||||||
const itemUICategory = [
|
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 84, 87, 88, 89, 96, 97, 98, 99,
|
|
||||||
105, 106, 107,
|
|
||||||
];
|
|
||||||
|
|
||||||
async function createilvlfilter() {
|
|
||||||
let results = [];
|
|
||||||
for (const category of itemUICategory) {
|
|
||||||
let url = `https://xivapi.com/search?indexes=item&filters=IsIndisposable=1,ItemUICategoryTargetID=${category}`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = response.json();
|
|
||||||
data.then((payload) => {
|
|
||||||
if (payload.Pagination.Results === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const item of payload.Results) {
|
|
||||||
let part = item.ID
|
|
||||||
results.push(part);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = createilvlfilter
|
|
|
@ -1,6 +1,3 @@
|
||||||
const ghpages = require("gh-pages");
|
const ghpages = require('gh-pages');
|
||||||
ghpages.publish("docs/", (err) => {
|
|
||||||
if (err) {
|
ghpages.publish('docs/', error => { if (error) console.error(error) });
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
244
index.js
|
@ -1,152 +1,150 @@
|
||||||
const fetch = require("node-fetch");
|
const cacheManager = require('cache-manager');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const fetch = require('node-fetch');
|
||||||
const port = process.env.PORT || 5000;
|
const fsStore = require('cache-manager-fs-binary');
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
|
|
||||||
const { CardCreator } = require('./create-card');
|
const { CardCreator } = require('./create-card');
|
||||||
|
|
||||||
const creator = new CardCreator();
|
const port = process.env.PORT || 5000;
|
||||||
|
const xivApiKey = typeof process.env.XIV_API_KEY === 'string' && process.env.XIV_API_KEY !== '' ? process.env.XIV_API_KEY : undefined;
|
||||||
|
const supportedLanguages = ['en', 'ja', 'de', 'fr'];
|
||||||
|
|
||||||
// node cachemanager
|
const app = express();
|
||||||
var cacheManager = require('cache-manager');
|
const creator = new CardCreator(xivApiKey);
|
||||||
// storage for the cachemanager
|
|
||||||
var fsStore = require('cache-manager-fs-binary');
|
// Initialize caching on disk
|
||||||
// initialize caching on disk
|
const diskCache = cacheManager.caching({
|
||||||
var diskCache = cacheManager.caching({
|
store: fsStore,
|
||||||
store: fsStore,
|
options: {
|
||||||
options: {
|
reviveBuffers: true,
|
||||||
reviveBuffers: true,
|
binaryAsStream: false,
|
||||||
binaryAsStream: false,
|
ttl: 14400, // s = 4h
|
||||||
ttl: 60 * 60 * 4 /* seconds */,
|
maxsize: 1000000000, // bytes = 1 GB
|
||||||
maxsize: 1000 * 1000 * 1000 /* max size in bytes on disk */,
|
path: 'diskcache',
|
||||||
path: 'diskcache',
|
preventfill: true,
|
||||||
preventfill: true
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getCharIdByName(world, name, retries = 1) {
|
// Rate limit all requests that result in XIV API calls
|
||||||
if (retries === -1) return undefined;
|
const limiter = rateLimit({
|
||||||
|
windowMs: 1000, // ms = 1s
|
||||||
|
max: 20, // default XIV API request limit
|
||||||
|
keyGenerator: () => 'global',
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(`https://xivapi.com/character/search?name=${name}&server=${world}`);
|
async function getCharacterIdByName(world, name, retries = 1) {
|
||||||
const data = await response.json();
|
if (retries === -1) return undefined;
|
||||||
|
|
||||||
if (data.Results[0] === undefined)
|
const searchUrl = new URL('https://xivapi.com/character/search');
|
||||||
return getCharIdByName(world, name, --retries);
|
searchUrl.searchParams.set('name', name)
|
||||||
|
searchUrl.searchParams.set('server', world)
|
||||||
|
if (xivApiKey != null) searchUrl.searchParams.set('private_key', xivApiKey)
|
||||||
|
|
||||||
return data.Results[0].ID;
|
const response = await fetch(searchUrl.toString());
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Results[0] === undefined) return getCharacterIdByName(world, name, --retries);
|
||||||
|
|
||||||
|
return data.Results[0].ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/prepare/id/:charaId', async (req, res) => {
|
async function cacheCreateCard(characterId, customImage, language) {
|
||||||
var cacheKey = `img:${req.params.charaId}`;
|
const cacheKey = `img:${characterId}:${customImage}:${language}`;
|
||||||
var ttl = 60 * 60 * 4; // 4 hours
|
|
||||||
|
|
||||||
diskCache.wrap(cacheKey,
|
return diskCache.wrap(cacheKey, async () => {
|
||||||
// called if the cache misses in order to generate the value to cache
|
await creator.ensureInit().catch(error => { throw new Error(`Init failed with: ${error}`) });
|
||||||
function (cb) {
|
const image = await creator.createCard(characterId, customImage, language).catch(error => { throw new Error(`Create card failed with: ${error}`) });
|
||||||
creator.ensureInit().then(() => creator.createCard(req.params.charaId), (reason) => cb('Init failed: ' + reason, null)).then(image => cb(null, {
|
|
||||||
binary: {
|
|
||||||
image: image,
|
|
||||||
}
|
|
||||||
})).catch((reason) => cb('createCard failed: ' + reason, null));
|
|
||||||
},
|
|
||||||
// Options, see node-cache-manager for more examples
|
|
||||||
{ ttl: ttl },
|
|
||||||
function (err, result) {
|
|
||||||
if (err !== null) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).send({status: "error", reason: err});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({status: "ok", url: `/characters/id/${req.params.charaId}.png`});
|
return {
|
||||||
}
|
binary: {
|
||||||
);
|
image,
|
||||||
})
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/prepare/name/:world/:charName', async (req, res) => {
|
function getOriginalQueryString(req) {
|
||||||
var id = await getCharIdByName(req.params.world, req.params.charName);
|
const url = new URL(req.originalUrl, 'http://example.org');
|
||||||
|
return url.search;
|
||||||
|
}
|
||||||
|
|
||||||
if (id === undefined) {
|
app.get('/prepare/id/:characterId', limiter, (req, res, next) => {
|
||||||
res.status(404).send({status: "error", reason: "Character not found."});
|
const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect(`/prepare/id/${id}`);
|
cacheCreateCard(req.params.characterId, null, language)
|
||||||
})
|
.then(() => {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'ok',
|
||||||
|
url: `/characters/id/${req.params.characterId}.png`,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/characters/id/:charaId.png', async (req, res) => {
|
app.get('/prepare/name/:world/:characterName', limiter, (req, res, next) => {
|
||||||
var cacheKey = `img:${req.params.charaId}`;
|
getCharacterIdByName(req.params.world, req.params.characterName)
|
||||||
var ttl = 60 * 60 * 4; // 4 hours
|
.then(characterId => {
|
||||||
|
if (characterId == null) {
|
||||||
|
res.status(404).send({ status: 'error', reason: 'Character not found.' });
|
||||||
|
} else {
|
||||||
|
res.redirect(`/prepare/id/${characterId}${getOriginalQueryString(req)}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
diskCache.wrap(cacheKey,
|
app.get('/characters/id/:characterId.png', limiter, (req, res, next) => {
|
||||||
// called if the cache misses in order to generate the value to cache
|
const language = typeof req.query.lang === 'string' && supportedLanguages.includes(req.query.lang) ? req.query.lang : supportedLanguages[0];
|
||||||
function (cb) {
|
|
||||||
creator.ensureInit().then(() => creator.createCard(req.params.charaId), (reason) => cb('Init failed: ' + reason, null)).then(image => cb(null, {
|
|
||||||
binary: {
|
|
||||||
image: image,
|
|
||||||
}
|
|
||||||
})).catch((reason) => cb('createCard failed: ' + reason, null));
|
|
||||||
},
|
|
||||||
// Options, see node-cache-manager for more examples
|
|
||||||
{ ttl: ttl },
|
|
||||||
function (err, result) {
|
|
||||||
if (err !== null) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).send({status: "error", reason: err});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var image = result.binary.image;
|
cacheCreateCard(req.params.characterId, null, language)
|
||||||
|
.then(result => {
|
||||||
|
const image = result.binary.image;
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'image/png',
|
'Cache-Control': 'public, max-age=14400',
|
||||||
'Content-Length': image.length,
|
'Content-Length': Buffer.byteLength(image),
|
||||||
'Cache-Control': 'public, max-age=14400'
|
'Content-Type': 'image/png',
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end(image, 'binary');
|
res.end(image, 'binary');
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
var usedStreams = ['image'];
|
app.get('/characters/id/:characterId', (req, res) => {
|
||||||
// you have to do the work to close the unused files
|
res.redirect(`/characters/id/${req.params.characterId}.png${getOriginalQueryString(req)}`);
|
||||||
// to prevent file descriptors leak
|
});
|
||||||
for (var key in result.binary) {
|
|
||||||
if (!result.binary.hasOwnProperty(key)) continue;
|
|
||||||
if (usedStreams.indexOf(key) < 0
|
|
||||||
&& result.binary[key] instanceof Stream.Readable) {
|
|
||||||
if (typeof result.binary[key].close === 'function') {
|
|
||||||
result.binary[key].close(); // close the stream (fs has it)
|
|
||||||
} else {
|
|
||||||
result.binary[key].resume(); // resume to the end and close
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/characters/id/:charaId', async (req, res) => {
|
app.get('/characters/name/:world/:characterName.png', limiter, (req, res, next) => {
|
||||||
res.redirect(`/characters/id/${req.params.charaId}.png`);
|
getCharacterIdByName(req.params.world, req.params.characterName)
|
||||||
})
|
.then(characterId => {
|
||||||
|
if (characterId == null) {
|
||||||
|
res.status(404).send({ status: 'error', reason: 'Character not found.' });
|
||||||
|
} else {
|
||||||
|
res.redirect(`/characters/id/${characterId}${getOriginalQueryString(req)}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/characters/name/:world/:charName.png', async (req, res) => {
|
app.get('/characters/name/:world/:characterName', (req, res) => {
|
||||||
var id = await getCharIdByName(req.params.world, req.params.charName);
|
res.redirect(`/characters/name/${req.params.world}/${req.params.characterName}.png${getOriginalQueryString(req)}`);
|
||||||
|
});
|
||||||
if (id === undefined) {
|
|
||||||
res.status(404).send({status: "error", reason: "Character not found."});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect(`/characters/id/${id}.png`);
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/characters/name/:world/:charName', async (req, res) => {
|
|
||||||
res.redirect(`/characters/name/${req.params.world}/${req.params.charName}.png`);
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
res.redirect('https://github.com/ArcaneDisgea/XIV-Character-Cards');
|
res.redirect('https://github.com/xivapi/XIV-Character-Cards');
|
||||||
})
|
});
|
||||||
|
|
||||||
|
app.use((error, req, res, next) => {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
reason: error instanceof Error ? error.stack : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Listening at http://localhost:${port}`)
|
console.log(`Listening at http://localhost:${port}`);
|
||||||
})
|
creator.ensureInit().then(() => console.log('CardCreator initialization complete'));
|
||||||
|
});
|
||||||
|
|
13
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "xiv-character-cards",
|
"name": "xiv-character-cards",
|
||||||
"description": "API to create fancy cards for FFXIV characters based on their Lodestone data, hosted at https://ffxiv-character-cards.herokuapp.com.",
|
"description": "API to create fancy cards for FFXIV characters based on their Lodestone data, hosted at https://ffxiv-character-cards.herokuapp.com.",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"main": "create-card.js",
|
"main": "create-card.js",
|
||||||
"repository": "https://github.com/xivapi/XIV-Character-Cards.git",
|
"repository": "https://github.com/xivapi/XIV-Character-Cards.git",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
|
@ -16,15 +16,16 @@
|
||||||
"docs:deploy": "npm run docs:generate && node gh-pages.js"
|
"docs:deploy": "npm run docs:generate && node gh-pages.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cache-manager-fs": "^1.0.8",
|
"cache-manager": "^3.4.4",
|
||||||
"cache-manager-fs-binary": "^1.0.4",
|
"cache-manager-fs-binary": "^1.0.4",
|
||||||
"canvas": "^2.6.1",
|
"canvas": "^2.8.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"node-fetch": "^2.6.1"
|
"express-rate-limit": "^5.5.0",
|
||||||
|
"node-fetch": "^2.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gh-pages": "^3.2.0",
|
"gh-pages": "^3.2.3",
|
||||||
"jsdoc": "^3.6.7",
|
"jsdoc": "^3.6.7",
|
||||||
"nodemon": "^2.0.7"
|
"nodemon": "^2.0.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
94
readme.md
|
@ -1,78 +1,76 @@
|
||||||
# XIV Character Cards
|
# XIV Character Cards
|
||||||
|
|
||||||

|

|
||||||
[](https://xivapi.github.io/XIV-Character-Cards/)
|
[](https://xivapi.github.io/XIV-Character-Cards/)
|
||||||
|
|
||||||
API to create fancy cards for FFXIV characters based on their Lodestone data, hosted at https://ffxiv-character-cards.herokuapp.com.
|
Library and API to create fancy cards for FFXIV characters based on their Lodestone data, powered by [xivapi.com](https://xivapi.com/) and hosted at [https://ffxiv-character-cards.herokuapp.com](https://ffxiv-character-cards.herokuapp.com).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Endpoints
|
## API
|
||||||
|
|
||||||
### Getting images
|
All API calls support the `lang` query parameter to create a character card with information in the specified language. The supported languages are the same as [xivapi.com](https://xivapi.com/docs/Common-Features#language), which are English (en), Japanese (ja), German (de) and French (fr).
|
||||||
|
|
||||||
``https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png``
|
E.g. a request for a german character card would look like this: ``https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png?lang=de``
|
||||||
<br>Get the PNG for a character by its Lodestone ID.
|
|
||||||
|
### Getting a card for a character by its Lodestone ID
|
||||||
|
|
||||||
|
``GET https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png``
|
||||||
|
|
||||||
|
### Getting card for a character by its world and name
|
||||||
|
|
||||||
|
``GET https://ffxiv-character-cards.herokuapp.com/characters/name/<WORLD>/<CHARACTER NAME>.png``
|
||||||
|
<br>**Note:** This is considerably slower than the creation by ID, since the character has to be looked up in the Lodestone first.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
``https://ffxiv-character-cards.herokuapp.com/characters/name/<WORLD>/<CHARACTER NAME>.png``
|
If you are using this API together with an application that requires the API to respond very quickly, like Discord, you may need to ask it to "prepare" the card image for a character beforehand. The API will reply with its status, and in case of success, the URL to the final image.
|
||||||
<br>Get the PNG for a character by its world and name.
|
|
||||||
|
|
||||||
### Requesting images to be cached
|
|
||||||
If you are using this API together with an application that requires the API to respond very quickly, like Discord, you need to ask it to "prepare" the image for a character beforehand.
|
|
||||||
|
|
||||||
``https://ffxiv-character-cards.herokuapp.com/prepare/id/<LODESTONE ID>``
|
|
||||||
<br>Request a character image to be cached by its Lodestone ID.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
``https://ffxiv-character-cards.herokuapp.com/prepare/name/<WORLD>/<CHARACTER NAME>``
|
|
||||||
<br>Request a character image to be cached by its world and name.
|
|
||||||
|
|
||||||
The API will reply with its status, and in case of success, the URL to the final image.
|
|
||||||
``{"status":"ok","url":"/characters/id/123456789.png"}``
|
``{"status":"ok","url":"/characters/id/123456789.png"}``
|
||||||
|
|
||||||
## Using in your application
|
### Requesting a card to be cached for a character by its Lodestone ID
|
||||||
|
|
||||||
```
|
``GET https://ffxiv-character-cards.herokuapp.com/prepare/id/<LODESTONE ID>``
|
||||||
|
|
||||||
|
### Requesting a card to be cached for a character by its world and name
|
||||||
|
|
||||||
|
``GET https://ffxiv-character-cards.herokuapp.com/prepare/name/<WORLD>/<CHARACTER NAME>``
|
||||||
|
|
||||||
|
## Library
|
||||||
|
|
||||||
|
To use the card creator as a library in your Node.JS application, first install it as a dependency with:
|
||||||
|
|
||||||
|
```sh
|
||||||
yarn add xiv-character-cards
|
yarn add xiv-character-cards
|
||||||
# or
|
# or
|
||||||
npm i xiv-character-cards
|
npm i xiv-character-cards
|
||||||
```
|
```
|
||||||
|
|
||||||
You will receive a PNG-buffer for you to use in your bot or application.<br>Check ``index.js`` for other usage examples.
|
You can then instantiate the class `CardCreator` from the library, call the asynchronous `insureInit()` function to make sure all resources are loaded and then use the asynchronous `createCard()` function with your characters Lodestone ID. You will receive a promise that resolves to a `Buffer` of the PNG image of your card, that you can use in your bot or application.
|
||||||
|
|
||||||
### Example
|
Check the [library documentation](https://xivapi.github.io/XIV-Character-Cards/) for more details.
|
||||||
|
|
||||||
|
> **Note:** The API server is not published as an NPM package, so if you want to host it yourself, clone the [Github repository](https://github.com/xivapi/XIV-Character-Cards) and put the Express.JS webserver defined in the `index.js` file behind a reverse proxy.
|
||||||
|
|
||||||
|
### Library example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { CardCreator } = require("xiv-character-cards");
|
const { CardCreator } = require("xiv-character-cards");
|
||||||
const fs = require("fs");
|
const { writeFileSync } = require("fs");
|
||||||
|
|
||||||
const card = new CardCreator();
|
const creator = new CardCreator();
|
||||||
const lodestoneid = "13821878";
|
const lodestoneId = "13821878";
|
||||||
|
|
||||||
function example(cb) {
|
async function example() {
|
||||||
card.ensureInit()
|
await creator.ensureInit();
|
||||||
.then(
|
return creator.createCard(lodestoneId);
|
||||||
() => card.createCard(lodestoneid),
|
|
||||||
(reason) => cb("Init failed: " + reason, null)
|
|
||||||
)
|
|
||||||
.then((image) =>
|
|
||||||
cb(null, {
|
|
||||||
binary: {
|
|
||||||
image: image,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch((reason) => cb("createCard failed: " + reason, null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
example((err, response) => {
|
example()
|
||||||
const buffer = response.binary.image;
|
.then(card => {
|
||||||
fs.writeFileSync(`./${lodestoneid}.png`, response.binary.image, (err) => {
|
writeFileSync(`./${lodestoneId}.png`, card);
|
||||||
if (err) {
|
})
|
||||||
console.log(err);
|
.catch(error => {
|
||||||
}
|
console.error('Creator initialization or card creation failed!');
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 596 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |