<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"

>
<channel>
	<title>Sacha Chua - tag - node</title>
	<atom:link href="https://sachachua.com/blog/tag/node/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/tag/node" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/tag/node/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
  
	<lastBuildDate>Thu, 09 Jul 2026 00:30:29 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>Windows: Pipe output to your clipboard, or how I&#8217;ve been using NodeJS and Org Mode together</title>
		<link>https://sachachua.com/blog/2015/02/windows-pipe-output-to-your-clipboard-or-how-ive-been-using-node-org-mode-together/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Wed, 18 Feb 2015 13:00:00 GMT</pubDate>
    <category>emacs</category>
<category>geek</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=27830</guid>
		<description><![CDATA[<p>It&apos;s not easy </p><div class="sketch-thumbnail"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2015-01-12%20Coding%20improvements%20&#45;&#45;%20index%20card%20%23workflow%20%23coding.png" data-src="https://sketches.sachachua.com/static/2015-01-12%20Coding%20improvements%20&#45;&#45;%20index%20card%20%23workflow%20%23coding.png" data-title="2015-01-12 Coding improvements &#45;&#45; index card #workflow #coding" data-w="1498" data-h="899"><picture>
      <img src="https://sketches.sachachua.com/thumbnails/2015-01-12%20Coding%20improvements%20&#45;&#45;%20index%20card%20%23workflow%20%23coding.png" width="" height="" alt="2015-01-12 Coding improvements &#45;&#45; index card #workflow #coding" loading="lazy" decoding="async">
      <figcaption>2015-01-12 Coding improvements &#45;&#45; index card #workflow #coding</figcaption>
    </picture></a></div> instead of one of the more scriptable operating systems out there, but I stay on it because I like the drawing programs. Cygwin and Vagrant fill enough gaps to keep me mostly sane. (Although maybe I should work up the courage to <a href="http://www.everydaylinuxuser.com/2014/05/install-ubuntu-1404-alongside-windows.html">dual-boot Windows 8.1 and a Linux distribution</a>, and then get my <a href="http://virantha.com/2014/03/17/one-touch-scanning-with-fujitsu-scansnap-in-linux/">ScanSnap working</a>.)<p></p>
<p>Anyway, I&apos;m making do. Thanks to <a href="http://nodejs.org/">Node</a> and the abundance of libraries available through NPM, Javascript is shaping up to be a surprisingly useful scripting language.</p>
<p>After I used the Flickr API library for Javascript to <a href="https://sachachua.com/blog/2015/02/de-dupe-link-using-flickr-api-neaten-archive-link-sketches-blog-posts/">cross-reference my Flickr archive with my blog posts</a>, I looked around for other things I could do with it. <a href="http://webecoz.com/">photoSync</a> occasionally didn&apos;t upload new pictures I added to its folders (or at least, not as quickly as I wanted). I wanted to replace photoSync with my own script that would:</p>
<ul class="org-ul">
<li>upload the picture only if it doesn&apos;t already exist,</li>
<li>add tags based on the filename,</li>
<li>add the photo to my Sketchbook photoset,</li>
<li>move the photo to the &quot;To blog&quot; folder, and</li>
<li>make it easy for me to refer to the Flickr image in my blog post or index.</li>
</ul>
<p>The <a href="https://github.com/chbrown/flickr-with-uploads">flickr-with-uploads</a> library made it easy to upload images and retrieve information, although the format was slightly different from the Flickr API library I used previously. (In retrospect, I should&apos;ve checked the <a href="https://www.npmjs.com/package/flickrapi">Flickr API</a> documentation first &#8211; there&apos;s an example upload request right on the main page. Oh well! Maybe I&apos;ll change it if I feel like rewriting it.)</p>
<p>I searched my existing photos to see if a photo with that title already existed. If it did, I displayed an Org-style list item with a link. If it didn&apos;t exist, I uploaded it, set the tags, added the item to the photo set, and moved it to the folder. Then I displayed an Org-style link, but using a plus character instead of a minus character, taking advantage of the fact that both + and &#8211; can be used for lists in Org.</p>
<p>While using <code>console.log(...)</code> to display these links in the terminal allowed me to mark and copy the link, I wanted to go one step further. Could I send the links directly to Emacs? I looked into getting <code>org-protocol</code> to work, but I was having problems figuring this out. (I solved those problems; details later in this post.)</p>
<p>What were some other ways I could get the information into Emacs aside from copying and pasting from the terminal window? Maybe I could put text directly into the clipboard. The <a href="https://www.npmjs.com/package/clipboard">node-clipboard</a> package didn&apos;t build for me and I couldn&apos;t get <a href="https://github.com/xavi-/node-copy-paste">node-copy-paste</a> to work either,about the node-copy-paste README told me about the existence of the <code>clip</code> command-line utility, which worked for me.</p>
<p>On Windows, <code>clip</code> allows you to pipe the output of commands into your clipboard. (There are similar programs for Linux or Mac OS X.) In Node, you can start a child process and communicate with it through pipes.</p>
<p>I got a little lost trying to figure out how to turn a string into a streamable object that I could set as the new standard input for the <code>clip</code> process I was going to spawn, but the solution turned out to be much simpler than that. Just <code>write(...)</code> to the appropriate stream, and call <code>end()</code> when you&apos;re done.</p>
<p>Here&apos;s the relevant bit of code that takes my <code>result</code> array and puts it into my clipboard:</p>
<blockquote><p>var child = cp.spawn(&apos;clip&apos;); child.stdin.write(result.join(&quot;\n&quot;)); child.stdin.end();</p></blockquote>
<p>Of course, to get to that point, I had to revise my script. Instead of letting all the callbacks finish whenever they wanted, I needed to be able to run some code after everything was done. I was a little familiar with the <code>async</code> library, so I used that. I copied the output to the clipboard instead of displaying it so that I could call it easily using ! (dired-do-shell-command) and get the output in my clipboard for easy yanking elsewhere, although I could probably change my batch file to pipe the result to <kbd>clip</kbd> and just separate the stderr stuff. Hmm. Anyway, here it is!</p>
<p><a href="https://gist.github.com/sachac/01b21572cccb0a558aa3">See this on Github</a></p>
<div class="org-src-container">
<pre class="src src-js2">/**
 * Upload the file to my Flickr sketchbook and then move it to
 * Dropbox/Inbox/To blog. Save the Org Mode links in the clipboard. -
 * means the photo already existed, + means it was uploaded.
 */

var async = require(&apos;async&apos;);
var cp = require(&apos;child_process&apos;);
var fs = require(&apos;fs&apos;);
var glob = require(&apos;glob&apos;);
var path = require(&apos;path&apos;);
var flickr = require(&apos;flickr-with-uploads&apos;);
var secret = require(&quot;./secret&quot;);
var SKETCHBOOK_PHOTOSET_ID = &apos;72157641017632565&apos;;
var BLOG_INBOX_DIRECTORY = &apos;c:\\sacha\\dropbox\\inbox\\to blog\\&apos;;
var api = flickr(secret.flickrOptions.api_key,
                 secret.flickrOptions.secret,
                 secret.flickrOptions.access_token,
                 secret.flickrOptions.access_token_secret);
var result = [];

function getTags(filename) {
  var tags = [];
  var match;
  var re = new RegExp(&apos;#([^ ]+)&apos;, &apos;g&apos;);
  while ((match = re.exec(filename)) !== null) {
    tags.push(match[1]);
  }
  return tags.join(&apos; &apos;);
}
// assert(getTags(&quot;foo #bar #baz qux&quot;) == &quot;bar baz&quot;);

function checkIfPhotoExists(filename, doesNotExist, existsFunction, done) {
  var base = path.basename(filename).replace(/.png$/, &apos;&apos;);
  api({method: &apos;flickr.photos.search&apos;,
       user_id: secret.flickrOptions.user_id,
       text: base},
      function(err, response) {
        var found = undefined;
        if (response &amp;&amp; response.photos[0].photo) {
          for (var i = 0; i &lt; response.photos[0].photo.length; i++) {
            if (response.photos[0].photo &amp;&amp; response.photos[0].photo[i][&apos;$&apos;].title == base) {
              found = i; break;
            }            
          }
        }
        if (found !== undefined) {
          existsFunction(response.photos[0].photo[found], done);
        } else {
          doesNotExist(filename, done);
        }
      });
}

function formatExistingPhotoAsOrg(photo, done) {
  var title = photo[&apos;$&apos;].title;
  var url = &apos;https://www.flickr.com/photos/&apos;
        + photo[&apos;$&apos;].owner
        + &apos;/&apos; + photo[&apos;$&apos;].id;
  result.push(&apos;- [[&apos; + url + &apos;][&apos; + title + &apos;]]&apos;);
  done();
}

function formatAsOrg(response) {
  var title = response.photo[0].title[0];
  var url = response.photo[0].urls[0].url[0][&apos;_&apos;];
  result.push(&apos;+ [[&apos; + url + &apos;][&apos; + title + &apos;]]&apos;);
}

function uploadImage(filename, done) {
  api({
    method: &apos;upload&apos;,
    title: path.basename(filename.replace(/.png$/, &apos;&apos;)),
    is_public: 1,
    hidden: 1,
    safety_level: 1,
    photo: fs.createReadStream(filename),
    tags: getTags(filename.replace(/.png$/, &apos;&apos;))
  }, function(err, response) {
    if (err) {
      console.log(&apos;Could not upload photo: &apos;, err);
      done();
    } else {
      var newPhoto = response.photoid[0];
      async.parallel(
        [
          function(done) {
            api({method: &apos;flickr.photos.getInfo&apos;,
                 photo_id: newPhoto}, function(err, response) {
                   if (response) { formatAsOrg(response); }
                   done();
                 });
          },
          function(done) {
            api({method: &apos;flickr.photosets.addPhoto&apos;,
                 photoset_id: SKETCHBOOK_PHOTOSET_ID,
                 photo_id: newPhoto}, function(err, response) {
                   if (!err) {
                     moveFileToBlogInbox(filename, done);
                   } else {
                     console.log(&apos;Could not add &apos; + filename + &apos; to Sketchbook&apos;);
                     done();
                   }
                 });
          }],
        function() {
          done();
        });
    }
  });
}

function moveFileToBlogInbox(filename, done) {
  fs.rename(filename, BLOG_INBOX_DIRECTORY + path.basename(filename),
            function(err) {
              if (err) { console.log(err); }
              done();
            });
}

var arguments = process.argv.slice(2);
async.each(arguments, function(item, done) {
  if (item.match(&apos;\\*&apos;)) {
    glob.glob(item, function(err, files) {
      if (!files) return;
      async.each(files, function(file, done) {
        checkIfPhotoExists(file, uploadImage, formatExistingPhotoAsOrg, done);
      }, function() {
        done();
      });
    });
  } else {
    checkIfPhotoExists(item, uploadImage, formatExistingPhotoAsOrg, done);
  }
}, function(err) {
  console.log(result.join(&quot;\n&quot;));
  var child = cp.spawn(&apos;clip&apos;);
  child.stdin.write(result.join(&quot;\n&quot;));
  child.stdin.end();
});
</pre>
</div>
<p>Wheeee! Hooray for automation. I made a Windows batch script like so:</p>
<p>up.bat</p>
<pre class="example">node g:\code\node\flickr-upload.js %*
</pre>
<p>and away I went. Not only did I have a handy way to process images from the command line, I could also mark the files in Emacs Dired with <code>m</code>, then type <code>!</code> to execute my <code>up</code> command on the selected images. Mwahaha!</p>
<p>Anyway, I thought I&apos;d write it up in case other people were curious about using Node to code little utilities, filling the clipboard in Windows, or getting data back into Emacs (sometimes the clipboard is enough).</p>
<p>Back to <code>org-protocol</code>, since I was curious about it. With <code>(require &apos;org-protocol) (server-start)</code>, <code>emacsclient org-protocol://store-link:/foo/bar</code> worked when I entered it at the command prompt. I was having a hard time getting it to work under Node, but eventually I figured out that:</p>
<ul class="org-ul">
<li>I needed to pass <code>-n</code> as one of the arguments to <code>emacsclient</code> so that it would return right away.</li>
<li>The <code>:</code> after <code>store-link</code> is important! I was passing <code>org-protocol://store-link/foo/bar</code> and wondering why it opened up a file called <code>bar</code>. <code>org-protocol://store-link:/foo/bar</code> was what I needed.</li>
</ul>
<p>I only just figured out that last bit while writing this post. Here&apos;s a small demonstration program:</p>
<div class="org-src-container">
<pre class="src src-js2">var cp = require(&apos;child_process&apos;);
var child = cp.execFile(&apos;emacsclient&apos;, [&apos;-n&apos;, &apos;org-protocol://store-link:/foo/bar&apos;]);
</pre>
</div>
<p>Yay!</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2015/02/2015-01-13-Using-Node-as-a-scripting-tool-index-card-javascript-nodejs-coding-scripting.png"><img loading="lazy" class="alignnone size-medium wp-image-27831" src="https://sachachua.com/blog/wp-content/uploads/2015/02/2015-01-13-Using-Node-as-a-scripting-tool-index-card-javascript-nodejs-coding-scripting-640x384.png" alt="2015-01-13 Using Node as a scripting tool &#45;&#45; index card #javascript #nodejs #coding #scripting" width="640" height="384" srcset="https://sachachua.com/blog/wp-content/uploads/2015/02/2015-01-13-Using-Node-as-a-scripting-tool-index-card-javascript-nodejs-coding-scripting-640x384.png 640w, https://sachachua.com/blog/wp-content/uploads/2015/02/2015-01-13-Using-Node-as-a-scripting-tool-index-card-javascript-nodejs-coding-scripting-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2015/02/2015-01-13-Using-Node-as-a-scripting-tool-index-card-javascript-nodejs-coding-scripting.png 1491w" sizes="(max-width: 640px) 100vw, 640px"></a></p>
<p></p><div class="sketch-thumbnail"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2015-01-13%20Emacs%20and%20the%20beginner's%20mind%20&#45;&#45;%20index%20card%20%23emacs%20%23beginner.png" data-src="https://sketches.sachachua.com/static/2015-01-13%20Emacs%20and%20the%20beginner's%20mind%20&#45;&#45;%20index%20card%20%23emacs%20%23beginner.png" data-title="2015-01-13 Emacs and the beginner's mind &#45;&#45; index card #emacs #beginner" data-w="1503" data-h="894"><picture>
      <img src="https://sketches.sachachua.com/thumbnails/2015-01-13%20Emacs%20and%20the%20beginner's%20mind%20&#45;&#45;%20index%20card%20%23emacs%20%23beginner.png" width="" height="" alt="2015-01-13 Emacs and the beginner's mind &#45;&#45; index card #emacs #beginner" loading="lazy" decoding="async">
      <figcaption>2015-01-13 Emacs and the beginner's mind &#45;&#45; index card #emacs #beginner</figcaption>
    </picture></a></div><p></p>

<p>You can <a href="https://sachachua.com/blog/2015/02/windows-pipe-output-to-your-clipboard-or-how-ive-been-using-node-org-mode-together/#comment">view 5 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2015%2F02%2Fwindows-pipe-output-to-your-clipboard-or-how-ive-been-using-node-org-mode-together%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>De-dupe and link: Using the Flickr API to neaten up my archive and link sketches to blog posts</title>
		<link>https://sachachua.com/blog/2015/02/de-dupe-link-using-flickr-api-neaten-archive-link-sketches-blog-posts/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Wed, 04 Feb 2015 13:00:00 GMT</pubDate>
    <category>development</category>
<category>geek</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=27764</guid>
		<description><![CDATA[<p>I&apos;ve been thinking about how to manage the relationships between my blog posts and my <a href="https://www.flickr.com/photos/sachac/">Flickr sketches</a>. Here&apos;s the flow of information:</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-06-Figuring-out-information-flow-index-card.png"><img loading="lazy" class="alignnone size-medium wp-image-27765" src="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-06-Figuring-out-information-flow-index-card-640x383.png" alt="2015-01-06 Figuring out information flow &#45;&#45; index card" width="640" height="383" srcset="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-06-Figuring-out-information-flow-index-card-640x383.png 640w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-06-Figuring-out-information-flow-index-card-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-06-Figuring-out-information-flow-index-card.png 1496w" sizes="(max-width: 640px) 100vw, 640px"></a></p>
<p></p><div class="sketch-thumbnail"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2015-01-06%20Abbreviations%20&#45;&#45;%20index%20card.png" data-src="https://sketches.sachachua.com/static/2015-01-06%20Abbreviations%20&#45;&#45;%20index%20card.png" data-title="2015-01-06 Abbreviations &#45;&#45; index card" data-w="1504" data-h="893"><picture>
      <img src="https://sketches.sachachua.com/thumbnails/2015-01-06%20Abbreviations%20&#45;&#45;%20index%20card.png" width="" height="" alt="2015-01-06 Abbreviations &#45;&#45; index card" loading="lazy" decoding="async">
      <figcaption>2015-01-06 Abbreviations &#45;&#45; index card</figcaption>
    </picture></a></div><p></p>
<p>I scan my sketches or draw them on the computer, and then I upload these sketches to Flickr using <a href="http://webecoz.com/">photoSync</a>, which synchronizes folders with albums. I include these sketches in my outlines and blog posts, and I update my <a href="https://sachachua.com/blog/index">index of blog posts</a> every month. I recently added a tweak to make it possible for people to <a href="https://sachachua.com/blog/2015/01/thoughts-context-connecting-posts-blog-post-index/?shareadraft=baba27682_54861cba96af6">go from a blog post to its index entry</a>, so it should be easier to see a post in context. I&apos;ve been thinking about keeping an additional info index to manage blog posts and sketches, including unpublished ones. We&apos;ll see how well that works. Lastly, I want to link my Flickr posts to my blog posts so that people can see the context of the sketch.</p>
<p>My higher goal is to be able to easily see the open ideas that I haven&apos;t summarized or linked to yet. There&apos;s no shortage of new ideas, but it might be interesting to revisit old ones that had a chance to simmer a bit. I wrote a little about this in <a href="https://sachachua.com/blog/2015/02/learning-artists-making-studies-ideas/?shareadraft=baba27754_54aad8179388e">Learning from artists: Making studies of ideas</a>. Let me flesh out what I want this archive to be like.</p>
<p><a href="https://sketches.sachachua.com/id/2015-01-05"><img loading="lazy" class="alignnone size-medium wp-image-27768" src="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Thinking-about-my-archive-index-card-640x380.png" alt="2015-01-05 Thinking about my archive &#45;&#45; index card" width="640" height="380" srcset="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Thinking-about-my-archive-index-card-640x380.png 640w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Thinking-about-my-archive-index-card-280x166.png 280w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Thinking-about-my-archive-index-card.png 1505w" sizes="(max-width: 640px) 100vw, 640px"><br>
2015.01.05 Thinking about my archive</a></p>
<p>When I pull on an idea, I&apos;d like to be able to see other open topics attached to it. I also want to be able to see open topics that might jog my memory.</p>
<p>How about the technical details? How can I organize my data so that I can get what I want from it?</p>
<p><a href="https://sketches.sachachua.com/id/2015-01-05"><img loading="lazy" class="alignnone size-medium wp-image-27769" src="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Figuring-out-the-technical-details-of-this-idea-or-visual-archive-I-want-index-card-640x382.png" alt="2015-01-05 Figuring out the technical details of this idea or visual archive I want &#45;&#45; index card" width="640" height="382" srcset="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Figuring-out-the-technical-details-of-this-idea-or-visual-archive-I-want-index-card-640x382.png 640w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Figuring-out-the-technical-details-of-this-idea-or-visual-archive-I-want-index-card-280x167.png 280w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-05-Figuring-out-the-technical-details-of-this-idea-or-visual-archive-I-want-index-card.png 1502w" sizes="(max-width: 640px) 100vw, 640px"><br>
2015.01.05 Figuring out the technical details of this idea or visual archive I want &#x2013; index card</a></p>
<p>Because blog posts link to sketches and other blog posts, I can model this as a <a href="http://en.wikipedia.org/wiki/Directed_graph">directed graph</a>. When I initially drew this, I thought I might be able to get away with an acyclic graph (no loops). However, since I habitually link to future posts (the time traveller&apos;s problem!), I can&apos;t make that simplifying assumption. In addition, a single item might be linked from multiple things, so it&apos;s not a simple tree (and therefore I can&apos;t use an outline). I&apos;ll probably start by extracting all the link information from my blog posts and then figuring out some kind of Org Mode-based way to update the graph.</p>
<p><img loading="lazy" class="alignnone size-medium wp-image-27767" src="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-07-Mapping-the-connections-in-my-blog-index-card-640x385.png" alt="2015-01-07 Mapping the connections in my blog &#45;&#45; index card" width="640" height="385" srcset="https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-07-Mapping-the-connections-in-my-blog-index-card-640x385.png 640w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-07-Mapping-the-connections-in-my-blog-index-card-280x168.png 280w, https://sachachua.com/blog/wp-content/uploads/2015/01/2015-01-07-Mapping-the-connections-in-my-blog-index-card.png 1489w" sizes="(max-width: 640px) 100vw, 640px"><br>
</p><div class="sketch-thumbnail"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2015-01-07%20Code%20insertion%20&#45;&#45;%20index%20card.png" data-src="https://sketches.sachachua.com/static/2015-01-07%20Code%20insertion%20&#45;&#45;%20index%20card.png" data-title="2015-01-07 Code insertion &#45;&#45; index card" data-w="1487" data-h="892"><picture>
      <img src="https://sketches.sachachua.com/thumbnails/2015-01-07%20Code%20insertion%20&#45;&#45;%20index%20card.png" width="" height="" alt="2015-01-07 Code insertion &#45;&#45; index card" loading="lazy" decoding="async">
      <figcaption>2015-01-07 Code insertion &#45;&#45; index card</figcaption>
    </picture></a></div><p></p>
<p>To get one step closer to being able to see open thoughts and relationships, I decided that my sketches on Flickr:</p>
<ul class="org-ul">
<li>should not have duplicates despite my past mess-ups, so that:
<ul class="org-ul">
<li>I can have an accurate count</li>
<li>it&apos;s easier for me to categorize</li>
<li>people get less confused</li>
</ul>
</li>
<li>should have hi-res versions if possible, despite the IFTTT recipe I tried that imported blog posts but unfortunately picked up the low-res thumbnails instead of the hi-res links</li>
<li>should link to the blog posts they&apos;re mentioned in, so that:
<ul class="org-ul">
<li>people can read more details if they come across a sketch in a search</li>
<li>I can keep track of which sketches haven&apos;t been blogged yet</li>
</ul>
</li>
</ul>
<p>I couldn&apos;t escape doing a bit of manual cleaning up, but I knew I could automate most of the fiddly bits. I installed <a href="https://www.npmjs.com/package/flickrapi">node-flickrapi</a> and <a href="https://github.com/cheeriojs/cheerio">cheerio</a> (for HTML parsing), and started playing.</p>
<div id="outline-container-unnumbered-1" class="outline-3">
<h3 id="unnumbered-1">Removing duplicates</h3>
<div id="text-unnumbered-1" class="outline-text-3">
<p>Most of the duplicates had resulted from the Great Renaming, when I added tags in the form of <code>#tag1</code> <code>#tag2</code> etc. to selected filenames. It turns out that adding these tags en-masse using Emacs&apos; writable Dired mode broke photoSync&apos;s ability to recognize the renamed files. As a result, I had files like this:</p>
<ul class="org-ul">
<li>2013-05-17 How I set up Autodesk Sketchbook Pro for sketchnoting.png</li>
<li>2013-05-17 How I set up Autodesk Sketchbook Pro for sketchnoting #tech #autodesk-sketchbook-pro #drawing.png</li>
</ul>
<p>This is neatly resolved by the following Javascript:</p>
<div class="org-src-container">
<pre class="src src-js2">exports.trimTitle = function(str) {
    return str.replace(/ &#45;&#45;.*$/g, &apos;&apos;).replace(/#[^ ]+/g, &apos;&apos;).replace(/[- _]/g, &apos;&apos;);
};
</pre>
</div>
<p>and a comparison function that compared the titles and IDs of two photos:</p>
<div class="org-src-container">
<pre class="src src-js2">exports.keepNewPhoto = function(oldPhoto, newPhoto) {
    if (newPhoto.title.length &gt; oldPhoto.title.length)
        return true;
    if (newPhoto.title.length &lt; oldPhoto.title.length)
        return false;
    if (newPhoto.id &lt; oldPhoto.id) 
        return true;
    return false;
};
</pre>
</div>
<p>So then this code can process the photos:</p>
<div class="org-src-container">
<pre class="src src-js2">exports.processPhoto = function(p, flickr) {
    var trimmed = exports.trimTitle(p.title);
    if (trimmed &amp;&amp; hash[trimmed] &amp;&amp; p.id != hash[trimmed].id) {
        // We keep the one with the longer title or the newer date
        if (exports.keepNewPhoto(hash[trimmed], p)) {
            exports.possiblyDeletePhoto(hash[trimmed], flickr);
            hash[trimmed] = p;
        }
        else if (p.id != hash[trimmed].id) {
            exports.possiblyDeletePhoto(p, flickr);
        }
    } else {
        hash[trimmed] = p;
    }
};
</pre>
</div>
<p>You can see the code on <a href="https://gist.github.com/7d27c90229a09650a307">Gist: duplicate_checker.js</a>.</p>
</div>
</div>
<div id="outline-container-unnumbered-2" class="outline-3">
<h3 id="unnumbered-2">High-resolution versions</h3>
<div id="text-unnumbered-2" class="outline-text-3">
<p>I couldn&apos;t easily automate this, but fortunately, the IFTTT script had only imported twenty images or so, clearly marked by a description that said: &quot;via sacha chua :: living an awesome life&#x2026;&quot;. I searched for each image, deleting the low-res entry if a high-resolution image was already in the system and replacing the low-res entry if that was the only one there.</p>
</div>
</div>
<div id="outline-container-unnumbered-3" class="outline-3">
<h3 id="unnumbered-3">Linking to blog posts</h3>
<div id="text-unnumbered-3" class="outline-text-3">
<p>This was the trickiest part, but also the most fun. I took advantage of the fact that WordPress transforms uploaded filenames in a mostly consistent way. I&apos;d previously added a bulk view that displayed any number of blog posts with very little additional markup, and I modified the relevant code in my theme to make parsing easier.</p>
<p><a href="https://gist.github.com/a20c80c4072c8b5ae961">See this on Gist:</a></p>
<div class="org-src-container">
<pre class="src src-js"><span class="org-comment-delimiter">/**</span>
<span class="org-comment"> * Adds &quot;Blogged&quot; links to Flickr for images that don&apos;t yet have &quot;Blogged&quot; in their description.</span>
<span class="org-comment"> * Command-line argument: URL to retrieve and parse</span>
<span class="org-comment"> */</span>

<span class="org-keyword">var</span> <span class="org-variable-name">secret</span> = require(<span class="org-string">&apos;./secret&apos;</span>);
<span class="org-keyword">var</span> <span class="org-variable-name">flickrOptions</span> = secret.flickrOptions;
<span class="org-keyword">var</span> <span class="org-variable-name">Flickr</span> = require(<span class="org-string">&quot;flickrapi&quot;</span>);
<span class="org-keyword">var</span> <span class="org-variable-name">fs</span> = require(<span class="org-string">&apos;fs&apos;</span>);
<span class="org-keyword">var</span> <span class="org-variable-name">request</span> = require(<span class="org-string">&apos;request&apos;</span>);
<span class="org-keyword">var</span> <span class="org-variable-name">cheerio</span> = require(<span class="org-string">&apos;cheerio&apos;</span>);
<span class="org-keyword">var</span> <span class="org-variable-name">imageData</span> = {};
<span class="org-keyword">var</span> <span class="org-variable-name">$</span>;

<span class="org-keyword">function</span> <span class="org-function-name">setDescriptionsFromURL</span>(<span class="org-variable-name">url</span>) {
  request(url, <span class="org-keyword">function</span>(<span class="org-variable-name">error</span>, <span class="org-variable-name">response</span>, <span class="org-variable-name">body</span>) {
    <span class="org-comment-delimiter">// </span><span class="org-comment">Parse the images</span>
    $ = cheerio.load(body);
    $(<span class="org-string">&apos;article&apos;</span>).each(<span class="org-keyword">function</span>() {
      <span class="org-keyword">var</span> <span class="org-variable-name">prettyLink</span> = $(<span class="org-constant">this</span>).find(<span class="org-string">&quot;h2 a&quot;</span>).attr(<span class="org-string">&quot;href&quot;</span>);
      <span class="org-keyword">if</span> (!prettyLink.match(<span class="org-string">/weekly/</span>i) &amp;&amp; !prettyLink.match(<span class="org-string">/monthly/</span>i)) {
        collectLinks($(<span class="org-constant">this</span>), prettyLink, imageData);
      }
    });
    updateFlickrPhotos();
  });
}

<span class="org-keyword">function</span> <span class="org-function-name">updateFlickrPhotos</span>() {
    Flickr.authenticate(flickrOptions, <span class="org-keyword">function</span>(<span class="org-variable-name">error</span>, <span class="org-variable-name">flickr</span>) {
      flickr.photos.search(
        {user_id: flickrOptions.user_id,
         per_page: 500,
         extras: <span class="org-string">&apos;description&apos;</span>,
         text: <span class="org-string">&apos; -blogged&apos;</span>}, <span class="org-keyword">function</span>(<span class="org-variable-name">err</span>, <span class="org-variable-name">result</span>) {
           processPage(result, flickr);
           <span class="org-keyword">for</span> (<span class="org-keyword">var</span> <span class="org-variable-name">i</span> = 2 ; i &lt; result.photos.pages; i++) {
             flickr.photos.search(
               {user_id: flickrOptions.user_id, per_page: 500, page: i,
                extras: <span class="org-string">&apos;description&apos;</span>, text: <span class="org-string">&apos; -blogged&apos;</span>},
               <span class="org-keyword">function</span>(<span class="org-variable-name">err</span>, <span class="org-variable-name">result</span>) {
                 processPage(err, result, flickr);
               });
           }
         });
    });
}

<span class="org-keyword">function</span> <span class="org-function-name">collectLinks</span>(<span class="org-variable-name">article</span>, <span class="org-variable-name">prettyLink</span>, <span class="org-variable-name">imageData</span>) {
  <span class="org-keyword">var</span> <span class="org-variable-name">results</span> = [];
  article.find(<span class="org-string">&quot;.body a&quot;</span>).each(<span class="org-keyword">function</span>() {
    <span class="org-keyword">var</span> <span class="org-variable-name">link</span> = $(<span class="org-constant">this</span>);
    <span class="org-keyword">if</span> (link.attr(<span class="org-string">&apos;href&apos;</span>)) {
      <span class="org-keyword">if</span> (link.attr(<span class="org-string">&apos;href&apos;</span>).match(<span class="org-string">/sachachua/</span>)
          || !link.attr(<span class="org-string">&apos;href&apos;</span>).match(<span class="org-string">/^http/</span>)) {
        imageData[exports.trimTitle(link.attr(<span class="org-string">&apos;href&apos;</span>))] = prettyLink;
      } <span class="org-keyword">else</span> <span class="org-keyword">if</span> (link.attr(<span class="org-string">&apos;href&apos;</span>).match(<span class="org-string">/flickr.com/</span>)) {
        imageData[exports.trimTitle(link.text())] = prettyLink;
      }
    }
  });
  <span class="org-keyword">return</span> results;
}

exports.trimTitle = <span class="org-keyword">function</span>(<span class="org-variable-name">str</span>) {
  <span class="org-keyword">return</span> str.replace(<span class="org-string">/^.*\//</span>, <span class="org-string">&apos;&apos;</span>).replace(<span class="org-string">/^wpid-/</span>g, <span class="org-string">&apos;&apos;</span>).replace(<span class="org-string">/[^A-Za-z0-9]/</span>g, <span class="org-string">&apos;&apos;</span>).replace(<span class="org-string">/png$/</span>, <span class="org-string">&apos;&apos;</span>).replace(<span class="org-string">/[0-9]$/</span>, <span class="org-string">&apos;&apos;</span>);
};

<span class="org-keyword">function</span> <span class="org-function-name">processPage</span>(<span class="org-variable-name">result</span>, <span class="org-variable-name">flickr</span>) {
  <span class="org-keyword">if</span> (!result) <span class="org-keyword">return</span>;
  <span class="org-keyword">for</span> (<span class="org-keyword">var</span> <span class="org-variable-name">i</span> = 0; i &lt; result.photos.photo.length; i++) {
    <span class="org-keyword">var</span> <span class="org-variable-name">p</span> = result.photos.photo[i];
    <span class="org-keyword">var</span> <span class="org-variable-name">trimmed</span> = exports.trimTitle(p.title);
    <span class="org-keyword">var</span> <span class="org-variable-name">noTags</span> = trimmed.replace(<span class="org-string">/#.*/</span>g, <span class="org-string">&apos;&apos;</span>);
    <span class="org-keyword">var</span> <span class="org-variable-name">withTags</span> = trimmed.replace(<span class="org-string">/#/</span>g, <span class="org-string">&apos;&apos;</span>);
    <span class="org-keyword">var</span> <span class="org-variable-name">found</span> = imageData[noTags] || imageData[withTags];
    <span class="org-keyword">if</span> (found) {
      <span class="org-keyword">var</span> <span class="org-variable-name">description</span> = p.description._content;
      <span class="org-keyword">if</span> (description.match(found)) <span class="org-keyword">continue</span>;
      <span class="org-keyword">if</span> (description) {
        description += <span class="org-string">&quot; - &quot;</span>;
      }
      description += <span class="org-string">&apos;&lt;a href=&quot;&apos;</span> + found + <span class="org-string">&apos;&quot;&gt;Blogged&lt;/a&gt;&apos;</span>;
      console.log(<span class="org-string">&quot;Updating &quot;</span> + p.title + <span class="org-string">&quot; with &quot;</span> + description);
      flickr.photos.setMeta(
        {photo_id: p.id,
         description: description},
        <span class="org-keyword">function</span>(<span class="org-variable-name">err</span>, <span class="org-variable-name">res</span>) {
          <span class="org-keyword">if</span> (err) { console.log(err, res); }
        } );
    }
  }
}

setDescriptionsFromURL(process.argv[2]);
</pre>
</div>
<p>And now sketches like </p><div class="sketch-thumbnail"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2013-11-11%20How%20can%20you%20make%20the%20most%20of%20your%20event%20sketches%20%23client-education%20%23sketchnoting.png" data-src="https://sketches.sachachua.com/static/2013-11-11%20How%20can%20you%20make%20the%20most%20of%20your%20event%20sketches%20%23client-education%20%23sketchnoting.png" data-title="2013-11-11 How can you make the most of your event sketches #client-education #sketchnoting" data-w="3299" data-h="2506"><picture>
      <img src="https://sketches.sachachua.com/thumbnails/2013-11-11%20How%20can%20you%20make%20the%20most%20of%20your%20event%20sketches%20%23client-education%20%23sketchnoting.png" width="" height="" alt="2013-11-11 How can you make the most of your event sketches #client-education #sketchnoting" loading="lazy" decoding="async">
      <figcaption>2013-11-11 How can you make the most of your event sketches #client-education #sketchnoting</figcaption>
    </picture></a></div> are now properly linked to their blog posts. Yay! Again, this script won&apos;t get everything, but it gets a decent number automatically sorted out.<p></p>
<p>Next steps:</p>
<ul class="org-ul">
<li>Run the image extraction and set description scripts monthly as part of my indexing process</li>
<li>Check my list of blogged images to see if they&apos;re matched up with Flickr sketches, so that I can identify images mysteriously missing from my sketchbook archive or not correctly linked</li>
</ul>
<p>Yay code!</p>
</div>
</div>
<p></p>

<p>You can <a href="https://sachachua.com/blog/2015/02/de-dupe-link-using-flickr-api-neaten-archive-link-sketches-blog-posts/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2015%2F02%2Fde-dupe-link-using-flickr-api-neaten-archive-link-sketches-blog-posts%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>