Now Reading
clickr, or a younger man’s Flickr clonejure

clickr, or a younger man’s Flickr clonejure

2024-01-22 04:35:05

A space station

Method again when, earlier than I received my first Macbook (the white 2006 mannequin, so cool!) and thus had no iPhoto to maintain all of my photographs for me, I used some open supply photograph album thingy on Linux (it was so way back that I do not even bear in mind what it was known as) on a server working underneath my desk with a hard and fast IP. Utilizing this superb setup, I might create photograph albums and share them with household who wished to maintain up with my thrilling life. After we moved to Tokyo in 2005, I removed the Linux server (and the desk it was underneath and the residence the desk was in) and introduced solely a laptop computer with me. After we first arrived, we did not even have web, so I borrowed wifi from somebody within the subsequent constructing who had helpfully eschewed a password, and I believed it might be a bit cheeky to find that individual and ask them to buy a hard and fast IP in order that I might run a webserver so I might share photograph albums with my household.

Fortunately for me, two folks named Stewart Butterfield and Caterina Pretend felt my ache and began an organization known as Ludicorp and tried to create a web-based massively multiplayer on-line recreation known as Recreation Neverending, which they did not do however made some fairly cool instruments for belongings which they then determined could possibly be become an internet photograph sharing service which they named Flickr.

Which is a really long-winded method of claiming—and until you are very new to this weblog, you’ll know that I’m a really long-winded individual—that I signed up for Flickr for my photo-sharing wants.

A number of years later, I purchased the aforementioned Macbook, and began utilizing iPhoto, which was a lot nicer than copying photographs from a reminiscence card onto my arduous drive after which importing them to Flickr, so I received kinda lazy about creating albums on Flickr, till my son was born and my household began demanding to see photos of him.

Flash ahead to a few years later, and now all of us have smartphones and stuff (even my mother!), so we simply take photos which routinely go to iCloud and iPhoto and iDon’tKnowWhereElse after which share them with one another in Sign and Telegram and WhatsApp and 95 different smartphone messaging apps like regular folks, so we do not actually use Flickr any extra. In actual fact, I solely bear in mind we’ve got Flickr after they electronic mail me every year to remind me they’re about to cost my bank card one other $100 or no matter.

My reluctance to pay for one thing that I do not use is exceeded solely by my reluctance to spend time on manually migrating off that factor, so I did not do something about this till a few years in the past, after I simply so occurred to get the annual renewal electronic mail proper earlier than my winter trip began, so I used to be like “Oh, now I am going to have some free time and I am going to in all probability be bored so I ought to in all probability see if Flickr has an API that I might use to obtain my photographs and put them onto S3 like the great lord meant.”

Flickr did in reality have an API, and higher but, they’d a Java
client
, which I wrapped up in some Clojure and named clickr as a result of it is like Flickr in Clojure ha ha ha I am so intelligent. I received the itemizing of albums and downloading of photographs and importing of them to S3 working fairly simply, however for some purpose did not truly again up the albums to S3 aside from the primary one, in all probability as a result of I evaluated some code within the REPL to start out the again up after which switched tabs to learn a weblog or one thing after which forgot what I used to be doing and closed my laptop computer or one thing equally absentminded.

Flash ahead to a few weeks in the past, after I as soon as once more received the well-known electronic mail from Flickr and remembered that I actually ought to get round to backing up these albums so I can cease paying Flickr and pay AWS just a few extra cents a 12 months to retailer them for me. Since I now hate Clojure and solely love Babashka, I made a decision that as a substitute of going again to the Clojure wrapper across the Java library, I might whip up a fast HTTP consumer in Babashka as a substitute after which simply use awyeah-api for the S3-ing. I received just a few hours into combating signing requests to get an OAuth entry token which I might then get the person’s (me) authorization to trade for an entry token—you recognize, as you do—after I determined that my life was approach to quick for taking a URL corresponding to

https://www.flickr.com/companies/oauth/request_token
?oauth_nonce=89601180
&oauth_timestamp=1305583298
&oauth_consumer_key=653e7a6ecc1d528c516cc8f92cf98611
&oauth_signature_method=HMAC-SHA1
&oauth_version=1.0
&oauth_callback=httppercent3Apercent2Fpercent2Fwww.instance.com

and making a base string corresponding to

GET&httpspercent3Apercent2Fpercent2Fwww.flickr.compercent2Fservicespercent2Foauthpercent2Frequest_token&oauth_callbackpercent3Dhttppercent253Apercent252Fpercent252Fwww.instance.compercent26oauth_consumer_keypercent3D653e7a6ecc1d528c516cc8f92cf98611percent26oauth_noncepercent3D95613465percent26oauth_signature_methodpercent3DHMAC-SHA1percent26oauth_timestamppercent3D1305586162percent26oauth_versionpercent3D1.0

after which producing the next signature

7w18YS2bONDPLpercent2FzgyzP5XTr5af4percent3D

after which questioning why within the hell Flickr’s API saved giving me a signature incorrect error when the rattling signature was clearly right as a result of you recognize…

A diagram showing the three step OAuth flow for Flickr

I quickly got here to the conclusion that I ought to retreat to the secure embrace of JVM Clojure and simply let the Java consumer do all of the nasty work of authenticating so I might do the enjoyable work of Clojuring.

Onwards and upwards!

I cloned my trusty clickr repo, then screamed as I noticed a mission.clj and realised that I did not even have Leiningen put in as a result of I roll with tools.deps now and I am unable to bear in mind any lein instructions and OMG it is almost 2024 (this was just a few weeks in the past, bear in mind?) and I might higher generate a deps.edn like a standard individual:

{:paths ["src" "dev"]
 :deps {com.flickr4java/flickr4java {:mvn/model "3.0.1"}}}

Having achieved this, I might open src/clickr/flickr.clj and attempt to bear in mind tips on how to use some code I wrote a million couple years in the past and did not doc in any method, form, or kind as a result of I am a foul boy and life’s too quick for documentation!

In accordance with Flickr4Java:

To make use of the API simply assemble an occasion of the category com.flickr4java.flickr.check.Flickr and request the interfaces which you want to work with.

String apiKey = "YOUR_API_KEY";
String sharedSecret = "YOUR_SHARED_SECRET";
Flickr f = new Flickr(apiKey, sharedSecret, new REST());

OK, so we will begin our namespace out by importing the Flickr consumer:

(ns clickr.flickr
  (:import (com.flickr4java.flickr Flickr
                                   REST)))

After which C-c M-j (cider-jack-in-clj) to start out a REPL, after which C-c C-k (cider-load-buffer) to guage the buffer, then it looks as if I want an API key and the corresponding secret, which I can seize from https://www.flickr.com/companies/apps/by/jmglov. Lemme simply drop that in a map for safe-keeping and perform a little C-v f c e (cider-pprint-eval-last-sexp-to-comment, obv):

(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"})
  ;; => #'clickr.flickr/config

  )

OK, so making a consumer ought to be so simple as this:

  (def flickr (Flickr. (:api-key config) (:secret config) (REST.)))
  ;; => #'clickr.flickr/flickr

And with this, it ought to be straightforward to determine tips on how to get a listing of albums, proper? Proper?

