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 -i value"
usage
else
invoke $API_BASE/mediaItem/$ID | filter -r '.productUrl'
fi
;;
*)
usage
;;
esac
No comments:
Post a Comment