Skip to content
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
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ Here below the new configuration options available in the `pibooth`_ configurati

# Credentials file downloaded from Google API
client_id_file =

#Activate or deactivate URL reduction
reduce_url_activated = False

#Service URL for reducing links
reduce_url = https://is.gd/create.php?format=json&url={url}


.. note:: Edit the configuration by running the command ``pibooth --config``.

Expand Down
181 changes: 112 additions & 69 deletions pibooth_google_photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ def pibooth_configure(cfg):
"Album name", "Pibooth")
cfg.add_option(SECTION, 'client_id_file', '',
"Credentials file downloaded from Google API")
cfg.add_option(SECTION, 'reduce_url_activated', False,
"Activate or deactivate URL reduction",
"Reduce URL Activated", False)
cfg.add_option(SECTION, 'reduce_url', 'https://is.gd/create.php?format=json&url={url}',
"Service URL for reducing links",
"Reduce URL", 'https://is.gd/create.php?format=json&url={url}')


@pibooth.hookimpl
Expand Down Expand Up @@ -62,19 +68,61 @@ def pibooth_startup(app, cfg):
app.google_photos = GooglePhotosApi(client_id_file, cfg.join_path(CACHE_FILE))


@pibooth.hookimpl
@pibooth.hookimpl(tryfirst=True)
def state_processing_exit(app, cfg):
"""Upload picture to google photo album"""
"""Upload picture to google photo album and shorten URL if needed"""
if hasattr(app, 'google_photos'):
photo_id = app.google_photos.upload(app.previous_picture_file,
# Déterminer quel fichier uploader
file_to_upload = app.previous_picture_file

# Si on est en mode vidéo et qu'un GIF a été créé, utiliser le GIF
if hasattr(app, 'selected_mode') and app.selected_mode == 'video' and hasattr(app, 'gif_path') and os.path.exists(app.gif_path):
file_to_upload = app.gif_path
LOGGER.info(f"Mode vidéo détecté, utilisation du GIF pour l'upload: {app.gif_path}")
else:
LOGGER.info(f"Mode photo ou pas de GIF, utilisation de l'image: {app.previous_picture_file}")

photo_id = app.google_photos.upload(file_to_upload,
cfg.get(SECTION, 'album_name'))

if not photo_id:
LOGGER.error("Échec critique de l'upload, annulation du processus")
app.previous_picture_url = None
return
if photo_id is not None:
app.previous_picture_url = app.google_photos.get_temp_url(photo_id)

# Ajout de la logique de réduction d'URL
if cfg.getboolean(SECTION, 'reduce_url_activated'):
reduce_service = cfg.get(SECTION, 'reduce_url', fallback='').strip()
if reduce_service:
try:
url = app.previous_picture_url
if not url.startswith('http'):
LOGGER.error("URL non valide pour le raccourcissement : %s", url)
return

api_url = reduce_service.format(url=url)
response = requests.get(api_url)

if response.status_code == 200:
shortened_url = response.json().get("shorturl")
if shortened_url:
app.previous_picture_url = shortened_url
LOGGER.debug(f"URL reduced: {shortened_url}")
else:
LOGGER.error("Invalid response from URL shortener")
else:
LOGGER.error(f"URL shortening error (HTTP {response.status_code}): {response.text}")
except Exception as e:
LOGGER.error(f"URL shortening failed: {str(e)}")
else:
LOGGER.error("URL reduction activated but no service URL configured")
else:
app.previous_picture_url = None



class GooglePhotosApi(object):

"""Google Photos interface.
Expand All @@ -93,7 +141,8 @@ class GooglePhotosApi(object):

URL = 'https://photoslibrary.googleapis.com/v1'
SCOPES = ['https://www.googleapis.com/auth/photoslibrary',
'https://www.googleapis.com/auth/photoslibrary.sharing']
'https://www.googleapis.com/auth/photoslibrary.sharing',
'https://www.googleapis.com/auth/photoslibrary.appendonly']

def __init__(self, client_id_file, token_file="token.json"):
self.client_id_file = client_id_file
Expand Down Expand Up @@ -147,8 +196,12 @@ def _get_authorized_session(self):
credentials.refresh(Request())
self._save_credentials(credentials)


if credentials:
return AuthorizedSession(credentials)
session = AuthorizedSession(credentials)
session.headers.update({'Content-Type': 'application/json'})
return session

return None

def is_reachable(self):
Expand Down Expand Up @@ -203,26 +256,16 @@ def create_album(self, album_name):

LOGGER.error("Can not create Google Photos album '%s'", album_name)
return None

def upload(self, filename, album_name):
"""Upload a photo file to the given Google Photos album.

:param filename: photo file full path
:type filename: str
:param album_name: name of albums to upload
:type album_name: str

:returns: uploaded photo ID
:rtype: str
"""
"""Upload a photo file to the given Google Photos album."""
photo_id = None

if not self.is_reachable():
LOGGER.error("Google Photos upload failure: no internet connexion!")
return photo_id

if not self._session:
# Plugin was disabled at startup but activated after
self._session = self._get_authorized_session()

album_id = self.get_album_id(album_name)
Expand All @@ -232,64 +275,64 @@ def upload(self, filename, album_name):
LOGGER.error("Google Photos upload failure: album '%s' not found!", album_name)
return photo_id

self._session.headers["Content-type"] = "application/octet-stream"
self._session.headers["X-Goog-Upload-Protocol"] = "raw"

with open(filename, mode='rb') as fp:
data = fp.read()

self._session.headers["X-Goog-Upload-File-Name"] = os.path.basename(filename)

LOGGER.info("Uploading picture '%s' to Google Photos", filename)
upload_resp = self._session.post(self.URL + '/uploads', data)

if upload_resp.status_code == 200 and upload_resp.content:
create_body = json.dumps(
{
"albumId": album_id,
"newMediaItems": [
{
"description": "",
"simpleMediaItem": {
"uploadToken": upload_resp.content.decode()
}
}
]
})

resp = self._session.post(self.URL + '/mediaItems:batchCreate', create_body).json()
LOGGER.debug("Google Photos server response: %s", resp)

if "newMediaItemResults" in resp:
status = resp["newMediaItemResults"][0]["status"]
if status.get("code") and (status.get("code") > 0):
LOGGER.error("Google Photos upload failure: can not add '%s' to library: %s",
os.path.basename(filename), status["message"])
else:
LOGGER.info("Google Photos upload successful: '%s' added to album '%s'",
os.path.basename(filename), album_name)

photo_id = resp["newMediaItemResults"][0]['mediaItem']['id']
else:
LOGGER.error("Google Photos upload failure: can not add '%s' to library",
os.path.basename(filename))
# Étape 1: Upload du fichier pour obtenir le token
upload_url = f'{self.URL}/uploads'
headers = {
'Content-Type': 'application/octet-stream',
'X-Goog-Upload-Protocol': 'raw',
'X-Goog-Upload-File-Name': os.path.basename(filename)
}

elif upload_resp.status_code != 200:
LOGGER.error("Google Photos upload failure: can not connect to '%s' (HTTP error %s)",
self.URL, upload_resp.status_code)
else:
LOGGER.error("Google Photos upload failure: no response content from server '%s'",
self.URL)
try:
with open(filename, 'rb') as f:
upload_resp = self._session.post(upload_url, headers=headers, data=f)
except Exception as e:
LOGGER.error("Upload failed: %s", str(e))
return None

if upload_resp.status_code != 200 or not upload_resp.content:
LOGGER.error("Upload error (HTTP %s): %s", upload_resp.status_code, upload_resp.text)
return None

# Étape 2: Création du média dans l'album
create_url = f'{self.URL}/mediaItems:batchCreate'
create_body = {
"albumId": album_id,
"newMediaItems": [{
"simpleMediaItem": {
"uploadToken": upload_resp.content.decode(),
"fileName": os.path.basename(filename)
}
}]
}

try:
del self._session.headers["Content-type"]
del self._session.headers["X-Goog-Upload-Protocol"]
del self._session.headers["X-Goog-Upload-File-Name"]
except KeyError:
pass
create_resp = self._session.post(create_url, json=create_body)
create_resp.raise_for_status()
response_data = create_resp.json()
except Exception as e:
LOGGER.error("Media creation failed: %s", str(e))
return None

if "newMediaItemResults" in response_data:
result = response_data["newMediaItemResults"][0]
status = result.get("status", {})

# Google renvoie parfois "message" au lieu de "code" pour le succès
if status.get("code") == 0 or "Success" in status.get("message", ""):
photo_id = result['mediaItem']['id']
LOGGER.info(f"Upload réussi : {filename} -> ID {photo_id}")
LOGGER.debug("Réponse complète : %s", response_data) # Debug supplémentaire
else:
LOGGER.error("Erreur d'upload : Code=%s | Message=%s",
status.get("code"),
status.get("message"))
else:
LOGGER.error("Invalid server response: %s", response_data)

return photo_id


def get_temp_url(self, photo_id):
"""
Get the temporary URL for the picture (valid 1 hour only).
Expand Down