Was die rechtliche Seite angeht: Das Urheberrecht liegt beim Nutzer, sollte er denn der Urheber sein. In Deutschland ist das nicht übertragbar und mit dem Akzeptieren der Instagram AGB wird gewährt man auch nur ein Nutzungsrecht.
Nach etwas Recherche fand ich heraus, dass die ganzen Plugins, die nur mit dem Benutzernamen auskamen, die URL https://www.instagram.com/[username]/?__a=1 nutzten. Diese funktioniert immer noch aber nur wenn die Cookies für die Session gesetzt sind. Sprich man muss eingeloggt sein.
Daten von Instagram laden
Wir benötigen zwei Routen. Eine zum Laden der Links zu den Bildern und Posts und eine zum Laden der Bilder. Die Metadaten werden gecached damit Instagram bei zu vielen Anfragen nicht einfach den Stecker zieht. Wie oben schon erwähnt ist in diesem speziellen Fall der Download der Bilder okay. Das machen wir auch, wenn diese Angefragt werden. Dabei optimieren wir auf Dateigröße und verringern die Auflösung. Der Downstream wird geklont. Einer landet direkt auf der Festplatte wo er später wieder abgerufen werden kann und der andere wird zum Client gesendet.
./api/routes/instagram.js const { promises: fs, createWriteStream } = require('fs');
const { get } = require('https');
const got = require('got');
const path = require('path');
const sharp = require('sharp');
const { parse } = require('../src/parse');
module.exports = async function(fastify, opts) {
/*
Die Funktion fetchData() wrapped get() aus dem nativen https Modul
in ein Promise, welches in ein Objekt aufgelöst wird, wenn der Stream
das End Event emittiert.
*/
const fetchData = user => new Promise(async (resolve, reject) => {
get({
hostname: opts.hostname,
path: opts.path(user),
headers: {
/*
Die Cookies wurden in app.js in die Redis Instanz geschrieben.
*/
cookie: await fastify.redis.get('cookies'),
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36'
}
}, async res => {
/*
Dem Array chunks[] wird beim auslösen des Data Events den Buffer
des Sockets angehangen. Beim Ende des Streams wird es dann zu einem
String zusammen gesetzt und dieser wird der parse() Funktion
übergebn. Durch Probleme mit der Zeichenkodierung konnte ich nicht
einfach alles mit JSON.parse() in ein Objekt konvertieren.
*/
const chunks = [];
res.on('data', data => chunks.push(data.toString('utf-8')));
res.on('error', error => reject(error));
res.on('end', async () => resolve({ user: user, media: parse(chunks.join()) }));
});
});
/*
Das ist die öffentliche Route zu den JSON Metadaten, die der
Client benötigt um den Markup zu erzeugen.
*/
fastify.get('/api/instagram/:user', async (req, res) => {
const { redis } = fastify;
const user = req.params.user;
let data = await redis.get(user);
/*
Prüfen ob die Daten im Cache vorhanden sind.
*/
if (!data || data.includes(null)) {
/*
Falls sie das nicht sind wird eine HTTP Anfrage an Instagram
gesendet, die benötigten Daten in ein Objekt geschrieben und
gecached.
*/
data = await fetchData(user);
data = JSON.stringify(data).replace(/null,/g, '');
redis.set(user, data);
redis.expire(user, opts.expire);
fastify.log.info(`cache for ${user} updated`);
}
/*
Die Daten werden zum Client gesendet.
*/
res.header('Content-Type', 'application/json; charset=utf-8');
res.send(data);
});
/*
Laden und senden der Bilder.
*/
fastify.get('/api/static/:image', async (req, res) => {
const image = req.params.image;
const [filename] = image.split('?');
const file = path.join(__dirname, '../static', filename);
try {
/*
Es wird geprüft ob das Bild schon vorhanden ist.
*/
await fs.stat(file);
res.sendFile(filename);
} catch(error) {
if (error.code === 'ENOENT') {
/*
Sollte das nicht der Fall sein, wird die URL in ihre Bestandteile
zerlegt um aus den Parametern die URL zu Instagram zu extrahieren.
*/
const url = req.url.split('?');
/*
Es werden drei Streams initialisiert. Einer ist der HTTP
Stream, der von Instagram downloaded, der andere ist ein
Dateistream für das Speichern auf den lokalen Datenträger
und der Dritte ist der von Sharp, welcher das Bild manipuliert.
*/
const gstream = got.stream(new URL(`${url[1]}?${url[2]}`));
const fstream = createWriteStream(file);
const sharpStream = sharp({
failOnError: false
});
/*
Wir senden schon mal alles okay. Es ist ein JPEG.
*/
res.raw.writeHead(200, { 'Content-Type': 'image/jpeg' });
/*
Der HTTP stream wird in Sharp gepiped (geleitet).
*/
gstream.pipe(sharpStream);
/*
Sharp macht daraus zwei Streams. Der eine wird gespeichert und
der andere wird zum Client gesendet.
*/
sharpStream
.clone()
.resize({ width: 680 })
.jpeg({ quality: 80 })
.pipe(res.raw);
sharpStream
.clone()
.resize({ width: 680 })
.jpeg({ quality: 80 })
.pipe(fstream);
sharpStream.on('error', error => fastify.log.error(error.message));
sharpStream.on('end', () => fastify.log.info(`${file} downloaded and resized`));
} else {
fastify.log.error(error.message);
throw error;
}
}
});
}
./api/src/parse.js const extract = (str, target) => str.split(target)[1].split('","')[0].replace(/,/g, '');
function parse(data) {
if (typeof data !== 'string') {
throw new Error('argument should be a string');
}
let buffer = data;
const arr = buffer.split('"__typename":"GraphImage",');
arr.splice(0, 1);
const result = arr.map(line => {
if (line.includes('"shortcode":"') && line.includes('"display_url":"')) {
return {
shortcode: extract(line, '"shortcode":"'),
display_url: extract(line, '"display_url":"'),
}
}
});
return result;
}
exports.parse = parse;
./api/routes/index.js module.exports = {
access: require('./access-token'),
// gatsby: require('./gatsby'),
instagram: require('./instagram'),
// private: require('./private'),
};
Weitere Blog Posts aus dieser Serie
Gatsby Webhook Service
Gatsby Webhook Service (2) - Das große Containern
Gatsby Webhook Service (3) - JWT Authentifizierung
Gatsby Webhook Service (4) - Instagram
Gatsby Webhook Service (5) - Gatsby