My usual post-trip photo workflow is this: (1) For every day of the trip, create an "All Photos" album, and add the appropriate day's photos to that album. (2) For every day of the trip, create a "Blog Photos" album. (3) Review each day's All Photos, copying the best pics into the Blog Photos. (4) For each day's blog post, include the contents of the "Blog Photos" album. Creating the 'All' and 'Blog' albums isn't hard, but it is tedious. The programmer in me doesn't do tedious.
A while back, I realized that if Google had a Photos API, I could script much of the above process. I could effortlessly create and copy each day's photos into the appropriate 'All' album and setup empty 'Blog' albums ready to fill. Alas, when I reviewed the available API I couldn't see any way to create a new album. My brilliant hack would have to wait.
I recently revisited this challenge, and to my surprise it appears Google has filled out its Photos API nicely. Most importantly, you can now create albums. I was psyched!
Using my youtube_tool as inspiration, I was able to throw together gphoto_auth and gphoto_tools. The former authenticates a command line session against your Google Photos account. The latter is the beginnings of a tool for working with the Google Photos API. Currently, gphoto_tools is limited: you can get a list of albums, as well as search for photos. To build out the above workflow, I'm going to need to add the ability to create albums and properly handle the nextPageToken. Still, even in its simple form, gphoto_tools is showing promise.
Here's a sample session:
# Setup a command line auth context $ gphoto_auth init Visit: https://accounts.google.com/o/oauth2/auth?client_id=278279054954-h2du6fu5qtk9jhh2euqn0o7kdesg819u.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary&response_type=code Code? ######################### # List out albums $ gphoto_tools -a albums AHnaBgvFACY2NHSg9kiIJ-tDfTGab2vOCa8aWeKmJovY0F-JhHbEL_0nuvII2w_wAJ7IT0Yk7IzP|Snap Circuits AHnaBguW1U9UKnD5bzLS9ktWlxfgtOtyfCkSq0k4O2C0jurETHIJF4gXC2FsR3NkuiOCFN6fAhha|New Year's Day Hike 2020 ... # 'Search' for the images within a given album (by album ID) $ gphoto_tools -a search -q "{'albumId' : 'AHnaBguW1U9UKnD5bzLS9ktWlxfgtOtyfCkSq0k4O2C0jurETHIJF4gXC2FsR3NkuiOCFN6fAhha' }" AHnaBgtpDZNj7odMrv7gquGGcVV02k6ORDnzu5yBSEmWDdCYbvjjp49DfBFOa5TQzLJOrGOXO1xFPe22LChRUiaU3Bk_QnqOCw|20200101_130629.jpg AHnaBguVuD5WJYJ8pO67lC2PL7QtS-JZ1nEJsti6S_-fD6S9Tn7TIj_t_NKLVAKaU-3awI3S8Oj4aiJxMBROXe2ESHLF47VEzw|20200101_132036.jpg ... # -v spits out raw json, you can use .jq to work with $ gphoto_tools -v -a search -q "{'albumId' : 'AHnaBguW1U9UKnD5bzLS9ktWlxfgtOtyfCkSq0k4O2C0jurETHIJF4gXC2FsR3NkuiOCFN6fAhha' }" | \ jq '.mediaItems[0]' { "id": "AHnaBgtpDZNj7odMrv7gquGGcVV02k6ORDnzu5yBSEmWDdCYbvjjp49DfBFOa5TQzLJOrGOXO1xFPe22LChRUiaU3Bk_QnqOCw", "productUrl": "https://photos.google.com/lr/album/AHnaBguW1U9UKnD5bzLS9ktWlxfgtOtyfCkSq0k4O2C0jurETHIJF4gXC2FsR3NkuiOCFN6fAhha/photo/AHnaBgtpDZNj7odMrv7gquGGcVV02k6ORDnzu5yBSEmWDdCYbvjjp49DfBFOa5TQzLJOrGOXO1xFPe22LChRUiaU3Bk_QnqOCw", "baseUrl": "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJ4PhDhqa6V1Zw4yQusBcqplUDMgw3yHFFnr4Ugs5Dkq94Oh2hSsO1bnk4SVCwBpcVSDEi0zjoow_D8EPO2B7Pa7VCZHHu2SKXGNk_pwzZgK8njQnu2nr0mou4mHIjiEMcx7KO/", "mimeType": "image/jpeg", "mediaMetadata": { "creationTime": "2020-01-01T18:06:25Z", "width": "3264", "height": "2448", "photo": { "cameraMake": "samsung", "cameraModel": "SM-G965U", "focalLength": 2.92, "apertureFNumber": 1.7, "isoEquivalent": 40 } }, "filename": "20200101_130629.jpg" } # Process the raw json to get at image metadata $ gphoto_tools -v -a search -q "{'albumId' : 'AHnaBguW1U9UKnD5bzLS9ktWlxfgtOtyfCkSq0k4O2C0jurETHIJF4gXC2FsR3NkuiOCFN6fAhha' }" | \ jq -r '.mediaItems[] | .filename + ":" + .mediaMetadata.photo.cameraModel' 20200101_130629.jpg:SM-G965U 20200101_132036.jpg:SM-G965U # Search your images by date range. Clunky, but powerful. $ gphoto_tools -a search -q "{'filters': {'dateFilter' : { 'ranges' : [ {'startDate': { 'year':2018, 'month':4, 'day':1}, 'endDate': {'year': 2018, 'month':4, 'day':7} } ] } } }" AHnaBgs8QzksmxLn9bGvkG0IrQaTJwm4uS1ViuljkbB35OeRGE8R9zrOp4AEn7a57QJ9xEUXZTC2hCUZ7ptzYs6GcUgd6af_6w|20180405_175606.jpg AHnaBgshr_njKJygznaBLZSbAoaD-d_WYfifvzdHHninNWArHfrRqqnm0LGZ3_n0pt-Bl1OnWxoHHOkcRmKHIf5uEJekMbrJjw|20180405_102015.jpg ... # The -u flag causes URLs to be returned instead of IDs $ gphoto_tools -u -a search -q "{'filters': {'dateFilter' : { 'ranges' : [ {'startDate': { 'year':2018, 'month':4, 'day':1}, 'endDate': {'year': 2018, 'month':4, 'day':7} } ] } } }" https://photos.google.com/lr/photo/AHnaBgs8QzksmxLn9bGvkG0IrQaTJwm4uS1ViuljkbB35OeRGE8R9zrOp4AEn7a57QJ9xEUXZTC2hCUZ7ptzYs6GcUgd6af_6w|20180405_175606.jpg https://photos.google.com/lr/photo/AHnaBgshr_njKJygznaBLZSbAoaD-d_WYfifvzdHHninNWArHfrRqqnm0LGZ3_n0pt-Bl1OnWxoHHOkcRmKHIf5uEJekMbrJjw|20180405_102015.jpg ...
I'm the fence as to whether I should make a simplified querying interface, say '-r' for date range. Or, whether it's best to leave the filter as plain JSON that corresponds to the API docs. The former makes the tool easier to use, while the latter maximizes flexibility. Time will tell which is the best route to go.
Here's both scripts:
gphoto_auth
#!/bin/bash ## ## Authenticate with Google Photos API ## USAGE="`basename $0` {auth|refresh|token} ctx" CTX_DIR=$HOME/.gphotos_auth CLIENT_ID=XXXXXXXXXXXXXXXXXXXX CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXX SCOPE='https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary' ctx=default function usage { echo "Usage: `basename $0` [-h] [-c context] {init|token}" exit } function age { if [ `uname` = 'Darwin' ] ; then modified=`stat -f "%a" $1` else modified=`stat -c %X $1` fi now=`date +%s` expr $now - $modified } function refresh { refresh_token=`cat $CTX_DIR/$ctx.refresh_token` curl -si \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET \ -d refresh_token=$refresh_token \ -d grant_type=refresh_token \ https://accounts.google.com/o/oauth2/token > $CTX_DIR/$ctx.refresh grep access_token $CTX_DIR/$ctx.refresh | sed -e 's/.*: "//' -e 's/",//' > $CTX_DIR/$ctx.access_token } while getopts :hc: opt ; do case $opt in c) ctx=$OPTARG ;; h) usage ;; esac done shift $(($OPTIND - 1)) cmd=$1 ; shift mkdir -p $CTX_DIR case $cmd in init) echo "Visit:" echo "https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=$SCOPE&response_type=code" echo -n "Code? " read code curl -s \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET \ -d code=$code \ -d grant_type=authorization_code \ -d redirect_uri=urn:ietf:wg:oauth:2.0:oob \ https://www.googleapis.com/oauth2/v4/token > $CTX_DIR/$ctx.init grep access_token $CTX_DIR/$ctx.init | sed -e 's/.*: "//' -e 's/",//' > $CTX_DIR/$ctx.access_token grep refresh_token $CTX_DIR/$ctx.init | sed -e 's/.*: "//' -e 's/"//' > $CTX_DIR/$ctx.refresh_token echo "Done" ;; token) if [ ! -f $CTX_DIR/$ctx.access_token ] ; then echo "Unknown context: $ctx. Try initing first." exit fi age=`age $CTX_DIR/$ctx.access_token` if [ $age -gt 3600 ] ; then refresh fi cat $CTX_DIR/$ctx.access_token ;; *) usage esac
gphoto_tools
#!/bin/bash ## ## command line tool for working with the google photos API. ## https://developers.google.com/photos/library/guides/overview ## API_BASE=https://photoslibrary.googleapis.com/v1 AUTH_TOKEN=`gphoto_auth token` ID_COL=.id function usage { echo -n "Usage: `basename $0` " echo -n "-a {albums|search|get} [-q json-query] [-i id] [-u]" echo "" exit } while getopts ":a:q:i:vu" opt; do case $opt in a) ACTION=$OPTARG ;; q) QUERY=$OPTARG ;; i) ID=$OPTARG ;; v) VERBOSE=yes ;; u) ID_COL=.productUrl ;; \?) usage ;; esac done function invoke { buffer=/tmp/gphoto.buffer.$$ curl -s -H "Authorization: Bearer $AUTH_TOKEN" "$@" > $buffer cat $buffer } function filter { if [ -z "$VERBOSE" ] ; then jq "$@" else cat fi } case $ACTION in albums) invoke -G $API_BASE/albums | filter -r ".albums[] | $ID_COL + \"|\" + .title" ;; search) if [ -z "$QUERY" ] ; then invoke $API_BASE/mediaItems | filter -r ".mediaItems[] | $ID_COL + \"|\" + .filename" else invoke -X POST $API_BASE/mediaItems:search -H "Content-Type: application/json" -d "$QUERY" | filter -r ".mediaItems[] | $ID_COL + \"|\" + .filename" fi ;; get) if [ -z "$ID" ] ; then echo "Missing -ivalue" usage else invoke $API_BASE/mediaItem/$ID | filter -r '.productUrl' fi ;; *) usage ;; esac
No comments:
Post a Comment