Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A few minor changes #9

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
# gphotos-upload
Simple but flexible script to upload photos to Google Photos. Useful if you have photos in a directory structure that you want to reflect as Google Photos albums.

## Usage
## Usage

```
usage: upload.py [-h] [--auth auth_file] [--album album_name]
[--log log_file]
[photo [photo ...]]
usage: upload.py [-h] [--auth auth_file] [--album album_name] [--log log_file] photo_dir

Upload photos to Google Photos.

positional arguments:
photo filename of a photo to upload
photo_dir path to the directory containing image files (filenames must end with .jpg extension)

optional arguments:
-h, --help show this help message and exit
--auth auth_file file for reading/storing user authentication tokens
--album album_name name of photo album to create (if it doesn't exist). Any
uploaded photos will be added to this album.
--auth auth_file file for reading/storing user authentication tokens. Default ./client_secret.json
--album album_name name of photo album to create (if it has not been created by this script). Any uploaded photos will be added to this album.
--log log_file name of output file for log messages
```


## Setup

### Obtaining a Google Photos API key
### Obtaining Google Photos API credentials

1. Obtain a Google Photos API key (Client ID and Client Secret) by following the instructions on [Getting started with Google Photos REST APIs](https://developers.google.com/photos/library/guides/get-started)
1. Obtain a Google Photos API credentials (Client ID and Client Secret) by following the instructions on [Getting started with Google Photos REST APIs](https://developers.google.com/photos/library/guides/get-started)

**NOTE** When selecting your application type in Step 4 of "Request an OAuth 2.0 client ID", please select "Other". There's also no need to carry out step 5 in that section.

2. Replace `YOUR_CLIENT_ID` in the client_id.json file with the provided Client ID.
3. Replace `YOUR_CLIENT_SECRET` in the client_id.json file wiht the provided Client Secret.
2. Download the credentials file to a secure place in your machine. The path to this file should be passed to upload.py script by using --auth optional argument. Otherwise, the credentials file will be assumed to be at ./client_secret.json (what would probably be wrong)

### Installing dependencies and running the script

Expand All @@ -40,6 +36,5 @@ optional arguments:
3. Change to the directory where you installed this script
4. Run `pipenv install` to download and install all the dependencies
5. Run `pipenv shell` to open a shell with all the dependencies available (you'll need to do this every time you want to run the script)
6. Now run the script via `python upload.py` as desired. Use `python upload.py -h` to get help.
6. Now run the script via `python upload.py` as desired. Use `python upload.py -h` to get help. As stated in previous section, you should probably use --auth option here


11 changes: 0 additions & 11 deletions client_id.json

This file was deleted.

103 changes: 59 additions & 44 deletions upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@
import json
import os.path
import argparse
from pathlib import Path
import logging


def parse_args(arg_input=None):
parser = argparse.ArgumentParser(description='Upload photos to Google Photos.')
parser.add_argument('--auth ', metavar='auth_file', dest='auth_file',
help='file for reading/storing user authentication tokens')
default='client_secret.json',
help='file for reading/storing user authentication tokens. Default ./client_secret.json')
parser.add_argument('--album', metavar='album_name', dest='album_name',
help='name of photo album to create (if it doesn\'t exist). Any uploaded photos will be added to this album.')
help='name of photo album to create (if it has not been created by this script). Any uploaded photos will be added to this album.')
parser.add_argument('--log', metavar='log_file', dest='log_file',
help='name of output file for log messages')
parser.add_argument('photos', metavar='photo',type=str, nargs='*',
help='filename of a photo to upload')
help='name of output file for log messages')
parser.add_argument('photos', metavar='photo_dir', type=lambda p: Path(p).absolute(),
default=Path(__file__).absolute().parent / "data",
help='path to the directory containing image files (filenames must end with .jpg extension)')
return parser.parse_args(arg_input)


def auth(scopes):
def auth(auth_token_file, scopes):
flow = InstalledAppFlow.from_client_secrets_file(
'client_id.json',
auth_token_file,
scopes=scopes)

