<?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 - flickr</title>
	<atom:link href="https://sachachua.com/blog/tag/flickr/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/tag/flickr" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/tag/flickr/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>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>