Tropy to Tumblr plugin

Hey, Im building a plugin with chatgpt to export any image directly to tumblr tagged, but I’m new to coding and don’t know anymore why my code says that “No items selected or found” in the log.

Full log:

Summary

{“level”:30,“time”:1735582632617,“type”:“renderer”,“name”:“project”,“msg”:“plugins scanned: 1”}
{“level”:30,“time”:1735582632636,“type”:“renderer”,“name”:“project”,“msg”:“plugins loaded: 1”}
{“level”:30,“time”:1735582632773,“type”:“renderer”,“name”:“project”,“msg”:“restoring projectFiles@tropy”}
{“level”:30,“time”:1735582632774,“type”:“renderer”,“name”:“project”,“msg”:“restoring recent@tropy”}
{“level”:30,“time”:1735582632774,“type”:“renderer”,“name”:“project”,“msg”:“restoring settings@tropy”}
{“level”:30,“time”:1735582632774,“type”:“renderer”,“name”:“project”,“msg”:“restoring ui@tropy”}
{“level”:30,“time”:1735582632775,“type”:“renderer”,“name”:“project”,“mode”:“w+”,“msg”:“open db C:\Users\j\AppData\Roaming\Tropy\ontology.db”}
{“level”:30,“time”:1735582632777,“type”:“browser”,“name”:“main”,“msg”:“ready after 148.94091796875ms”}
{“level”:30,“time”:1735582632847,“type”:“renderer”,“name”:“project”,“mode”:“webgl”,“resolution”:1,“msg”:“Esper.instance created with webgl renderer”}
{“level”:30,“time”:1735582632856,“type”:“renderer”,“name”:“project”,“mode”:“w”,“msg”:“open db C:\Users\j\Documents\Catalog56.tropy\project.tpy”}
{“level”:30,“time”:1735582632872,“type”:“renderer”,“name”:“project”,“msg”:“project ready 371ms [dom:108ms init:37ms load:129ms]”}
{“level”:30,“time”:1735582632888,“type”:“renderer”,“name”:“project”,“msg”:“restoring project.watch@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring project.watch@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring nav@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring notepad@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring esper@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring imports@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring sidebar@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632889,“type”:“renderer”,“name”:“project”,“msg”:“restoring panel@3a2bcb3a-747a-4915-a577-356835c2f44c”}
{“level”:30,“time”:1735582632891,“type”:“renderer”,“name”:“project”,“mode”:“w”,“msg”:“open db C:\Users\j\Documents\Catalog56.tropy\project.tpy”}
{“level”:30,“time”:1735582632892,“type”:“renderer”,“name”:“project”,“mode”:“w”,“msg”:“open db C:\Users\j\Documents\Catalog56.tropy\project.tpy”}
{“level”:30,“time”:1735582636192,“type”:“renderer”,“name”:“project”,“plugin”:“tropy-to-tumblr”,“msg”:“Export items:”}
{“level”:30,“time”:1735582636192,“type”:“renderer”,“name”:“project”,“plugin”:“tropy-to-tumblr”,“msg”:“No items selected or found.”}

Also I’ll paste the index.js:

// index.js
const fs = require(‘fs’);
const path = require(‘path’);
const FormData = require(‘form-data’);
const OAuth = require(‘oauth-1.0a’);
const crypto = require(‘crypto’);
// node-fetch 2.x syntax:
const fetch = require(‘node-fetch’);

