pibooth-pcloud 1.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pibooth_pcloud-1.0.0.dist-info/METADATA +148 -0
- pibooth_pcloud-1.0.0.dist-info/RECORD +8 -0
- pibooth_pcloud-1.0.0.dist-info/WHEEL +6 -0
- pibooth_pcloud-1.0.0.dist-info/entry_points.txt +5 -0
- pibooth_pcloud-1.0.0.dist-info/licenses/LICENSE +21 -0
- pibooth_pcloud-1.0.0.dist-info/top_level.txt +2 -0
- pibooth_pcloud.py +349 -0
- pibooth_pcloud_token.py +85 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pibooth_pcloud
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Pibooth plugin to upload photos to pCloud and display a QR code
|
|
5
|
+
Home-page: https://github.com/ceeeeb/pibooth-pcloud
|
|
6
|
+
Download-URL: https://github.com/ceeeeb/pibooth-pcloud/archive/1.0.0.tar.gz
|
|
7
|
+
Author: Christophe
|
|
8
|
+
License: MIT license
|
|
9
|
+
Keywords: Raspberry Pi,camera,photobooth,pcloud
|
|
10
|
+
Platform: unix
|
|
11
|
+
Platform: linux
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Other Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Natural Language :: English
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
|
|
23
|
+
Description-Content-Type: text/x-rst
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pibooth>=2.0.0
|
|
26
|
+
Requires-Dist: qrcode>=6.1
|
|
27
|
+
Requires-Dist: requests
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: download-url
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: platform
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
===============
|
|
42
|
+
pibooth-pcloud
|
|
43
|
+
===============
|
|
44
|
+
|
|
45
|
+
|PythonVersions| |License|
|
|
46
|
+
|
|
47
|
+
Plugin ``pibooth-pcloud`` pour `pibooth <https://github.com/pibooth/pibooth>`_
|
|
48
|
+
qui envoie automatiquement les photos finales vers un compte
|
|
49
|
+
`pCloud <https://www.pcloud.com>`_ et affiche un QR code vers la galerie
|
|
50
|
+
publique sur l'écran d'attente.
|
|
51
|
+
|
|
52
|
+
Fonctionnalités :
|
|
53
|
+
|
|
54
|
+
- Authentification par email + mot de passe (digest par appel, aucun OAuth à
|
|
55
|
+
créer côté pCloud).
|
|
56
|
+
- Upload d'un album par événement (``folder_path/album_name``) et lien public
|
|
57
|
+
généré sur ce sous-dossier.
|
|
58
|
+
- Synchronisation de rattrapage : au démarrage et après chaque photo, les
|
|
59
|
+
fichiers présents en local mais absents de pCloud sont envoyés, ce qui
|
|
60
|
+
absorbe les coupures Internet pendant un événement.
|
|
61
|
+
|
|
62
|
+
.. note:: Une connexion internet est requise pendant l'événement (ou plus
|
|
63
|
+
tard pour rattraper les uploads).
|
|
64
|
+
|
|
65
|
+
Installation
|
|
66
|
+
------------
|
|
67
|
+
|
|
68
|
+
::
|
|
69
|
+
|
|
70
|
+
pip install pibooth-pcloud
|
|
71
|
+
|
|
72
|
+
Configuration
|
|
73
|
+
-------------
|
|
74
|
+
|
|
75
|
+
Vérifier les identifiants pCloud
|
|
76
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
77
|
+
|
|
78
|
+
Le script fourni valide les identifiants et imprime le bloc à coller dans
|
|
79
|
+
``~/.config/pibooth/pibooth.cfg`` :
|
|
80
|
+
|
|
81
|
+
::
|
|
82
|
+
|
|
83
|
+
pibooth-pcloud-token
|
|
84
|
+
|
|
85
|
+
L'email doit être vérifié côté pCloud — sans vérification, l'API refuse la
|
|
86
|
+
création de liens publics.
|
|
87
|
+
|
|
88
|
+
Exemple de configuration
|
|
89
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
90
|
+
|
|
91
|
+
Voir ``pibooth.cfg.example`` à la racine du dépôt. Bloc à ajouter dans
|
|
92
|
+
``~/.config/pibooth/pibooth.cfg`` :
|
|
93
|
+
|
|
94
|
+
.. code-block:: ini
|
|
95
|
+
|
|
96
|
+
[PCLOUD]
|
|
97
|
+
|
|
98
|
+
# Activer le plugin
|
|
99
|
+
activate = True
|
|
100
|
+
|
|
101
|
+
# Identifiants pCloud (stockés en clair — restreindre les droits du fichier)
|
|
102
|
+
email = your.email@example.com
|
|
103
|
+
password = your-password
|
|
104
|
+
|
|
105
|
+
# Région du compte (EU ou US)
|
|
106
|
+
region = EU
|
|
107
|
+
|
|
108
|
+
# Dossier parent sur pCloud (créé s'il n'existe pas)
|
|
109
|
+
folder_path = /Pibooth
|
|
110
|
+
|
|
111
|
+
# Sous-dossier événement sur pCloud ; le lien public cible ce sous-dossier
|
|
112
|
+
album_name = MonEvenement
|
|
113
|
+
|
|
114
|
+
# Position du QR code (top-left, top-right, bottom-left, bottom-right, center)
|
|
115
|
+
qr_position = top-right
|
|
116
|
+
|
|
117
|
+
# Taille du QR code (3-10)
|
|
118
|
+
qr_size = 5
|
|
119
|
+
|
|
120
|
+
# Marge du QR code par rapport au bord (pixels)
|
|
121
|
+
qr_margin = 10
|
|
122
|
+
|
|
123
|
+
Après édition, restreindre les droits du fichier :
|
|
124
|
+
|
|
125
|
+
::
|
|
126
|
+
|
|
127
|
+
chmod 600 ~/.config/pibooth/pibooth.cfg
|
|
128
|
+
|
|
129
|
+
Fonctionnement
|
|
130
|
+
--------------
|
|
131
|
+
|
|
132
|
+
Au démarrage de pibooth :
|
|
133
|
+
|
|
134
|
+
- Authentification par digest, création du dossier parent et de l'album.
|
|
135
|
+
- Récupération du lien public de l'album, génération du QR code.
|
|
136
|
+
- Première passe de synchronisation : les photos locales absentes de pCloud
|
|
137
|
+
sont envoyées.
|
|
138
|
+
|
|
139
|
+
Après chaque photo (``state_processing_exit``) :
|
|
140
|
+
|
|
141
|
+
- Synchronisation en arrière-plan : la nouvelle photo et toute photo
|
|
142
|
+
restée en retard sont envoyées. L'opération ne bloque pas l'interface.
|
|
143
|
+
|
|
144
|
+
.. |PythonVersions| image:: https://img.shields.io/badge/python-3.6+-green.svg
|
|
145
|
+
:target: https://www.python.org/downloads/
|
|
146
|
+
|
|
147
|
+
.. |License| image:: https://img.shields.io/badge/license-MIT-blue.svg
|
|
148
|
+
:target: https://opensource.org/licenses/MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pibooth_pcloud.py,sha256=dooJGktirwsd0wpFvPIVCRLa2zvG18X9-sKvJNRaALE,12753
|
|
2
|
+
pibooth_pcloud_token.py,sha256=jPI4q5FnLBxI7uewiRUB7L7cXs75oSPJ2RXncp0VHcM,2453
|
|
3
|
+
pibooth_pcloud-1.0.0.dist-info/licenses/LICENSE,sha256=6dgyVqCDdYODzES1xSQng3QB-ku4MXaHwTdXDQxQrsA,1067
|
|
4
|
+
pibooth_pcloud-1.0.0.dist-info/METADATA,sha256=C8rGpGS355YknfLl5qaDZUwsVoxTY4neQ9UD84X5p8w,4332
|
|
5
|
+
pibooth_pcloud-1.0.0.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
6
|
+
pibooth_pcloud-1.0.0.dist-info/entry_points.txt,sha256=kG4UkfOw2WJQFCYwbgDZ1xFk3hfZKTtW6WCUIbJ7HR4,110
|
|
7
|
+
pibooth_pcloud-1.0.0.dist-info/top_level.txt,sha256=smhRoBNpbSZURcXRNuv-W-ltAcIRU3lEeJS7WyI8u04,36
|
|
8
|
+
pibooth_pcloud-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christophe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pibooth_pcloud.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Pibooth plugin for pCloud upload."""
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
import qrcode
|
|
12
|
+
import pygame
|
|
13
|
+
|
|
14
|
+
import pibooth
|
|
15
|
+
from pibooth.utils import LOGGER
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
|
|
19
|
+
SECTION = "PCLOUD"
|
|
20
|
+
|
|
21
|
+
# pCloud API hosts per region
|
|
22
|
+
API_HOSTS = {
|
|
23
|
+
"US": "https://api.pcloud.com",
|
|
24
|
+
"EU": "https://eapi.pcloud.com",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _api_url(host, method):
|
|
29
|
+
return f"{host}/{method}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
###########################################################################
|
|
33
|
+
# HOOKS pibooth
|
|
34
|
+
###########################################################################
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pibooth.hookimpl
|
|
38
|
+
def pibooth_configure(cfg):
|
|
39
|
+
"""Declare the plugin configuration options."""
|
|
40
|
+
cfg.add_option(SECTION, 'activate', True,
|
|
41
|
+
"Enable upload to pCloud",
|
|
42
|
+
"Enable upload", ['True', 'False'])
|
|
43
|
+
cfg.add_option(SECTION, 'email', '',
|
|
44
|
+
"pCloud account email",
|
|
45
|
+
"Email", '')
|
|
46
|
+
cfg.add_option(SECTION, 'password', '',
|
|
47
|
+
"pCloud account password (stored in clear, chmod 600 recommended)",
|
|
48
|
+
"Password", '')
|
|
49
|
+
cfg.add_option(SECTION, 'region', 'EU',
|
|
50
|
+
"pCloud account region (US or EU)",
|
|
51
|
+
"Region", ['US', 'EU'])
|
|
52
|
+
cfg.add_option(SECTION, 'folder_path', '/Pibooth',
|
|
53
|
+
"Remote parent folder on pCloud",
|
|
54
|
+
"Folder Path", '/Pibooth')
|
|
55
|
+
cfg.add_option(SECTION, 'album_name', 'Pibooth',
|
|
56
|
+
"Sub-folder (event album) inside folder_path; the public link points here",
|
|
57
|
+
"Album Name", 'Pibooth')
|
|
58
|
+
cfg.add_option(SECTION, 'qr_position', 'top-left',
|
|
59
|
+
"QR code position (top-left, top-right, bottom-left, bottom-right, center)",
|
|
60
|
+
"QR Position", ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'])
|
|
61
|
+
cfg.add_option(SECTION, 'qr_size', 5,
|
|
62
|
+
"QR code box_size (3-10)",
|
|
63
|
+
"QR Size", '5')
|
|
64
|
+
cfg.add_option(SECTION, 'qr_margin', 10,
|
|
65
|
+
"QR code margin from screen edge in pixels",
|
|
66
|
+
"QR Margin", '10')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pibooth.hookimpl
|
|
70
|
+
def pibooth_startup(app, cfg):
|
|
71
|
+
"""Initialize PCloudUpload instance and create QR code."""
|
|
72
|
+
activate = cfg.getboolean(SECTION, 'activate')
|
|
73
|
+
if not activate:
|
|
74
|
+
LOGGER.info("pCloud upload is disabled")
|
|
75
|
+
app.pcloud = None
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
email = cfg.get(SECTION, 'email').strip()
|
|
79
|
+
password = cfg.get(SECTION, 'password')
|
|
80
|
+
if not email or not password:
|
|
81
|
+
LOGGER.warning("pCloud email or password is not configured")
|
|
82
|
+
app.pcloud = None
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
region = cfg.get(SECTION, 'region').upper()
|
|
86
|
+
api_host = API_HOSTS.get(region, API_HOSTS["EU"])
|
|
87
|
+
parent_path = cfg.get(SECTION, 'folder_path').strip().rstrip('/') or '/Pibooth'
|
|
88
|
+
if not parent_path.startswith('/'):
|
|
89
|
+
parent_path = '/' + parent_path
|
|
90
|
+
album_name = cfg.get(SECTION, 'album_name').strip() or 'Pibooth'
|
|
91
|
+
album_path = f"{parent_path}/{album_name}"
|
|
92
|
+
|
|
93
|
+
app.pcloud = PCloudUpload(email, password, api_host)
|
|
94
|
+
app.pcloud.qr_position = cfg.get(SECTION, 'qr_position')
|
|
95
|
+
app.pcloud.qr_size = cfg.getint(SECTION, 'qr_size')
|
|
96
|
+
app.pcloud.qr_margin = cfg.getint(SECTION, 'qr_margin')
|
|
97
|
+
app.pcloud.local_rep = cfg.get('GENERAL', 'directory')
|
|
98
|
+
|
|
99
|
+
if not app.pcloud.check_credentials():
|
|
100
|
+
LOGGER.error("pCloud authentication failed, plugin disabled")
|
|
101
|
+
app.pcloud = None
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if app.pcloud.ensure_folder(parent_path) is None:
|
|
105
|
+
LOGGER.error("Cannot create pCloud folder '%s', plugin disabled", parent_path)
|
|
106
|
+
app.pcloud = None
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
album_id = app.pcloud.ensure_folder(album_path)
|
|
110
|
+
if album_id is None:
|
|
111
|
+
LOGGER.error("Cannot create pCloud folder '%s', plugin disabled", album_path)
|
|
112
|
+
app.pcloud = None
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
app.pcloud.folder_path = album_path
|
|
116
|
+
app.pcloud.folder_id = album_id
|
|
117
|
+
|
|
118
|
+
gallery_link = app.pcloud.get_folder_public_link(album_id)
|
|
119
|
+
if not gallery_link:
|
|
120
|
+
LOGGER.warning("Could not create public link, QR code will show folder path")
|
|
121
|
+
gallery_link = album_path
|
|
122
|
+
|
|
123
|
+
app.pcloud.gallery_link = gallery_link
|
|
124
|
+
|
|
125
|
+
# Generate QR code
|
|
126
|
+
qr_box_size = max(3, min(10, app.pcloud.qr_size))
|
|
127
|
+
qr = qrcode.QRCode(
|
|
128
|
+
version=1,
|
|
129
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
130
|
+
box_size=qr_box_size,
|
|
131
|
+
border=2,
|
|
132
|
+
)
|
|
133
|
+
qr.add_data(gallery_link)
|
|
134
|
+
qr.make(fit=True)
|
|
135
|
+
image = qr.make_image(fill_color="black", back_color="white").convert('RGB')
|
|
136
|
+
image.save(os.path.join(app.pcloud.local_rep, 'QRCODE.png'), "PNG")
|
|
137
|
+
app.pcloud.qr_image = pygame.image.fromstring(
|
|
138
|
+
image.tobytes(), image.size, image.mode
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
LOGGER.info("pCloud plugin ready (region=%s, folder=%s)", region, album_path)
|
|
142
|
+
|
|
143
|
+
# Catch up anything left over from previous sessions (e.g. offline uploads).
|
|
144
|
+
_sync_in_background(app)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pibooth.hookimpl
|
|
148
|
+
def state_wait_enter(cfg, app, win):
|
|
149
|
+
"""Display QR code on the wait screen."""
|
|
150
|
+
if not getattr(app, 'pcloud', None):
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
win_rect = win.get_rect()
|
|
154
|
+
qr_rect = app.pcloud.qr_image.get_rect()
|
|
155
|
+
margin = app.pcloud.qr_margin
|
|
156
|
+
position = app.pcloud.qr_position
|
|
157
|
+
|
|
158
|
+
positions = {
|
|
159
|
+
"top-left": (margin, margin),
|
|
160
|
+
"top-right": (win_rect.width - qr_rect.width - margin, margin),
|
|
161
|
+
"bottom-left": (margin, win_rect.height - qr_rect.height - margin),
|
|
162
|
+
"bottom-right": (win_rect.width - qr_rect.width - margin,
|
|
163
|
+
win_rect.height - qr_rect.height - margin),
|
|
164
|
+
"center": ((win_rect.width - qr_rect.width) // 2,
|
|
165
|
+
(win_rect.height - qr_rect.height) // 2),
|
|
166
|
+
}
|
|
167
|
+
x, y = positions.get(position, (margin, margin))
|
|
168
|
+
win.surface.blit(app.pcloud.qr_image, (x, y))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
PICTURE_PATTERN = "*_pibooth.jpg"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _sync_in_background(app):
|
|
175
|
+
"""Run a catch-up sync in a daemon thread, guarded by the upload lock."""
|
|
176
|
+
def _do_sync():
|
|
177
|
+
if not app.pcloud.upload_lock.acquire(blocking=False):
|
|
178
|
+
LOGGER.debug("pCloud sync already in progress, skipping")
|
|
179
|
+
return
|
|
180
|
+
try:
|
|
181
|
+
app.pcloud.sync_missing(
|
|
182
|
+
app.pcloud.local_rep, PICTURE_PATTERN,
|
|
183
|
+
app.pcloud.folder_path, app.pcloud.folder_id,
|
|
184
|
+
)
|
|
185
|
+
finally:
|
|
186
|
+
app.pcloud.upload_lock.release()
|
|
187
|
+
|
|
188
|
+
threading.Thread(target=_do_sync, daemon=True).start()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pibooth.hookimpl
|
|
192
|
+
def state_processing_exit(app, cfg):
|
|
193
|
+
"""Upload captured photo and catch up any missing ones (non-blocking)."""
|
|
194
|
+
if not getattr(app, 'pcloud', None):
|
|
195
|
+
return
|
|
196
|
+
_sync_in_background(app)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
###########################################################################
|
|
200
|
+
# pCloud API client
|
|
201
|
+
###########################################################################
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PCloudUpload:
|
|
205
|
+
|
|
206
|
+
DIGEST_TTL = 60 # seconds — pCloud digests are short-lived, refresh often
|
|
207
|
+
|
|
208
|
+
def __init__(self, email, password, api_host):
|
|
209
|
+
self.email = email
|
|
210
|
+
self.password = password
|
|
211
|
+
self.api_host = api_host
|
|
212
|
+
self.upload_lock = threading.Lock()
|
|
213
|
+
self._digest_lock = threading.Lock()
|
|
214
|
+
self._username_sha1 = hashlib.sha1(email.lower().encode()).hexdigest()
|
|
215
|
+
self.gallery_link = ""
|
|
216
|
+
self.folder_path = ""
|
|
217
|
+
self.folder_id = 0
|
|
218
|
+
self.qr_image = None
|
|
219
|
+
self.qr_position = "top-left"
|
|
220
|
+
self.qr_size = 5
|
|
221
|
+
self.qr_margin = 10
|
|
222
|
+
self.local_rep = ""
|
|
223
|
+
|
|
224
|
+
def _fresh_auth_params(self):
|
|
225
|
+
"""Fetch a new digest and compute the password digest for a single call."""
|
|
226
|
+
with self._digest_lock:
|
|
227
|
+
resp = requests.get(_api_url(self.api_host, "getdigest"), timeout=30)
|
|
228
|
+
resp.raise_for_status()
|
|
229
|
+
data = resp.json()
|
|
230
|
+
if data.get("result") != 0:
|
|
231
|
+
raise RuntimeError(f"getdigest failed: {data.get('error')}")
|
|
232
|
+
digest = data["digest"]
|
|
233
|
+
password_digest = hashlib.sha1(
|
|
234
|
+
(self.password + self._username_sha1 + digest).encode()
|
|
235
|
+
).hexdigest()
|
|
236
|
+
return {"username": self.email, "digest": digest,
|
|
237
|
+
"passworddigest": password_digest}
|
|
238
|
+
|
|
239
|
+
def _call(self, method, params=None, files=None, timeout=30):
|
|
240
|
+
"""Call a pCloud API method authenticated with a fresh digest."""
|
|
241
|
+
url = _api_url(self.api_host, method)
|
|
242
|
+
params = dict(params or {})
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
params.update(self._fresh_auth_params())
|
|
246
|
+
if files:
|
|
247
|
+
resp = requests.post(url, data=params, files=files, timeout=timeout)
|
|
248
|
+
else:
|
|
249
|
+
resp = requests.get(url, params=params, timeout=timeout)
|
|
250
|
+
resp.raise_for_status()
|
|
251
|
+
data = resp.json()
|
|
252
|
+
if data.get("result") != 0:
|
|
253
|
+
LOGGER.error("pCloud API error %s: %s",
|
|
254
|
+
data.get("result"), data.get("error", "unknown"))
|
|
255
|
+
return None
|
|
256
|
+
return data
|
|
257
|
+
except (requests.RequestException, RuntimeError) as e:
|
|
258
|
+
LOGGER.error("pCloud request failed (%s): %s", method, e)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def check_credentials(self):
|
|
262
|
+
"""Verify the email/password authenticate successfully."""
|
|
263
|
+
data = self._call("userinfo")
|
|
264
|
+
if data:
|
|
265
|
+
LOGGER.info("pCloud authenticated as %s", data.get("email", "unknown"))
|
|
266
|
+
return True
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def ensure_folder(self, path):
|
|
270
|
+
"""Create the folder (and parents) if it does not exist.
|
|
271
|
+
Returns the folder ID or None on error.
|
|
272
|
+
"""
|
|
273
|
+
data = self._call("createfolderifnotexists", {"path": path})
|
|
274
|
+
if data and "metadata" in data:
|
|
275
|
+
folder_id = data["metadata"]["folderid"]
|
|
276
|
+
LOGGER.info("pCloud folder ready: %s (id=%s)", path, folder_id)
|
|
277
|
+
return folder_id
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def list_folder_filenames(self, folder_id):
|
|
281
|
+
"""Return the set of file names present in a remote folder."""
|
|
282
|
+
data = self._call("listfolder", {"folderid": folder_id})
|
|
283
|
+
if not data or "metadata" not in data:
|
|
284
|
+
return None
|
|
285
|
+
return {entry["name"] for entry in data["metadata"].get("contents", [])
|
|
286
|
+
if not entry.get("isfolder")}
|
|
287
|
+
|
|
288
|
+
def sync_missing(self, local_dir, pattern, remote_folder_path, folder_id):
|
|
289
|
+
"""Upload local files matching pattern that are absent on pCloud.
|
|
290
|
+
|
|
291
|
+
Returns the number of files successfully uploaded.
|
|
292
|
+
"""
|
|
293
|
+
if not os.path.isdir(local_dir):
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
remote_names = self.list_folder_filenames(folder_id)
|
|
297
|
+
if remote_names is None:
|
|
298
|
+
LOGGER.warning("pCloud sync: cannot list remote folder, skipping")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
uploaded = 0
|
|
302
|
+
for local_path in sorted(glob.glob(os.path.join(local_dir, pattern))):
|
|
303
|
+
if os.path.basename(local_path) in remote_names:
|
|
304
|
+
continue
|
|
305
|
+
if self.upload_file(local_path, remote_folder_path):
|
|
306
|
+
uploaded += 1
|
|
307
|
+
if uploaded:
|
|
308
|
+
LOGGER.info("pCloud sync: %d missing file(s) uploaded", uploaded)
|
|
309
|
+
return uploaded
|
|
310
|
+
|
|
311
|
+
def get_folder_public_link(self, folder_id):
|
|
312
|
+
"""Get or create a public link for the folder."""
|
|
313
|
+
data = self._call("getfolderpublink", {"folderid": folder_id})
|
|
314
|
+
if data and "link" in data:
|
|
315
|
+
LOGGER.info("pCloud public link: %s", data["link"])
|
|
316
|
+
return data["link"]
|
|
317
|
+
# Try listing existing public links
|
|
318
|
+
data = self._call("listpublinks")
|
|
319
|
+
if data and "publinks" in data:
|
|
320
|
+
for link in data["publinks"]:
|
|
321
|
+
if link.get("folderid") == folder_id:
|
|
322
|
+
return link.get("link", "")
|
|
323
|
+
LOGGER.warning("Could not get public link for folder %s", folder_id)
|
|
324
|
+
return ""
|
|
325
|
+
|
|
326
|
+
def upload_file(self, local_path, remote_folder_path):
|
|
327
|
+
"""Upload a single file to the given pCloud folder."""
|
|
328
|
+
filename = os.path.basename(local_path)
|
|
329
|
+
LOGGER.info("Uploading %s to pCloud:%s", filename, remote_folder_path)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
with open(local_path, 'rb') as f:
|
|
333
|
+
data = self._call(
|
|
334
|
+
"uploadfile",
|
|
335
|
+
params={"path": remote_folder_path, "filename": filename,
|
|
336
|
+
"nopartial": 1, "renameifexists": 1},
|
|
337
|
+
files={"file": (filename, f)},
|
|
338
|
+
timeout=120,
|
|
339
|
+
)
|
|
340
|
+
except OSError as e:
|
|
341
|
+
LOGGER.error("Cannot read file %s: %s", local_path, e)
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
if data:
|
|
345
|
+
LOGGER.info("Photo uploaded to pCloud successfully: %s", filename)
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
LOGGER.error("Upload to pCloud failed for %s", filename)
|
|
349
|
+
return False
|
pibooth_pcloud_token.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Validate pCloud email/password and print the pibooth config block.
|
|
5
|
+
|
|
6
|
+
pibooth-pcloud authenticates each API call with a fresh digest derived from
|
|
7
|
+
email+password — no long-lived token. This helper just confirms the
|
|
8
|
+
credentials work and formats the config block ready to paste into
|
|
9
|
+
~/.config/pibooth/pibooth.cfg.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import getpass
|
|
13
|
+
import hashlib
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
API_HOSTS = {
|
|
18
|
+
"EU": "https://eapi.pcloud.com",
|
|
19
|
+
"US": "https://api.pcloud.com",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
print("=" * 60)
|
|
25
|
+
print(" pibooth-pcloud - Verification des identifiants")
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
print()
|
|
28
|
+
|
|
29
|
+
region = input("Region du compte (EU/US) [EU] : ").strip().upper() or "EU"
|
|
30
|
+
if region not in API_HOSTS:
|
|
31
|
+
print(f"Region invalide : {region}")
|
|
32
|
+
return
|
|
33
|
+
host = API_HOSTS[region]
|
|
34
|
+
|
|
35
|
+
email = input("Email pCloud : ").strip()
|
|
36
|
+
password = getpass.getpass("Mot de passe pCloud : ")
|
|
37
|
+
if not email or not password:
|
|
38
|
+
print("Erreur : email et mot de passe requis.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
digest_resp = requests.get(f"{host}/getdigest", timeout=30).json()
|
|
42
|
+
if digest_resp.get("result") != 0:
|
|
43
|
+
print(f"Erreur getdigest : {digest_resp.get('error')}")
|
|
44
|
+
return
|
|
45
|
+
digest = digest_resp["digest"]
|
|
46
|
+
|
|
47
|
+
username_sha1 = hashlib.sha1(email.lower().encode()).hexdigest()
|
|
48
|
+
password_digest = hashlib.sha1(
|
|
49
|
+
(password + username_sha1 + digest).encode()
|
|
50
|
+
).hexdigest()
|
|
51
|
+
|
|
52
|
+
data = requests.get(f"{host}/userinfo", params={
|
|
53
|
+
"username": email, "digest": digest,
|
|
54
|
+
"passworddigest": password_digest,
|
|
55
|
+
}, timeout=30).json()
|
|
56
|
+
|
|
57
|
+
if data.get("result") != 0:
|
|
58
|
+
print(f"Echec : {data.get('error', 'code ' + str(data.get('result')))}")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
print("=" * 60)
|
|
63
|
+
print(" AUTHENTIFICATION REUSSIE")
|
|
64
|
+
print("=" * 60)
|
|
65
|
+
print()
|
|
66
|
+
print(f" Compte : {data.get('email', email)}")
|
|
67
|
+
print(f" Region : {region}")
|
|
68
|
+
print(f" Plan : {data.get('plan', '?')}")
|
|
69
|
+
print()
|
|
70
|
+
print("A ajouter dans ~/.config/pibooth/pibooth.cfg :")
|
|
71
|
+
print()
|
|
72
|
+
print(" [PCLOUD]")
|
|
73
|
+
print(" activate = True")
|
|
74
|
+
print(f" email = {email}")
|
|
75
|
+
print(f" password = {password}")
|
|
76
|
+
print(f" region = {region}")
|
|
77
|
+
print(" folder_path = /Pibooth")
|
|
78
|
+
print(" qr_position = top-right")
|
|
79
|
+
print()
|
|
80
|
+
print("Puis restreindre les droits :")
|
|
81
|
+
print(" chmod 600 ~/.config/pibooth/pibooth.cfg")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|