credentials = flow.run_local_server(host='localhost',
Expand All @@ -32,10 +36,11 @@ def auth(scopes):

return credentials


def get_authorized_session(auth_token_file):

scopes=['https://www.googleapis.com/auth/photoslibrary',
'https://www.googleapis.com/auth/photoslibrary.sharing']
scopes = ['https://www.googleapis.com/auth/photoslibrary',
'https://www.googleapis.com/auth/photoslibrary.sharing']

cred = None

Expand All @@ -47,9 +52,8 @@ def get_authorized_session(auth_token_file):
except ValueError:
logging.debug("Error loading auth tokens - Incorrect format")


if not cred:
cred = auth(scopes)
cred = auth(auth_token_file, scopes)

session = AuthorizedSession(cred)

Expand Down Expand Up @@ -79,10 +83,11 @@ def save_cred(cred, auth_file):

# Generator to loop through all albums


def getAlbums(session, appCreatedOnly=False):

params = {
'excludeNonAppCreatedData': appCreatedOnly
'excludeNonAppCreatedData': appCreatedOnly
}

while True:
Expand All @@ -104,9 +109,10 @@ def getAlbums(session, appCreatedOnly=False):
else:
return


def create_or_retrieve_album(session, album_title):

# Find albums created by this app to see if one matches album_title
# Find albums created by this app to see if one matches album_title

for a in getAlbums(session, True):
if a["title"].lower() == album_title.lower():
Expand All @@ -116,8 +122,8 @@ def create_or_retrieve_album(session, album_title):

# No matches, create new album

create_album_body = json.dumps({"album":{"title": album_title}})
#print(create_album_body)
create_album_body = json.dumps({"album": {"title": album_title}})
# print(create_album_body)
resp = session.post('https://photoslibrary.googleapis.com/v1/albums', create_album_body).json()

logging.debug("Server response: {}".format(resp))
Expand All @@ -129,6 +135,7 @@ def create_or_retrieve_album(session, album_title):
logging.error("Could not find or create photo album '\{0}\'. Server Response: {1}".format(album_title, resp))
return None


def upload_photos(session, photo_file_list, album_name):

album_id = create_or_retrieve_album(session, album_name) if album_name else None
Expand All @@ -140,40 +147,45 @@ def upload_photos(session, photo_file_list, album_name):
session.headers["Content-type"] = "application/octet-stream"
session.headers["X-Goog-Upload-Protocol"] = "raw"

for photo_file_name in photo_file_list:
for photo_file_name in photo_file_list.glob('**/*.jpg'):

try:
photo_file = open(photo_file_name, mode='rb')
photo_bytes = photo_file.read()
except OSError as err:
logging.error("Could not read file \'{0}\' -- {1}".format(photo_file_name, err))
continue
try:
photo_file = open(photo_file_name, mode='rb')
photo_bytes = photo_file.read()
except OSError as err:
logging.error("Could not read file \'{0}\' -- {1}".format(photo_file_name, err))
continue

session.headers["X-Goog-Upload-File-Name"] = os.path.basename(photo_file_name)
session.headers["X-Goog-Upload-File-Name"] = os.path.basename(photo_file_name)

logging.info("Uploading photo -- \'{}\'".format(photo_file_name))
logging.info("Uploading photo -- \'{}\'".format(photo_file_name))

upload_token = session.post('https://photoslibrary.googleapis.com/v1/uploads', photo_bytes)
upload_token = session.post('https://photoslibrary.googleapis.com/v1/uploads', photo_bytes)

if (upload_token.status_code == 200) and (upload_token.content):
if (upload_token.status_code == 200) and (upload_token.content):

create_body = json.dumps({"albumId":album_id, "newMediaItems":[{"description":"","simpleMediaItem":{"uploadToken":upload_token.content.decode()}}]}, indent=4)
create_body = json.dumps({"albumId": album_id, "newMediaItems": [
{"description": "", "simpleMediaItem": {"uploadToken": upload_token.content.decode()}}]}, indent=4)

resp = session.post('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', create_body).json()
resp = session.post('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', create_body).json()

logging.debug("Server response: {}".format(resp))
logging.debug("Server response: {}".format(resp))

if "newMediaItemResults" in resp:
status = resp["newMediaItemResults"][0]["status"]
if status.get("code") and (status.get("code") > 0):
logging.error("Could not add \'{0}\' to library -- {1}".format(os.path.basename(photo_file_name), status["message"]))
else:
logging.info("Added \'{}\' to library and album \'{}\' ".format(os.path.basename(photo_file_name), album_name))
if "newMediaItemResults" in resp:
status = resp["newMediaItemResults"][0]["status"]
if status.get("code") and (status.get("code") > 0):
logging.error(
"Could not add \'{0}\' to library -- {1}".format(os.path.basename(photo_file_name), status["message"]))
else:
logging.error("Could not add \'{0}\' to library. Server Response -- {1}".format(os.path.basename(photo_file_name), resp))

logging.info("Added \'{}\' to library and album \'{}\' ".format(
os.path.basename(photo_file_name), album_name))
else:
logging.error("Could not upload \'{0}\'. Server Response - {1}".format(os.path.basename(photo_file_name), upload_token))
logging.error(
"Could not add \'{0}\' to library. Server Response -- {1}".format(os.path.basename(photo_file_name), resp))

else:
logging.error(
"Could not upload \'{0}\'. Server Response - {1}".format(os.path.basename(photo_file_name), upload_token))

try:
del(session.headers["Content-type"])
Expand All @@ -182,25 +194,28 @@ def upload_photos(session, photo_file_list, album_name):
except KeyError:
pass


def main():

args = parse_args()

logging.basicConfig(format='%(asctime)s %(module)s.%(funcName)s:%(levelname)s:%(message)s',
datefmt='%m/%d/%Y %I_%M_%S %p',
filename=args.log_file,
level=logging.INFO)
datefmt='%m/%d/%Y %I_%M_%S %p',
filename=args.log_file,
level=logging.INFO)

session = get_authorized_session(args.auth_file)

upload_photos(session, args.photos, args.album_name)

# As a quick status check, dump the albums and their key attributes

print("{:<50} | {:>8} | {} ".format("PHOTO ALBUM","# PHOTOS", "IS WRITEABLE?"))
print("{:<50} | {:>8} | {} ".format("PHOTO ALBUM", "# PHOTOS", "IS WRITEABLE?"))

for a in getAlbums(session):
print("{:<50} | {:>8} | {} ".format(a["title"],a.get("mediaItemsCount", "0"), str(a.get("isWriteable", False))))
print("{:<50} | {:>8} | {} ".format(a["title"], a.get(
"mediaItemsCount", "0"), str(a.get("isWriteable", False))))


if __name__ == '__main__':
main()
main()