What the Flickr?

Popping over to Flickr’s API reference, I am going to simply do a fast seek for album and… 0 occurrences? Wait what now?

OK, time to scan the web page for one thing that appears album-ish. 🙄

Ah, photosets.getList appears promising. Searching the Flickr4Java repo for
photoset
turns up a src/examples/java/Backup.java, which appears fairly useful:

public class Backup {
    non-public last String nsid;
    non-public last Flickr flickr;
    non-public AuthStore authStore;

    public Backup(String apiKey, String nsid, String sharedSecret, File authsDir) throws FlickrException {
        flickr = new Flickr(apiKey, sharedSecret, new REST());
        this.nsid = nsid;

        if (authsDir != null) {
            this.authStore = new FileAuthStore(authsDir);
        }
    }

    // ...

    public static void predominant(String[] args) throws Exception {
        if (args.size < 4) {
            System.out.println("Utilization: java " + Backup.class.getName() + " api_key nsid shared_secret output_dir");
            System.exit(1);
        }
        Backup bf = new Backup(args[0], args[1], args[2], new File(System.getProperty("person.house") + File.separatorChar + ".flickrAuth"));
        bf.doBackup(new File(args[3]));
    }
}

If we have a look at the doBackup() technique, it appears like there’s some authorisation we have to do above and past the API key and secret, which aligns with all of the Flickr API docs stated about “each request have to be signed OAuth blah blah blah”:

RequestContext rc = RequestContext.getRequestContext();

if (this.authStore != null) {
    Auth auth = this.authStore.retrieve(this.nsid);
    if (auth == null) {
        this.authorize();
    } else {
        rc.setAuth(auth);
    }
}

Assuming we’ve got no Auth object from the beginning, let’s take a look at what the authorize() technique does:

non-public void authorize() throws IOException, FlickrException {
    AuthInterface authInterface = flickr.getAuthInterface();
    OAuth1RequestToken requestToken = authInterface.getRequestToken();

    String url = authInterface.getAuthorizationUrl(requestToken, Permission.READ);
    System.out.println("Comply with this URL to authorise your self on Flickr");
    System.out.println(url);
    System.out.println("Paste within the token it provides you:");
    System.out.print(">>");

    String tokenKey = new Scanner(System.in).nextLine();

    OAuth1Token accessToken = authInterface.getAccessToken(requestToken, tokenKey);

    Auth auth = authInterface.checkToken(accessToken);
    RequestContext.getRequestContext().setAuth(auth);
    this.authStore.retailer(auth);
    System.out.println("Thanks.  You in all probability won't have to do that each time.  Now beginning backup.");
}

OK, translating this to Clojure is fairly straight-forward. I am going to begin with my common strategy when coping with API purchasers of making a “context” which incorporates my config, varied purchasers, and every other information that I’d must go round. Let’s create an init-client operate that takes my config as an argument and returns such a context:

(defn init-client [{:keys [api-key secret] :as ctx}]
  (assoc ctx :flickr {:consumer (Flickr. api-key secret (REST.))}))

We will name that and get a context again containing the consumer:

(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"})
  ;; => #'clickr.flickr/config

  (init-client config)
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :flickr
  ;;     {:consumer
  ;;      #object[com.flickr4java.flickr.Flickr 0x12246439 "com.flickr4java.flickr.Flickr@12246439"]}}

  )

Now I’ve a consumer, however I must carry out the authorisation dance from the Java code. Within the constructor of the Java class, they create a FileAuthStore within the authsDir, which is definitely the listing known as output_dir within the predominant() operate. 🙄

Let’s be an excellent citizen by creating this auth retailer within the person’s house listing. So as to do that, let’s add the superior babashka.fs library to our deps:

{:paths ["src" "dev"]
 :deps {babashka/fs {:mvn/model "0.4.19"}
        com.flickr4java/flickr4java {:mvn/model "3.0.1"}}}

babashka.fs incorporates a bunch of actually helpful utility features that work in each Babashka and JVM Clojure!

Tragically, after including a brand new dep, I must restart my REPL. It’s attainable to hotload dependencies right into a working REPL course of, which ought to have gotten
easier in Clojure
1.12
, and naturally borkdude has a deps.add-lib mission which ought to make it even simpler, however I have never used these items but, so I am going to simply do the primitive factor and restart (swearing underneath my breath), in fact.

Now that we’ve got babashka.fs, making a FileAuthStore within the ~/.flickrAuth listing is so simple as this:

(ns clickr.flickr
  (:require [babashka.fs :as fs])
  (:import (com.flickr4java.flickr Flickr
                                   REST)
           (com.flickr4java.flickr.util FileAuthStore)))

(defn make-auth-store []
  (FileAuthStore. (fs/file (fs/house) ".flickrAuth")))

Let’s create the auth retailer in init-client and add it to the context together with the Flickr consumer itself:

(defn init-client [{:keys [api-key secret] :as ctx}]
  (let [client (Flickr. api-key secret (REST.))
        auth-store (make-auth-store)]
    (assoc ctx :flickr {:consumer consumer, :auth-store auth-store})))

Now that we’ve got a approach to create an auth retailer, let’s write an authorise operate to trade a request token for an entry token:

(ns clickr.flickr
  (:require [babashka.fs :as fs])
  (:import (com.flickr4java.flickr Flickr
                                   REST)
           (com.flickr4java.flickr.auth Permission)
           (com.flickr4java.flickr.util FileAuthStore)))

(defn authorise [{:keys [flickr] :as ctx}]
  (let [{:keys [client]} flickr
        auth-interface (.getAuthInterface consumer)
        req-token (.getRequestToken auth-interface)
        url (.getAuthorizationUrl auth-interface req-token Permission/READ)]
    (replace ctx :flickr assoc :url url, :req-token req-token)))

Let’s attempt it out:

(remark

  (def ctx (init-client config))
  ;; => #'clickr.flickr/ctx

  (authorise ctx)
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :flickr
  ;;     {:consumer
  ;;      #object[com.flickr4java.flickr.Flickr 0x12246439 "com.flickr4java.flickr.Flickr@12246439"]
  ;;      :auth-store
  ;;      #object[com.flickr4java.flickr.util.FileAuthStore 0x6b1f411f "com.flickr4java.flickr.util.FileAuthStore@6b1f411f"]
  ;;      :url
  ;;      "https://www.flickr.com/companies/oauth/authorize?oauth_token=72157720906512072-49cebfb020e7fb06&perms=learn"
  ;;      :req-token
  ;;      #object[com.github.scribejava.core.model.OAuth1RequestToken 0x2617e72c "com.github.scribejava.core.model.OAuth1RequestToken@43e76617"]}}

  )

If I go to that URL in an internet browser the place we’re signed into Flickr, I am prompted to authorise the app akin to the API key I beforehand created:

A prompt to authorize an app to my Flickr account

If I click on “OK, I am going to authorize it”, I get a code that I ought to “sort into the appliance”. Let’s set that up by modifying the authorise operate to simply accept a code, which it should use to trade the request token for an entry token, then retailer that entry token within the auth retailer that we beforehand created.

(ns clickr.flickr
  (:require [babashka.fs :as fs])
  (:import (com.flickr4java.flickr Flickr
                                   RequestContext
                                   REST)
           (com.flickr4java.flickr.auth Permission)
           (com.flickr4java.flickr.util FileAuthStore)))

(defn authorise [{:keys [flickr] :as ctx}]
  (let [{:keys [client auth-store req-token token-key]} flickr
        auth-interface (.getAuthInterface consumer)]
    (if (and req-token token-key)
      (let [access-token (.getAccessToken auth-interface req-token token-key)
            auth (.checkToken auth-interface access-token)]
        (.setAuth (RequestContext/getRequestContext) auth)
        (.retailer auth-store auth)
        (replace ctx :flickr dissoc :req-token :token-key :url))
      (let [req-token (.getRequestToken auth-interface)
            url (.getAuthorizationUrl auth-interface req-token Permission/READ)]
        (replace ctx :flickr assoc :url url, :req-token req-token)))))

The concept right here is that in the event you go a request token and token code within the Flickr context, you need to trade a request token for an entry token, and in the event you do not, you need to get hold of a request token and the URL to authorise it.

OK, let’s give it a whirl:

(remark

  (def ctx (authorise ctx))
  ;; => #'clickr.flickr/ctx

  (get-in ctx [:flickr :url])
  ;; => "https://www.flickr.com/companies/oauth/authorize?oauth_token=72157720906550331-8cf736dfbfe34398&perms=learn"

  (authorise (assoc-in ctx [:flickr :token-key] "123-456-789"))
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :flickr
  ;;     {:consumer
  ;;      #object[com.flickr4java.flickr.Flickr 0x67f9df60 "com.flickr4java.flickr.Flickr@67f9df60"],
  ;;      :auth-store
  ;;      #object[com.flickr4java.flickr.util.FileAuthStore 0x237de92 "com.flickr4java.flickr.util.FileAuthStore@237de92"]}}

  )

In principle, we must always now have a Flickr4Java request context with an entry token, and that entry token saved in our auth retailer. Let’s try what’s within the auth retailer:

(remark

  (.retrieveAll (get-in ctx [:flickr :auth-store]))
  ;; => [#object[com.flickr4java.flickr.auth.Auth 0x7737c4f2 "com.flickr4java.flickr.auth.Auth@7737c4f2"]]

  )

Ooh, thrilling!

If we forged our thoughts again to that Java code:

Auth auth = this.authStore.retrieve(this.nsid);
if (auth == null) {
    this.authorize();
} else {
    rc.setAuth(auth);
}

we see that the authorize() technique is just known as if we do not have an entry token (the Auth object). If we’ve got one, we simply stuff it within the request context and transfer on with our life. Let’s make one last change to our authorise operate to do the identical factor:

(defn authorise [{:keys [flickr] :as ctx}]
  (let [{:keys [client auth-store req-token token-key]} flickr
        auth-interface (.getAuthInterface consumer)
        auth (-> auth-store .retrieveAll first)]
    (cond
      ;; Now we have a sound entry token from the auth retailer
      auth
      (do
        (.setAuth (RequestContext/getRequestContext) auth)
        (replace ctx :flickr assoc :auth auth))

      ;; Now we have a request token and a token key, so trade the request
      ;; token for an entry token
      (and req-token token-key)
      (let [access-token (.getAccessToken auth-interface req-token token-key)
            auth (.checkToken auth-interface access-token)]
        (.setAuth (RequestContext/getRequestContext) auth)
        (.retailer auth-store auth)
        (replace ctx :flickr dissoc :req-token :token-key :url))

      ;; We have no tokens, so seize a request token and the URL to
      ;; authorise it
      :default
      (let [req-token (.getRequestToken auth-interface)
            url (.getAuthorizationUrl auth-interface req-token Permission/READ)]
        (replace ctx :flickr assoc :url url, :req-token req-token)))))

We will see if it really works by making a model new consumer and calling authorise on it:

(remark

  (-> (init-client config) authorise)
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :flickr
  ;;     {:consumer
  ;;      #object[com.flickr4java.flickr.Flickr 0x15c343a9 "com.flickr4java.flickr.Flickr@15c343a9"],
  ;;      :auth-store
  ;;      #object[com.flickr4java.flickr.util.FileAuthStore 0x6df7251f "com.flickr4java.flickr.util.FileAuthStore@6df7251f"]
  ;;      :auth
  ;;      #object[com.flickr4java.flickr.auth.Auth 0x4d1d0eb0 "com.flickr4java.flickr.auth.Auth@4d1d0eb0"]}}

  )

That requires a celebration!

A woman on a beach at sunrise with her head thrown back, saying

Given that you simply solely must undergo the rigmarole of exchanging a request token for an auth token as soon as, I really feel prefer it’s cheap to have init-client go forward and authorise the consumer. This can work simply high-quality for the rigmarole case as effectively, since you possibly can simply name authorise on the consumer your self if it has a :url key set, so there is no hurt in doing this.

(defn init-client [{:keys [api-key secret] :as ctx}]
  (let [client (Flickr. api-key secret (REST.))
        auth-store (make-auth-store)]
    (-> ctx
        (assoc :flickr {:consumer consumer, :auth-store auth-store})
        authorise)))

OK, so what about these albums once more?

Earlier than we received sidetracked by all of this annoying safety stuff, the precise aim was to get a listing of my photograph albums, which we realized are apparently known as “photosets” within the Flickr API. Let’s take a look at how the instance Java code offers with them:

PhotosetsInterface pi = flickr.getPhotosetsInterface();
Iterator units = pi.getList(this.nsid).getPhotosets().iterator();

The nsid thingy seems to simply be my Flickr person ID, which I can seize from the Auth object. Let’s seize an album and see what it is all about!

(remark

  (def album
    (let [ps-interface (.getPhotosetsInterface (get-in ctx [:flickr :client]))
          user-id (.. (get-in ctx [:flickr :auth]) getUser getId)]
      (-> ps-interface (.getList user-id) .getPhotosets first)))
  ;; => #'clickr.flickr/album

  album
  ;; => #object[com.flickr4java.flickr.photosets.Photoset 0x726a87d4 "com.flickr4java.flickr.photosets.Photoset@726a87d4"]

  )

Cool, I suppose. Wanting on the Flickr API reference for photosets.getList, we will see an instance XML response:

<photosets web page="1" pages="1" perpage="30" complete="2" cancreate="1">
  <photoset id="72157626216528324" major="5504567858" secret="017804c585" server="5174" farm="6" photographs="22" movies="0" count_views="137" count_comments="0" can_comment="1" date_create="1299514498" date_update="1300335009">
    <title>Avis Blanche</title>
    <description>My Grandma's Recipe File.</description>
  </photoset>
  <photoset id="72157624618609504" major="4847770787" secret="6abd09a292" server="4153" farm="5" photographs="43" movies="12" count_views="523" count_comments="1" can_comment="1" date_create="1280530593" date_update="1308091378">
    <title>Mah Kittehs</title>
    <description>Sixty and Niner. Born on the third of Might, 2010, or thereabouts. Got here to my place on Thursday, July 29, 2010.</description>
  </photoset>
</photosets>

Let’s make some assumptions about what getters the Photoset class has and see if we will discover out something attention-grabbing about our album:

(remark

  (.getId album)
  ;; => "72177720314024335"

  (.getTitle album)
  ;; => "clickr demo"

  (.getDescription album)
  ;; => "Photograph album demo for my clickr weblog publish"

  )

Good! Given this, let’s write a operate that turns a Photoset right into a plain outdated map (or POM—that is what meaning, proper?):

(defn ->album [photoset]
  {:id (.getId photoset)
   :title (.getTitle photoset)
   :description (.getDescription photoset)
   :object photoset})

(remark

  (->album album)
  ;; => {:id "72177720314024335",
  ;;     :title "clickr demo",
  ;;     :description "Photograph album demo for my clickr weblog publish",
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photosets.Photoset 0x726a87d4 "com.flickr4java.flickr.photosets.Photoset@726a87d4"]}

  )

And now that we’ve got this, why not a operate that grabs all of my albums?

(defn get-albums [{:keys [flickr] :as ctx}]
  (let [user-id (.. (:auth flickr) getUser getId)]
    (->> (-> (:consumer flickr)
             (.getPhotosetsInterface)
             (.getList user-id)
             (.getPhotosets))
         (map ->album))))

(remark

  (->> (get-albums ctx)
       depend)
  ;; => 165

  (->> (get-albums ctx)
       (take 2))
  ;; => ({:id "72177720314024335",
  ;;      :title "clickr demo",
  ;;      :description "Photograph album demo for my clickr weblog publish",
  ;;      :object
  ;;      #object[com.flickr4java.flickr.photosets.Photoset 0x9d8b7ae "com.flickr4java.flickr.photosets.Photoset@9d8b7ae"]}
  ;;     {:id "72157706528674711",
  ;;      :title "Kai's eighth Birthday",
  ;;      :description
  ;;      "April 2015. Kaiche's first 12 months at SIS. Treating his classmates to muffins in school. Sushi and cake with mommy and daddy. ",
  ;;      :object
  ;;      #object[com.flickr4java.flickr.photosets.Photoset 0x1bb741fe "com.flickr4java.flickr.photosets.Photoset@1bb741fe"]})

  )

Alright, it appears like I’ve fairly just a few albums to take care of right here. However an album ought to include some photographs, proper? Let’s have a look at if we will work out tips on how to seize them.

In Java, that apparently appears like this:

Photoset set = (Photoset) units.subsequent();
PhotoList photographs = pi.getPhotos(set.getId(), 500, 1);

Let’s attempt that in Clojure:

(remark

  (def album (-> (get-albums ctx) first))
  ;; => #'clickr.flickr/album

  (def ps-interface (.getPhotosetsInterface (get-in ctx [:flickr :client])))
  ;; => #'clickr.flickr/ps-interface

  (def photographs (.getPhotos ps-interface (:id album) 500 1))
  ;; => #'clickr.flickr/photographs

  (depend photographs)
  ;; => 8

  (first photographs)
  ;; => #object[com.flickr4java.flickr.photos.Photo 0xd254585 "com.flickr4java.flickr.photos.Photo@14ea992b"]
  )

In accordance with the Flickr API documentation for photosets.getPhotos, the 500 and the 1 are the variety of photographs to return per web page and the web page quantity, each of that are the default values. Appears high-quality.

The docs are slightly sparse on what’s in a photograph, although:

<photoset id="4" major="2483" web page="1" perpage="500" pages="1" complete="2">
  <photograph id="2484" secret="123456" server="1" title="my photograph" isprimary="0" />
  <photograph id="2483" secret="123456" server="1" title="flickr rocks" isprimary="1" />
</photoset>

Let’s ask the JVM what getters the photograph class has:

  (def photograph (first photographs))
  ;; => #'clickr.flickr/photograph

  (->> photograph class .getDeclaredMethods
       (map #(.getName %))
       (filter #(str/starts-with? % "get"))
       type)
  ;; => ("getBaseImageUrl"
  ;;     "getComments"
  ;;     "getCountry"
  ;;     "getCounty"
  ;;     "getDateAdded"
  ;;     "getDatePosted"
  ;;     "getDateTaken"
  ;;     "getDescription"
  ;;     "getEditability"
  ;;     "getFarm"
  ;;     "getGeoData"
  ;;     "getHdMp4"
  ;;     "getHdMp4Url"
  ;;     "getIconFarm"
  ;;     "getIconServer"
  ;;     "getId"
  ;;     "getImage"
  ;;     "getImageAsStream"
  ;;     "getLarge1600Size"
  ;;     "getLarge1600Url"
  ;;     "getLarge2048Size"
  ;;     "getLarge2048Url"
  ;;     "getLargeAsStream"
  ;;     "getLargeImage"
  ;;     "getLargeSize"
  ;;     "getLargeUrl"
  ;;     "getLastUpdate"
  ;;     "getLicense"
  ;;     "getLocality"
  ;;     "getMedia"
  ;;     "getMediaStatus"
  ;;     "getMedium640Size"
  ;;     "getMedium640Url"
  ;;     "getMedium800Size"
  ;;     "getMedium800Url"
  ;;     "getMediumAsStream"
  ;;     "getMediumImage"
  ;;     "getMediumSize"
  ;;     "getMediumUrl"
  ;;     "getMobileMp4"
  ;;     "getMobileMp4Url"
  ;;     "getNotes"
  ;;     "getOriginalAsStream"
  ;;     "getOriginalBaseImageUrl"
  ;;     "getOriginalFormat"
  ;;     "getOriginalHeight"
  ;;     "getOriginalImage"
  ;;     "getOriginalImage"
  ;;     "getOriginalImageAsStream"
  ;;     "getOriginalSecret"
  ;;     "getOriginalSize"
  ;;     "getOriginalUrl"
  ;;     "getOriginalWidth"
  ;;     "getOwner"
  ;;     "getPathAlias"
  ;;     "getPermissions"
  ;;     "getPhotoUrl"
  ;;     "getPlaceId"
  ;;     "getPublicEditability"
  ;;     "getRegion"
  ;;     "getRotation"
  ;;     "getSecret"
  ;;     "getServer"
  ;;     "getSiteMP4Size"
  ;;     "getSiteMP4Url"
  ;;     "getSizes"
  ;;     "getSmall320Size"
  ;;     "getSmall320Url"
  ;;     "getSmallAsInputStream"
  ;;     "getSmallImage"
  ;;     "getSmallSize"
  ;;     "getSmallSquareAsInputStream"
  ;;     "getSmallSquareImage"
  ;;     "getSmallSquareUrl"
  ;;     "getSmallUrl"
  ;;     "getSquareLargeSize"
  ;;     "getSquareLargeUrl"
  ;;     "getSquareSize"
  ;;     "getStats"
  ;;     "getTags"
  ;;     "getTakenGranularity"
  ;;     "getThumbnailAsInputStream"
  ;;     "getThumbnailImage"
  ;;     "getThumbnailSize"
  ;;     "getThumbnailUrl"
  ;;     "getTitle"
  ;;     "getUrl"
  ;;     "getUrls"
  ;;     "getUsage"
  ;;     "getVideoOriginalSize"
  ;;     "getVideoOriginalUrl"
  ;;     "getVideoPlayerSize"
  ;;     "getVideoPlayerUrl"
  ;;     "getViews")

Let’s write a ->photograph operate to POM-ify this suckah!

(defn ->photograph [photo]
  {:id (.getId photograph)
   :title (.getTitle photograph)
   :description (.getDescription photograph)
   :date-taken (.getDateTaken photograph)
   :width (.getOriginalWidth photograph)
   :peak (.getOriginalHeight photograph)
   :geo-data (.getGeoData photograph)
   :rotation (.getRotation photograph)
   :object photograph})

(remark

  (->photograph photograph)
  ;; => {:description nil,
  ;;     :date-taken nil,
  ;;     :geo-data nil,
  ;;     :rotation -1,
  ;;     :width 0,
  ;;     :title "sean-hargreaves-phoenix-new-5-final-a",
  ;;     :id "53460147147",
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photos.Photo 0xd254585 "com.flickr4java.flickr.photos.Photo@14ea992b"],
  ;;     :peak 0}

  )

And now that we’ve got this, let’s add the photographs to the the album map:

(defn ->album [{:keys [flickr] :as ctx} photoset]
  (let [ps-interface (.getPhotosetsInterface (:client flickr))]
    {:id (.getId photoset)
     :title (.getTitle photoset)
     :description (.getDescription photoset)
     :photographs (->> (.getPhotos ps-interface (:id album) 500 1)
                  (map (partial ->photograph ctx)))
     :object photoset}))

Notice that to be able to listing the photographs in an album, we want the PhotosetInterface, which we get from the Flickr consumer within the context, so we have to add the context as an argument to ->album. In actual fact, let’s make it a conference that ctx is all the time the primary argument to features on this namespace, and add it to ->photograph as effectively:

(defn ->photograph [_ctx photo]
  {:id (.getId photograph)
   :title (.getTitle photograph)
   :description (.getDescription photograph)
   :date-taken (.getDateTaken photograph)
   :width (.getOriginalWidth photograph)
   :peak (.getOriginalHeight photograph)
   :geo-data (.getGeoData photograph)
   :rotation (.getRotation photograph)
   :object photograph})

We do not really need it on this operate, so we’ll prepend an underscore to the arg title so CIDER or LSP or clj-kondo or no matter tooling you are utilizing will not yell at us that it is unused. This can be a conference that I first found in Erlang, and I feel it is a cool approach to say that “that is the context, which we do not want right here (but)”.

Lastly, we have to go the context alongside in get-albums as effectively:

(defn get-albums [{:keys [flickr] :as ctx}]
  (let [user-id (.. (:auth flickr) getUser getId)]
    (->> (-> (:consumer flickr)
             (.getPhotosetsInterface)
             (.getList user-id)
             (.getPhotosets))
         (map (partial ->album ctx)))))

Making off with the products

Now that we’ve got a approach to listing albums and the photographs inside them, let’s have a look at tips on how to obtain stated photographs. Studying on in Backup.java, I encounter this good things:

Photograph p = (Photograph) setIterator.subsequent();
String url = p.getLargeUrl();
URL u = new URL(url);
String filename = u.getFile();
filename = filename.substring(filename.lastIndexOf("/") + 1, filename.size());
System.out.println("Now writing " + filename + " to " + setDirectory.getCanonicalPath());
BufferedInputStream inStream = new BufferedInputStream(photoInt.getImageAsStream(p, Measurement.LARGE));
File newFile = new File(setDirectory, filename);

FileOutputStream fos = new FileOutputStream(newFile);

int learn;

whereas ((learn = inStream.learn()) != -1) {
    fos.write(learn);
}
fos.flush();
fos.shut();
inStream.shut();

That appears like one approach to do it. 😬

Let’s have a look at if we will clear this up slightly. This complete mess:

whereas ((learn = inStream.learn()) != -1) {
    fos.write(learn);
}

may be changed with clojure.java.io/copy, so let’s replace the ns kind to require in clojure.java.io, and import the opposite Java lessons talked about on this code snippet while we’re at it:

(ns clickr.flickr
  (:require [babashka.fs :as fs]
            [clojure.java.io :as io])
  (:import (com.flickr4java.flickr Flickr
                                   RequestContext
                                   REST)
           (com.flickr4java.flickr.auth Permission)
           (com.flickr4java.flickr.photographs Measurement)
           (com.flickr4java.flickr.util FileAuthStore)
           (java.io BufferedInputStream
                    FileOutputStream)))

I additionally discover these items to get the filename of the photograph to be a bit annoying:

String url = p.getLargeUrl();
URL u = new URL(url);
String filename = u.getFile();
filename = filename.substring(filename.lastIndexOf("/") + 1, filename.size());

Let’s make the filename the photograph’s ID plus its format. We will do that in our ->photograph operate like so:

(defn ->photograph [_ photo]
  (let [id (.getId photo)
        extension (.getOriginalFormat photo)
        filename (format "%s.%s" id extension)]
    {:id id
     :filename filename
     :title (.getTitle photograph)
     :description (.getDescription photograph)
     :date-taken (.getDateTaken photograph)
     :width (.getOriginalWidth photograph)
     :peak (.getOriginalHeight photograph)
     :geo-data (.getGeoData photograph)
     :rotation (.getRotation photograph)
     :object photograph}))

Now we will write a operate to obtain a photograph properly. To be good residents, let’s put the file within the tmp listing (/tmp on Linux and MacOS, who is aware of the place on Home windows). Fortunately for us, babashka.fs has a helpful temp-dir operate that may do that! 🎉

(defn download-photo! [{:keys [flickr] :as ctx}
                       {:keys [filename] :as photograph}]
  (let [p-interface (.getPhotosInterface (:client flickr))]
    (with-open [in (BufferedInputStream. (.getImageAsStream p-interface (:object photo) Size/LARGE))
                out (FileOutputStream. (fs/file (fs/temp-dir) filename))]
      (io/copy in out))))

Now let’s attempt calling it and seeing if it really works!

(remark

  (def album (-> (get-albums ctx) first))
  ;; => #'clickr.flickr/album

  (def photograph (-> album :photographs first))
  ;; => #'clickr.flickr/photograph

  photograph
  ;; => {:description nil,
  ;;     :date-taken nil,
  ;;     :geo-data nil,
  ;;     :rotation -1,
  ;;     :width 0,
  ;;     :title "sean-hargreaves-phoenix-new-5-final-a",
  ;;     :filename "53460147147.jpg",
  ;;     :id "53460147147",
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photos.Photo 0x5d288375 "com.flickr4java.flickr.photos.Photo@14ea992b"],
  ;;     :peak 0}

  (download-photo! ctx photograph)
  ;; => nil

  (fs/exists? (fs/file (fs/temp-dir) (:filename photograph)))
  ;; => true

  )

Certain sufficient, we now have a /tmp/53460147147.jpg file!

A space station

Backing issues up

Now that we have confirmed that we will obtain a photograph, let’s take into consideration how we would like the backup course of to work. What we in all probability need is to create a “folder” in our S3 bucket for every album, after which put all the photographs for that album inside it. Let’s write a operate to obtain a complete album.

The primary order of enterprise is to create a listing to carry the photographs within the album that we’re about to obtain. Let’s use the album ID because the title of the listing and drop it within the tmp listing:

(remark

  (fs/file (fs/temp-dir) (:id album))
  ;; => #object[java.io.File 0x4e8e72c2 "/tmp/72177720314024335"]

  )

We will now use create-dirs to create the listing:


(remark

  (->> (fs/file (fs/temp-dir) (:id album)) fs/create-dirs)
  ;; => #object[sun.nio.fs.UnixPath 0x1fbb7d68 "/tmp/72177720314024335"]

  )

OK, so we’ve got a listing to carry the photographs, however we will need to replace download-photo! to make use of that listing as a substitute of dropping stuff straight into /tmp. Straightforward sufficient:

(defn download-photo! [{:keys [flickr out-dir] :as ctx}
                       {:keys [filename] :as photograph}]
  (let [p-interface (.getPhotosInterface (:client flickr))]
    (with-open [in (BufferedInputStream. (.getImageAsStream p-interface (:object photo) Size/LARGE))
                out (FileOutputStream. (fs/file out-dir filename))]
      (io/copy in out))))

(remark

  (def album-dir (->> (fs/file (fs/temp-dir) (:id album)) fs/create-dirs))
  ;; => #'clickr.flickr/album-dir

  (download-photo! (assoc ctx :out-dir album-dir) photograph)
  ;; => nil

  (fs/exists? (fs/file album-dir (format "%s.jpg" (:id photograph))))
  ;; => true

  )

Having thus tamed download-photo!, we will write download-album!:

(defn download-album! [ctx {:keys [id photos] :as album}]
  (let [album-dir (->> id (fs/file (fs/temp-dir)) fs/create-dirs fs/file)]
    (->> photographs
         (map (partial download-photo! (assoc ctx :out-dir album-dir)))
         doall)))

(remark

  (download-album! ctx album)
  ;; => (nil nil nil nil nil nil nil nil)

  (fs/glob album-dir "*")
  ;; => [#object[sun.nio.fs.UnixPath 0x72466b2f "/tmp/72177720314024335/53461405604.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x788d40e5 "/tmp/72177720314024335/53460163402.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x26f7ff12 "/tmp/72177720314024335/53460161007.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x250e8e92 "/tmp/72177720314024335/53460147147.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x17fa6f15 "/tmp/72177720314024335/53461214223.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x5e28f67 "/tmp/72177720314024335/53461091151.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x1cf4ffa6 "/tmp/72177720314024335/53460151727.jpg"]
  ;;     #object[sun.nio.fs.UnixPath 0x4ddf88b4 "/tmp/72177720314024335/53461088046.jpg"]]

  )

Incredible! Although I’ve to confess that I do not discover the listing of nils very pleasant. Let’s make one final change to download-photo in order that it returns the photograph, with the placement of the downloaded file added to it:

See Also

(defn download-photo! [{:keys [flickr out-dir] :as ctx}
                       {:keys [filename] :as photograph}]
  (let [p-interface (.getPhotosInterface (:client flickr))
        out-file (fs/file out-dir filename)]
    (with-open [in (BufferedInputStream. (.getImageAsStream p-interface (:object photo) Size/LARGE))
                out (FileOutputStream. out-file)]
      (io/copy in out))
    (assoc photograph :out-file out-file)))

(remark

  (download-photo! (assoc ctx :out-dir album-dir) photograph)
  ;; => {:description nil,
  ;;     :date-taken nil,
  ;;     :geo-data nil,
  ;;     :rotation -1,
  ;;     :width 0,
  ;;     :title "sean-hargreaves-phoenix-new-5-final-a",
  ;;     :filename "53460147147.jpg",
  ;;     :id "53460147147",
  ;;     :out-file
  ;;     #object[java.io.File 0x44f5abfa "/tmp/72177720314024335/53460147147.jpg"],
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photos.Photo 0x4d361782 "com.flickr4java.flickr.photos.Photo@14ea992b"],
  ;;     :peak 0}

  )

A lot nicer! We will now do the identical factor to download-album!:

(defn download-album! [ctx {:keys [id photos] :as album}]
  (let [album-dir (->> id (fs/file (fs/temp-dir)) fs/create-dirs fs/file)
        photos (->> photos
                    (map (partial download-photo! (assoc ctx :out-dir album-dir)))
                    doall)]
    (assoc album :out-dir album-dir, :photographs photographs)))

(remark

  (download-album! ctx album)
  ;; => {:id "72177720314024335",
  ;;     :title "clickr demo",
  ;;     :description "Photograph album demo for my clickr weblog publish",
  ;;     :photographs
  ;;     ({:description nil,
  ;;       :date-taken nil,
  ;;       :geo-data nil,
  ;;       :rotation -1,
  ;;       :width 0,
  ;;       :title "sean-hargreaves-phoenix-new-5-final-a",
  ;;       :filename "53460147147.jpg",
  ;;       :id "53460147147",
  ;;       :out-file
  ;;       #object[java.io.File 0x163fa069 "/tmp/72177720314024335/53460147147.jpg"],
  ;;       :object
  ;;       #object[com.flickr4java.flickr.photos.Photo 0x4d361782 "com.flickr4java.flickr.photos.Photo@14ea992b"],
  ;;       :peak 0}
  ;; [...]
  ;;      {:description nil,
  ;;       :date-taken nil,
  ;;       :geo-data nil,
  ;;       :rotation -1,
  ;;       :width 0,
  ;;       :title "daniel-jennings-img-7554",
  ;;       :filename "53460151727.jpg",
  ;;       :id "53460151727",
  ;;       :out-file
  ;;       #object[java.io.File 0x289e2344 "/tmp/72177720314024335/53460151727.jpg"],
  ;;       :object
  ;;       #object[com.flickr4java.flickr.photos.Photo 0x805e360 "com.flickr4java.flickr.photos.Photo@436e36e8"],
  ;;       :peak 0}),
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photosets.Photoset 0x19d70721 "com.flickr4java.flickr.photosets.Photoset@19d70721"],
  ;;     :out-dir #object[java.io.File 0x67b12ec4 "/tmp/72177720314024335"]}

  )

Importing to S3

We’re a lot of the method there now. The ultimate piece of the puzzle is taking our pretty listing filled with downloaded information and placing it on S3, as described above. To perform this, let’s avail ourselves of the Cognitect aws-api library that has served us
so well in the past
. First, let’s add it to deps.edn:

{:paths ["src" "dev"]
 :deps {babashka/fs {:mvn/model "0.4.19"}
        com.cognitect.aws/api {:mvn/model "0.8.686"}
        com.cognitect.aws/endpoints {:mvn/model "1.1.12.504"}
        com.cognitect.aws/s3 {:mvn/model "848.2.1413.0"}
        com.flickr4java/flickr4java {:mvn/model "3.0.1"}}}

Sadly, we’ll now must restart our REPL. I actually need to get scorching reloading working! Seems like a mission for an additional day, although I am certain will probably be incredibly simple

In any case, having now restarted our REPL, let’s create a clickr.s3 namespace for ourselves:

(ns clickr.s3
  (:require [cognitect.aws.client.api :as aws]
            [clojure.string :as str]
            [clojure.java.io :as io])
  (:import (java.io ByteArrayInputStream)))

Now we’ll want an S3 consumer. Let’s comply with the identical sample we used for our Flickr consumer:

(defn init-client [{:keys [aws-region] :as ctx}]
  (let [client (aws/client {:api :s3, :region aws-region})]
    (assoc ctx :s3 {:consumer consumer})))

Now we will truly use the identical config as we used earlier than! 🤯 We simply want so as to add the AWS area in there, then we will name init-client:


(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"
               :aws-region "eu-west-1"})
  ;; => #'clickr.s3/config

  (def ctx (init-client config))
  ;; => #'clickr.s3/ctx

  (-> ctx :s3 :consumer)
  ;; => #object[cognitect.aws.client.impl.Client 0x6b5a7704 "cognitect.aws.client.impl.Client@6b5a7704"]

  )

With a working consumer in hand, we will write a operate to add a photograph. Let’s comply with the identical sample as download-photo!: do the side-effecting factor after which return the photograph, assoc-ing within the S3 key the place we uploaded it.

(defn upload-photo! [{:keys [s3 s3-bucket] :as ctx}
                     {:keys [out-file] :as photograph}]
  (let [s3-key :???]
    (aws/invoke (:consumer s3)
                {:op :PutObject
                 :request {:Bucket s3-bucket
                           :Key s3-key
                           :Physique :???}})
    (assoc photograph :s3-key s3-key)))

OK, a pair issues right here. First, we have to add the S3 bucket to our context. That is straightforward sufficient:


(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"
               :aws-region "eu-west-1"
               :s3-bucket "photographs.jmglov.web"})
  ;; => #'clickr.s3/config

  (def ctx (init-client config))
  ;; => #'clickr.s3/ctx

  ctx
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :aws-region "eu-west-1",
  ;;     :s3-bucket "photographs.jmglov.web",
  ;;     :s3
  ;;     {:consumer
  ;;      #object[cognitect.aws.client.impl.Client 0xcd319cb "cognitect.aws.client.impl.Client@cd319cb"]}}

  )

Subsequent, we want a approach to learn within the photograph file. babashka.fs involves the rescue once more with a operate known as read-all-bytes! Let’s require in babashka.fs:

(ns clickr.s3
  (:require [babashka.fs :as fs]
            [cognitect.aws.client.api :as aws]))

And now we will use that in our upload-photo! operate:

(defn upload-photo! [{:keys [s3 s3-bucket] :as ctx}
                     {:keys [out-file] :as photograph}]
  (let [s3-key :???]
    (aws/invoke (:consumer s3)
                {:op :PutObject
                 :request {:Bucket s3-bucket
                           :Key s3-key
                           :Physique (fs/read-all-bytes out-file)}})
    (assoc photograph :s3-key s3-key)))

Lastly, we have to work out what the S3 key ought to be. Utilizing the conference from download-album!, we all know the photograph shall be in a listing akin to the album ID. Let’s use that as the important thing and prepend clickr/ to it in order that we do not pollute the top-level of the S3 bucket with a bunch of nonsense. We will seize the listing and filename utilizing our outdated pal babashka.fs, in fact:

(remark

  (def photograph {:out-file "/tmp/72177720314024335/53460151727.jpg"})
  ;; => #'clickr.s3/photograph

  (-> photograph :out-file fs/file-name)
  ;; => "53460151727.jpg"

  (-> photograph :out-file fs/mother or father fs/file-name)
  ;; => "72177720314024335"

  )

Cool! With this, we’ve got sufficient to assemble the S3 key:

(defn upload-photo! [{:keys [s3 s3-bucket] :as ctx}
                     {:keys [out-file] :as photograph}]
  (let [s3-key (format "%s/%s/%s"
                       "clickr"
                       (-> photo :out-file fs/parent fs/file-name)
                       (-> photo :out-file fs/file-name))]
    (aws/invoke (:consumer s3)
                {:op :PutObject
                 :request {:Bucket s3-bucket
                           :Key s3-key
                           :Physique (fs/read-all-bytes out-file)}})
    (assoc photograph :s3-key s3-key)))

We will do that out:

(remark

  (upload-photo! ctx photograph)
  ;; => {:out-file "/tmp/72177720314024335/53460151727.jpg",
  ;;     :s3-key "clickr/72177720314024335/53460151727.jpg"}

  )

And one ought to all the time belief however confirm, proper?

: jmglov@laurana; aws s3 ls s3://photographs.jmglov.web/clickr/72177720314024335/53460151727.jpg
2024-01-17 12:06:17      90743 53460151727.jpg

Wow, that was surprisingly painless!

One factor that is bothering me slightly, although, is that hardcoded “clickr” in there. Let’s transfer that to the config like we did with the S3 bucket:

(defn upload-photo! [{:keys [s3 s3-bucket s3-prefix] :as ctx}
                     {:keys [out-file] :as photograph}]
  (let [s3-key (format "%s/%s/%s"
                       s3-prefix
                       (-> photo :out-file fs/parent fs/file-name)
                       (-> photo :out-file fs/file-name))]
    (aws/invoke (:consumer s3)
                {:op :PutObject
                 :request {:Bucket s3-bucket
                           :Key s3-key
                           :Physique (fs/read-all-bytes out-file)}})
    (assoc photograph :s3-key s3-key)))

(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"
               :aws-region "eu-west-1"
               :s3-bucket "photographs.jmglov.web"
               :s3-prefix "clickr"})
  ;; => #'clickr.s3/config

  (def ctx (init-client config))
  ;; => #'clickr.s3/ctx

  ctx
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :aws-region "eu-west-1",
  ;;     :s3-bucket "photographs.jmglov.web",
  ;;     :s3-prefix "clickr",
  ;;     :s3
  ;;     {:consumer
  ;;      #object[cognitect.aws.client.impl.Client 0x2dc1f1c2 "cognitect.aws.client.impl.Client@2dc1f1c2"]}}

  (upload-photo! ctx photograph)
  ;; => {:out-file "/tmp/72177720314024335/53460151727.jpg",
  ;;     :s3-key "clickr/72177720314024335/53460151727.jpg"}

  )

Having achieved this, writing a operate to add all of the photographs in an album is kind of easy:

(defn upload-album! [ctx {:keys [photos] :as album}]
  (replace album :photographs #(doall (map (partial upload-photo! ctx) %))))

(remark

  (def album-dir "/tmp/72177720314024335")
  ;; => #'clickr.s3/album-dir

  (def album {:id "72177720314024335"
              :out-dir album-dir
              :photographs (->> (fs/glob album-dir "*")
                           (map (fn [out-file] {:out-file (str out-file)})))})
  ;; => #'clickr.s3/album

  (upload-album! ctx album)
  ;; => {:id "72177720314024335",
  ;;     :out-dir "/tmp/72177720314024335",
  ;;     :photographs
  ;;     ({:out-file "/tmp/72177720314024335/53461405604.jpg",
  ;;       :s3-key "clickr/72177720314024335/53461405604.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53460163402.jpg",
  ;;       :s3-key "clickr/72177720314024335/53460163402.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53460161007.jpg",
  ;;       :s3-key "clickr/72177720314024335/53460161007.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53460147147.jpg",
  ;;       :s3-key "clickr/72177720314024335/53460147147.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53461214223.jpg",
  ;;       :s3-key "clickr/72177720314024335/53461214223.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53461091151.jpg",
  ;;       :s3-key "clickr/72177720314024335/53461091151.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53460151727.jpg",
  ;;       :s3-key "clickr/72177720314024335/53460151727.jpg"}
  ;;      {:out-file "/tmp/72177720314024335/53461088046.jpg",
  ;;       :s3-key "clickr/72177720314024335/53461088046.jpg"})}

  )

Tying all of it collectively

All of this appears moderately cheap, however our REPL-driven growth of the clickr.s3 namespace was fairly mocktacular, which fills me with a obscure sense of unease. Let’s make sure that we will add an trustworthy to goodness album!

To verify we have no cruft mendacity round in our REPL, let’s create a brand new namespace and require within the flickr and s3 stuff:

(ns person
  (:require [clickr.flickr :as flickr]
            [clickr.s3 :as s3]))

We’ll want some config, which we will simply copy and paste straight from our experiments within the s3 namespace:

(remark

  (def config {:api-key "beefface5678910"
               :secret "facecafe1234"
               :aws-region "eu-west-1"
               :s3-bucket "photographs.jmglov.web"
               :s3-prefix "clickr"})
  ;; => #'person/config

  )

With this config firmly in hand, we will create flickr and s3 purchasers:

(remark

  (def ctx (-> config flickr/init-client s3/init-client))
  ;; => #'person/ctx

  ctx
  ;; => {:api-key "beefface5678910",
  ;;     :secret "facecafe1234",
  ;;     :aws-region "eu-west-1",
  ;;     :s3-bucket "photographs.jmglov.web",
  ;;     :s3-prefix "clickr",
  ;;     :flickr
  ;;     {:consumer
  ;;      #object[com.flickr4java.flickr.Flickr 0x2c4181a2 "com.flickr4java.flickr.Flickr@2c4181a2"],
  ;;      :auth-store
  ;;      #object[com.flickr4java.flickr.util.FileAuthStore 0x5368861c "com.flickr4java.flickr.util.FileAuthStore@5368861c"],
  ;;      :auth
  ;;      #object[com.flickr4java.flickr.auth.Auth 0x7c8ff0d6 "com.flickr4java.flickr.auth.Auth@7c8ff0d6"]},
  ;;     :s3
  ;;     {:consumer
  ;;      #object[cognitect.aws.client.impl.Client 0x5b35646d "cognitect.aws.client.impl.Client@5b35646d"]}}

  )

Obtain an album:

(remark

  (def album (->> (flickr/get-albums' ctx)
                  first
                  (flickr/download-album!' ctx)))
  ;; => #'person/album

  (->> album :photographs (map :out-file))
  ;; => (#object[java.io.File 0x1c2cd5ae "/tmp/72177720314024335/53460147147.jpg"]
  ;;     #object[java.io.File 0x1678a01 "/tmp/72177720314024335/53461405604.jpg"]
  ;;     #object[java.io.File 0x4f708220 "/tmp/72177720314024335/53461091151.jpg"]
  ;;     #object[java.io.File 0x75723234 "/tmp/72177720314024335/53461088046.jpg"]
  ;;     #object[java.io.File 0x6cace2fd "/tmp/72177720314024335/53460163402.jpg"]
  ;;     #object[java.io.File 0x51265aeb "/tmp/72177720314024335/53460161007.jpg"]
  ;;     #object[java.io.File 0x1a0ca36d "/tmp/72177720314024335/53461214223.jpg"]
  ;;     #object[java.io.File 0x7e8bc18a "/tmp/72177720314024335/53460151727.jpg"])

  )

And add it to S3!

(remark

  (s3/upload-album! ctx album)
  ;; => {:id "72177720314024335",
  ;;     :title "clickr demo",
  ;;     :description "Photograph album demo for my clickr weblog publish",
  ;;     :photographs
  ;;     ({:description nil,
  ;;       :date-taken nil,
  ;;       :geo-data nil,
  ;;       :rotation -1,
  ;;       :width 0,
  ;;       :title "sean-hargreaves-phoenix-new-5-final-a",
  ;;       :filename "53460147147.jpg",
  ;;       :id "53460147147",
  ;;       :s3-key "clickr/72177720314024335/53460147147.jpg",
  ;;       :out-file
  ;;       #object[java.io.File 0x1c2cd5ae "/tmp/72177720314024335/53460147147.jpg"],
  ;;       :object
  ;;       #object[com.flickr4java.flickr.photos.Photo 0x3bbff9e1 "com.flickr4java.flickr.photos.Photo@14ea992b"],
  ;;       :peak 0}
  ;; [...]
  ;;      {:description nil,
  ;;       :date-taken nil,
  ;;       :geo-data nil,
  ;;       :rotation -1,
  ;;       :width 0,
  ;;       :title "daniel-jennings-img-7554",
  ;;       :filename "53460151727.jpg",
  ;;       :id "53460151727",
  ;;       :s3-key "clickr/72177720314024335/53460151727.jpg",
  ;;       :out-file
  ;;       #object[java.io.File 0x7e8bc18a "/tmp/72177720314024335/53460151727.jpg"],
  ;;       :object
  ;;       #object[com.flickr4java.flickr.photos.Photo 0x1e20bd4c "com.flickr4java.flickr.photos.Photo@436e36e8"],
  ;;       :peak 0}),
  ;;     :object
  ;;     #object[com.flickr4java.flickr.photosets.Photoset 0x6411aa2d "com.flickr4java.flickr.photosets.Photoset@6411aa2d"],
  ;;     :out-dir #object[java.io.File 0x377eb990 "/tmp/72177720314024335"]}

  )

And test that each one is correct with the world:

: jmglov@laurana; aws s3 ls s3://photographs.jmglov.web/clickr/72177720314024335/
2024-01-17 12:48:26     125643 53460147147.jpg
2024-01-17 12:48:29      90743 53460151727.jpg
2024-01-17 12:48:28      88417 53460161007.jpg
2024-01-17 12:48:28     185725 53460163402.jpg
2024-01-17 12:48:27     178392 53461088046.jpg
2024-01-17 12:48:27     106074 53461091151.jpg
2024-01-17 12:48:29      88013 53461214223.jpg
2024-01-17 12:48:26      98035 53461405604.jpg

A woman on a beach at sunrise with her head thrown back, saying

However wait only a second right here…

The insufferable lightness of being dissatisfied

A person sitting alone, holding their face in their hands

Very similar to Angelica Schuyler, I am going to by no means be happy, as a result of having to browse my albums with the S3 console is form of a let down, to not point out that this:

The Flickr site, displaying the photos in the clickr demo album

appears method higher than this:

The AWS S3 console, displaying the files we uploaded as a table of text

What’s a younger man to do? Nicely, you may have to stay round for the following instalment of this exciting series, which I promise I am going to truly write, as a result of I’ve began writing it already and… um… belief me?

Half 2: clickr goes frontend

Talk about this publish here.

Revealed: 2024-01-17


Tagged:


clojure


aws


clickr

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top