class TropyToTumblr {
/**

  • Tropy plugin constructor.
  • @param {Object} options - Config from package.json ‘options’
  • @param {Object} context - Tropy context (logger, dialog, etc.)
    */
    constructor(options, context) {
    this.options = options;
    this.context = context;
    this.logger = context.logger;
// Load user-defined fields, or defaults from package.json "options"
this.blogName = options.blogIdentifier || 'cataleg56';
this.consumerKey = options.consumerKey || '';
this.consumerSecret = options.consumerSecret || '';
this.token = options.token || '';
this.tokenSecret = options.tokenSecret || '';

// Initialize OAuth 1.0
this.oauth = OAuth({
  consumer: { key: this.consumerKey, secret: this.consumerSecret },
  signature_method: 'HMAC-SHA1',
  hash_function(baseString, key) {
    return crypto
      .createHmac('sha1', key)
      .update(baseString)
      .digest('base64');
  }
});

}

/**

  • The export() hook is called when user chooses
  • File → Export → TropyToTumblr
  • @param {Array} items - selected Tropy items (JSON-LD), or all items if none selected
    */
    async export(items) {
    // Log what Tropy actually passes
    this.logger.info(‘Export items:’, items);
// If Tropy passes nothing or an empty array, log and return
if (!Array.isArray(items) || items.length === 0) {
  this.logger.info('No items selected or found.');
  return;
}

for (const item of items) {
  // Gather local photo file paths via .photo[].path
  const photoPaths = this.findLocalPhotos(item);

  if (photoPaths.length === 0) {
    this.logger.info(`No photos found in item: ${item.title || '[Untitled]'}`);
    continue;
  }

  // Post each item as a single Tumblr photo post
  this.logger.info(`Posting ${photoPaths.length} photo(s) from: ${item.title || '[Untitled]'}`);
  await this.postPhotosToTumblr(photoPaths, item.title || '');
}

}

/**

  • Extract local file paths from Tropy’s JSON-LD:
    • item[“@graph”] is an array of nodes
    • each node may have a “photo” array
    • each photo entry has “path”: “C:\Users\j\…”
      */
      findLocalPhotos(item) {
      const photoPaths = ;
      const graph = item[‘@graph’];
if (!Array.isArray(graph)) {
  return photoPaths;
}

for (const node of graph) {
  if (Array.isArray(node.photo)) {
    for (const p of node.photo) {
      // Check that p.path is valid and file exists
      if (p.path && fs.existsSync(p.path)) {
        photoPaths.push(p.path);
      }
    }
  }
}

return photoPaths;

}

/**

  • Post photos to Tumblr using OAuth 1.0
  • @param {Array} photoPaths - array of local image paths
  • @param {string} caption - optional text for the Tumblr post (the Tropy item title)
    */
    async postPhotosToTumblr(photoPaths, caption) {
    // Tumblr API endpoint
    const url = https://api.tumblr.com/v2/blog/${this.blogName}.tumblr.com/post;
// Build the multipart form data
const form = new FormData();
form.append('type', 'photo');
form.append('caption', caption);

// "data[]" param for each image
for (const filepath of photoPaths) {
  form.append('data[]', fs.createReadStream(filepath));
}

// Prepare request data for OAuth signature
const requestData = {
  url,
  method: 'POST',
  data: {}
};
const token = {
  key: this.token,
  secret: this.tokenSecret
};

// Generate OAuth header
const oauthHeaders = this.oauth.toHeader(
  this.oauth.authorize(requestData, token)
);

// Merge OAuth header + form-data headers
const fetchHeaders = {
  ...oauthHeaders,
  ...form.getHeaders()
};

try {
  const response = await fetch(url, {
    method: 'POST',
    headers: fetchHeaders,
    body: form
  });
  const json = await response.json();

  if (!response.ok) {
    this.logger.error('Tumblr API error:', json);
  } else {
    this.logger.info('Posted to Tumblr successfully:', json);
  }
} catch (err) {
  this.logger.error('Failed to post photos to Tumblr:', err);
}

}
}

module.exports = TropyToTumblr;

and the package.json:

{
“name”: “tropy-to-tumblr”,
“productName”: “TropyToTumblr”,
“version”: “1.0.0”,
“description”: “A Tropy plugin to export images to Tumblr (no tumblr.js)”,
“author”: “Your Name”,
“main”: “./index.js”,
“icon”: “./icon.svg”,
“license”: “MIT”,

“hooks”: {
“export”: true,
“import”: false
},

“options”: [
{
“field”: “blogIdentifier”,
“type”: “string”,
“default”: “cataleg56”,
“label”: “Your Tumblr blog name (without .tumblr.com)”
},
{
“field”: “consumerKey”,
“type”: “string”,
“default”: “kg5w6bS33HkFcnrx7orlSxtoxmtbowiARhiDQM3udZlUhmfSMG”,
“label”: “Consumer Key”
},
{
“field”: “consumerSecret”,
“type”: “string”,
“default”: “ZWmbU11xdOT9vsqDVkYZv9BWXl75bafDNICmvQZeGwBVFr54Yw”,
“label”: “Consumer Secret”
},
{
“field”: “token”,
“type”: “string”,
“default”: “sVNtX9LFaP6H7Xf3E4WWHduIPJFRnZY9zuKk1uvqWFK2jQ2fZb”,
“label”: “Access Token”
},
{
“field”: “tokenSecret”,
“type”: “string”,
“default”: “kRIjPATQD3x4tjoN6wMpAyiZmNbDmSuLQZwerWCzcLKWWCSIuf”,
“label”: “Token Secret”
}
],

“dependencies”: {
“oauth-1.0a”: “^2.2.6”,
“node-fetch”: “^2.6.9”,
“form-data”: “^4.0.0”
}
}

Thanks for the help!

The items passed to the export plugin hook are passed as a JSON-LD object; your code is assuming it’s a normal array, but it’s a JSON-LD graph instead. It’s not an Array and it doesn’t have a length property so your code reports that there is no data there.

I’d recommend looking at some of the existing plugins for reference. In particular the archive and Omeka plugin would seem pertinent.

Hello, thank you for the early response!

I got the plugin running!! You can export any images directly to your Tumblr blog with its tags!

Here it is on github: TropyToTumblr

1 Like

Awesome, thanks for sharing!