1#!/usr/bin/env python3 2 3# Copyright (C) 2015-2020 Damon Lynch <damonlynch@gmail.com> 4# Copyright (C) 2012-2015 Jim Easterbrook <jim@jim-easterbrook.me.uk> 5 6# This file is part of Rapid Photo Downloader. 7# 8# Rapid Photo Downloader is free software: you can redistribute it and/or 9# modify it under the terms of the GNU General Public License as published by 10# the Free Software Foundation, either version 3 of the License, or 11# (at your option) any later version. 12# 13# Rapid Photo Downloader is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with Rapid Photo Downloader. If not, 20# see <http://www.gnu.org/licenses/>. 21 22__author__ = 'Damon Lynch' 23__copyright__ = "Copyright 2015-2020, Damon Lynch. Copyright 2012-2015 Jim Easterbrook." 24 25import logging 26import os 27import io 28from collections import namedtuple 29import re 30from typing import Optional, List, Tuple, Union 31 32import gphoto2 as gp 33from raphodo.storage import StorageSpace 34from raphodo.constants import CameraErrorCode 35from raphodo.utilities import format_size_for_user 36 37 38def python_gphoto2_version(): 39 return gp.__version__ 40 41 42def gphoto2_version(): 43 return gp.gp_library_version(0)[0] 44 45 46def gphoto2_python_logging(): 47 """ 48 Version 2.0.0 of gphoto2 introduces a COMPATIBILITY CHANGE: 49 gp_log_add_func and use_python_logging now return a 50 Python object which must be stored until logging is no longer needed. 51 Could just go with the None returned by default from a function that 52 returns nothing, but want to make this explicit. 53 54 :return: either True or a Python object that must be stored until logging 55 is no longer needed 56 """ 57 58 return gp.use_python_logging() or True 59 60 61def autodetect_cameras(context: gp.Context, 62 suppress_errors: bool=True) -> Union[gp.CameraList, List]: 63 """ 64 Do camera auto detection for multiple versions of gphoto2-python 65 66 Version 2.2.0 of gphoto2 introduces a COMPATIBILITY CHANGE: 67 Removed Context.camera_autodetect method. 68 Was quickly reintroduced in 2.2.1, but is due for removal. 69 70 :return: CameraList of model and port 71 """ 72 73 try: 74 return gp.check_result(gp.gp_camera_autodetect(context)) 75 except Exception: 76 if not suppress_errors: 77 raise 78 return [] 79 80 81# convert error codes to error names 82gphoto2_error_codes = { 83 code: name for code, name in ( 84 ((getattr(gp, attr), attr) for attr in dir(gp) if attr.startswith('GP_ERROR')) 85 ) 86} 87 88 89def gphoto2_named_error(code: int) -> str: 90 return gphoto2_error_codes.get(code, 'Unknown gphoto2 error') 91 92 93class CameraError(Exception): 94 def __init__(self, code: CameraErrorCode) -> None: 95 self.code = code 96 97 def __repr__(self) -> str: 98 if self.code == CameraErrorCode.inaccessible: 99 return "inaccessible" 100 else: 101 return "locked" 102 103 def __str__(self) -> str: 104 if self.code == CameraErrorCode.inaccessible: 105 return "The camera is inaccessible" 106 else: 107 return "The camera is locked" 108 109 110class CameraProblemEx(CameraError): 111 def __init__(self, code: CameraErrorCode, 112 gp_exception: Optional[gp.GPhoto2Error]=None, 113 py_exception: Optional[Exception]=None) -> None: 114 super().__init__(code) 115 if gp_exception is not None: 116 self.gp_code = gp_exception.code 117 else: 118 self.gp_code = None 119 self.py_exception = py_exception 120 121 def __repr__(self) -> str: 122 if self.code == CameraErrorCode.read: 123 return "read error" 124 elif self.code == CameraErrorCode.write: 125 return 'write error' 126 else: 127 return repr(super()) 128 129 def __str__(self) -> str: 130 if self.code == CameraErrorCode.read: 131 return "Could not read file from camera" 132 elif self.code == CameraErrorCode.write: 133 return 'Could not write file from camera' 134 else: 135 return str(super()) 136 137 138def generate_devname(camera_port: str) -> Optional[str]: 139 """ 140 Generate udev DEVNAME. 141 142 >>> generate_devname('usb:001,003') 143 '/dev/bus/usb/001/003' 144 145 >>> generate_devname('usb::001,003') 146 147 :param camera_port: 148 :return: devname if it could be generated, else None 149 """ 150 151 match = re.match('usb:([0-9]+),([0-9]+)', camera_port) 152 if match is not None: 153 p1, p2 = match.groups() 154 return '/dev/bus/usb/{}/{}'.format(p1, p2) 155 return None 156 157 158class Camera: 159 160 """Access a camera via libgphoto2.""" 161 162 def __init__(self, model: str, 163 port:str, 164 get_folders: bool=True, 165 raise_errors: bool=False, 166 context: gp.Context=None, 167 specific_folders: Optional[List[str]]=None) -> None: 168 """ 169 Initialize a camera via libgphoto2. 170 171 :param model: camera model, as returned by camera_autodetect() or 172 gp_camera_autodetect() 173 :param port: camera port, as returned by camera_autodetect() 174 :param get_folders: whether to detect the DCIM folders on the 175 camera 176 :param raise_errors: if True, if necessary free camera, 177 and raise error that occurs during initialization 178 :param specific_folders: folders such as DCIM, PRIVATE, 179 and MP_ROOT that are searched for if get_folders is True. 180 If None, the root level folders are returned -- one for each 181 storage slot. 182 """ 183 184 self.model = model 185 self.port = port 186 # class method _concise_model_name discusses why a display name is 187 # needed 188 self.display_name = model 189 self.camera_config = None 190 191 if context is None: 192 self.context = gp.Context() 193 else: 194 self.context = context 195 196 self._select_camera(model, port) 197 198 self.specific_folders = None # type: Optional[List[str]] 199 self.specific_folder_located = False 200 self._dual_slots_active = False 201 202 self.storage_info = [] 203 204 self.camera_initialized = False 205 try: 206 self.camera.init(self.context) 207 self.camera_initialized = True 208 except gp.GPhoto2Error as e: 209 if e.code == gp.GP_ERROR_IO_USB_CLAIM: 210 error_code = CameraErrorCode.inaccessible 211 logging.error("{} is already mounted".format(model)) 212 elif e.code == gp.GP_ERROR: 213 logging.error("An error occurred initializing the camera using libgphoto2") 214 error_code = CameraErrorCode.inaccessible 215 else: 216 logging.error("Unable to access camera: %s", gphoto2_named_error(e.code)) 217 error_code = CameraErrorCode.locked 218 if raise_errors: 219 raise CameraProblemEx(error_code, gp_exception=e) 220 return 221 222 concise_model_name = self._concise_model_name() 223 if concise_model_name: 224 self.display_name = concise_model_name 225 226 if get_folders: 227 try: 228 self.specific_folders = self._locate_specific_folders( 229 path='/', specific_folders=specific_folders 230 ) 231 self.specific_folder_located = len(self.specific_folders) > 0 232 233 logging.debug( 234 "Folders located on %s: %s", self.display_name, 235 ', '.join(', '.join(map(str, sl)) for sl in self.specific_folders) 236 ) 237 except gp.GPhoto2Error as e: 238 logging.error( 239 "Unable to access camera %s: %s. Is it locked?", 240 self.display_name, gphoto2_named_error(e.code) 241 ) 242 if raise_errors: 243 self.free_camera() 244 raise CameraProblemEx(CameraErrorCode.locked, gp_exception=e) 245 246 self.folders_and_files = [] 247 self.audio_files = {} 248 self.video_thumbnails = [] 249 abilities = self.camera.get_abilities() 250 self.can_fetch_thumbnails = abilities.file_operations & gp.GP_FILE_OPERATION_PREVIEW != 0 251 252 def camera_has_folders_to_scan(self) -> bool: 253 """ 254 Check whether the camera has been initialized and if a DCIM or other specific folder 255 has been located 256 257 :return: True if the camera is initialized and a DCIM or other specific folder has 258 been located 259 """ 260 return self.camera_initialized and self.specific_folder_located 261 262 def _locate_specific_folders(self, 263 path: str, 264 specific_folders: Optional[List[str]]) -> List[Optional[List[str]]]: 265 """ 266 Scan camera looking for folders such as DCIM, PRIVATE, and MP_ROOT. 267 268 Looks in either the root of the path passed, or in one of the root 269 folders subfolders (it does not scan subfolders of those subfolders). 270 271 Returns all instances of the specific folders, which is helpful for 272 cameras that have more than one storage (memory card / internal memory) 273 slot. 274 275 No error checking: exceptions must be caught by the caller 276 277 :param path: the root folder to start scanning in 278 :param specific_folders: the subfolders to look for. If None, return the 279 root of each storage device 280 :return: the paths including the specific folders (if found), or empty list 281 """ 282 283 # turn list of two items into a dictionary, for easier access 284 # no error checking as exceptions are caught by the caller 285 folders = dict(self.camera.folder_list_folders(path, self.context)) 286 287 if specific_folders is None: 288 found_folders = [[path + folder] for folder in folders] 289 else: 290 found_folders = [] 291 292 # look for the folders one level down from the root folder 293 # it is at this level that specific folders like DCIM will be found 294 for subfolder in folders: 295 subpath = os.path.join(path, subfolder) 296 subfolders = dict(self.camera.folder_list_folders(subpath, self.context)) 297 ff = [ 298 os.path.join(subpath, folder) for folder in specific_folders 299 if folder in subfolders 300 ] 301 if ff: 302 found_folders.append(ff) 303 304 self._dual_slots_active = len(found_folders) > 1 305 306 return found_folders 307 308 def get_file_info(self, folder, file_name) -> Tuple[int, int]: 309 """ 310 Returns modification time and file size 311 312 :type folder: str 313 :type file_name: str 314 :param folder: full path where file is located 315 :param file_name: 316 :return: tuple of modification time and file size 317 """ 318 info = self.camera.file_get_info(folder, file_name, self.context) 319 modification_time = info.file.mtime 320 size = info.file.size 321 return modification_time, size 322 323 def get_exif_extract(self, folder: str, 324 file_name: str, 325 size_in_bytes: int=200) -> bytearray: 326 """" 327 Attempt to read only the exif portion of the file. 328 329 Assumes exif is located at the beginning of the file. 330 Use the result like this: 331 metadata = GExiv2.Metadata() 332 metadata.open_buf(buf) 333 334 :param folder: directory on the camera the file is stored 335 :param file_name: the photo's file name 336 :param size_in_bytes: how much of the photo to read, starting 337 from the front of the file 338 """ 339 340 buffer = bytearray(size_in_bytes) 341 try: 342 self.camera.file_read( 343 folder, file_name, gp.GP_FILE_TYPE_NORMAL, 0, buffer, self.context 344 ) 345 except gp.GPhoto2Error as e: 346 logging.error( 347 "Unable to extract portion of file from camera %s: %s", 348 self.display_name, gphoto2_named_error(e.code) 349 ) 350 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=e) 351 else: 352 return buffer 353 354 def get_exif_extract_from_jpeg(self, folder: str, file_name: str) -> bytearray: 355 """ 356 Extract strictly the app1 (exif) section of a jpeg. 357 358 Uses libgphoto2 to extract the exif header. 359 360 Assumes jpeg on camera is straight from the camera, i.e. not 361 modified by an exif altering program off the camera. 362 363 :param folder: directory on the camera where the jpeg is stored 364 :param file_name: name of the jpeg 365 :return: first section of jpeg such that it can be read by 366 exiv2 or similar 367 368 """ 369 370 camera_file = self._get_file(folder, file_name, None, gp.GP_FILE_TYPE_EXIF) 371 372 try: 373 exif_data = gp.check_result(gp.gp_file_get_data_and_size(camera_file)) 374 except gp.GPhoto2Error as ex: 375 logging.error( 376 'Error getting exif info for %s from camera %s: %s', 377 os.path.join(folder, file_name), self.display_name, gphoto2_named_error(ex.code) 378 ) 379 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) 380 return bytearray(exif_data) 381 382 def get_exif_extract_from_jpeg_manual_parse(self, folder: str, 383 file_name: str) -> Optional[bytes]: 384 """ 385 Extract exif section of a jpeg. 386 387 I wrote this before I understood that libpghoto2 provides the 388 same functionality! 389 390 Reads first few bytes of jpeg on camera to determine the 391 location and length of the exif header, then reads in the 392 header. 393 394 Assumes jpeg on camera is straight from the camera, i.e. not 395 modified by an exif altering program off the camera. 396 397 :param folder: directory on the camera where the jpeg is stored 398 :param file_name: name of the jpeg 399 :return: first section of jpeg such that it can be read by 400 exiv2 or similar 401 402 """ 403 404 # Step 1: determine the location of APP1 in the jpeg file 405 # See http://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_JPEG_files 406 407 soi_marker_length = 2 408 marker_length = 2 409 exif_header_length = 8 410 read0_size = soi_marker_length + marker_length + exif_header_length 411 412 view = memoryview(bytearray(read0_size)) 413 try: 414 bytes_read = gp.check_result(self.camera.file_read( 415 folder, file_name, gp.GP_FILE_TYPE_NORMAL, 416 0, view, self.context)) 417 except gp.GPhoto2Error as ex: 418 logging.error( 419 'Error reading %s from camera: %s', 420 os.path.join(folder, file_name), gphoto2_named_error(ex.code) 421 ) 422 return None 423 424 jpeg_header = view.tobytes() 425 view.release() 426 427 if jpeg_header[0:2] != b'\xff\xd8': 428 logging.error("%s not a jpeg image: no SOI marker", file_name) 429 return None 430 431 app_marker = jpeg_header[2:4] 432 433 # Step 2: handle presence of APP0 - it's optional 434 if app_marker == b'\xff\xe0': 435 # There is an APP0 before the probable APP1 436 # Don't neeed the content of the APP0 437 app0_data_length = jpeg_header[4] * 256 + jpeg_header[5] 438 # We've already read twelve bytes total, going into the APP1 data. 439 # Now we want to download the rest of the APP1, along with the app0 marker 440 # and the app0 exif header 441 read1_size = app0_data_length + 2 442 app0_view = memoryview(bytearray(read1_size)) 443 try: 444 bytes_read = gp.check_result(self.camera.file_read( 445 folder, file_name, gp.GP_FILE_TYPE_NORMAL, 446 read0_size, app0_view, self.context)) 447 except gp.GPhoto2Error as ex: 448 logging.error( 449 'Error reading %s from camera: %s', 450 os.path.join(folder, file_name), gphoto2_named_error(ex.code) 451 ) 452 app0 = app0_view.tobytes() 453 app0_view.release() 454 app_marker = app0[(exif_header_length + 2) * -1:exif_header_length * -1] 455 exif_header = app0[exif_header_length * -1:] 456 jpeg_header = jpeg_header + app0 457 offset = read0_size + read1_size 458 else: 459 exif_header = jpeg_header[exif_header_length * -1:] 460 offset = read0_size 461 462 # Step 3: process exif header 463 if app_marker != b'\xff\xe1': 464 logging.error("Could not locate APP1 marker in %s", file_name) 465 return None 466 if exif_header[2:6] != b'Exif' or exif_header[6:8] != b'\x00\x00': 467 logging.error("APP1 is malformed in %s", file_name) 468 return None 469 app1_data_length = exif_header[0] * 256 + exif_header[1] 470 471 # Step 4: read APP1 472 view = memoryview(bytearray(app1_data_length)) 473 try: 474 bytes_read = gp.check_result( 475 self.camera.file_read( 476 folder, file_name, gp.GP_FILE_TYPE_NORMAL, offset, view, self.context 477 ) 478 ) 479 except gp.GPhoto2Error as ex: 480 logging.error( 481 'Error reading %s from camera: %s', 482 os.path.join(folder, file_name), gphoto2_named_error(ex.code) 483 ) 484 return None 485 return jpeg_header + view.tobytes() 486 487 def _get_file(self, dir_name: str, 488 file_name: str, 489 dest_full_filename: Optional[str]=None, 490 file_type: int=gp.GP_FILE_TYPE_NORMAL) -> gp.CameraFile: 491 492 try: 493 camera_file = gp.check_result( 494 gp.gp_camera_file_get(self.camera, dir_name, file_name, file_type, self.context) 495 ) 496 except gp.GPhoto2Error as ex: 497 logging.error( 498 'Error reading %s from camera %s: %s', 499 os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code) 500 ) 501 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) 502 503 if dest_full_filename is not None: 504 try: 505 gp.check_result(gp.gp_file_save(camera_file, dest_full_filename)) 506 except gp.GPhoto2Error as ex: 507 logging.error( 508 'Error saving %s from camera %s: %s', 509 os.path.join(dir_name, file_name), self.display_name, 510 gphoto2_named_error(ex.code) 511 ) 512 raise CameraProblemEx(code=CameraErrorCode.write, gp_exception=ex) 513 514 return camera_file 515 516 def save_file(self, dir_name: str, 517 file_name: str, 518 dest_full_filename: str) -> None: 519 """ 520 Save the file from the camera to a local destination. 521 522 :param dir_name: directory on the camera 523 :param file_name: the photo or video 524 :param dest_full_filename: full path including filename where 525 the file will be saved. 526 """ 527 528 self._get_file(dir_name, file_name, dest_full_filename) 529 530 def save_file_chunk(self, dir_name: str, 531 file_name: str, 532 chunk_size_in_bytes: int, 533 dest_full_filename: str, 534 mtime: int=None) -> None: 535 """ 536 Save the file from the camera to a local destination. 537 538 :param dir_name: directory on the camera 539 :param file_name: the photo or video 540 :param chunk_size_in_bytes: how much of the file to read, starting 541 from the front of the file 542 :param dest_full_filename: full path including filename where 543 the file will be saved. 544 :param mtime: if specified, set the file modification time to this value 545 """ 546 547 # get_exif_extract() can raise CameraProblemEx(code=CameraErrorCode.read): 548 buffer = self.get_exif_extract(dir_name, file_name, chunk_size_in_bytes) 549 550 view = memoryview(buffer) 551 dest_file = None 552 try: 553 dest_file = io.open(dest_full_filename, 'wb') 554 src_bytes = view.tobytes() 555 dest_file.write(src_bytes) 556 dest_file.close() 557 if mtime is not None: 558 os.utime(dest_full_filename, times=(mtime, mtime)) 559 except (OSError, PermissionError) as ex: 560 logging.error( 561 'Error saving file %s from camera %s: %s', 562 os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.errno) 563 ) 564 if dest_file is not None: 565 dest_file.close() 566 raise CameraProblemEx(code=CameraErrorCode.write, py_exception=ex) 567 568 def save_file_by_chunks(self, dir_name: str, 569 file_name: str, 570 size: int, 571 dest_full_filename: str, 572 progress_callback, 573 check_for_command, 574 return_file_bytes = False, 575 chunk_size=1048576) -> Optional[bytes]: 576 """ 577 :param dir_name: directory on the camera 578 :param file_name: the photo or video 579 :param size: the size of the file in bytes 580 :param dest_full_filename: full path including filename where 581 the file will be saved 582 :param progress_callback: a function with which to update 583 copy progress 584 :param check_for_command: a function with which to check to see 585 if the execution should pause, resume or stop 586 :param return_file_bytes: if True, return a copy of the file's 587 bytes, else make that part of the return value None 588 :param chunk_size: the size of the chunks to copy. The default 589 is 1MB. 590 :return: True if the file was successfully saved, else False, 591 and the bytes that were copied 592 """ 593 594 src_bytes = None 595 view = memoryview(bytearray(size)) 596 amount_downloaded = 0 597 for offset in range(0, size, chunk_size): 598 check_for_command() 599 stop = min(offset + chunk_size, size) 600 try: 601 bytes_read = gp.check_result( 602 self.camera.file_read( 603 dir_name, file_name, gp.GP_FILE_TYPE_NORMAL, offset, view[offset:stop], 604 self.context 605 ) 606 ) 607 amount_downloaded += bytes_read 608 if progress_callback is not None: 609 progress_callback(amount_downloaded, size) 610 except gp.GPhoto2Error as ex: 611 logging.error( 612 'Error copying file %s from camera %s: %s', 613 os.path.join(dir_name, file_name), self.display_name, 614 gphoto2_named_error(ex.code) 615 ) 616 if progress_callback is not None: 617 progress_callback(size, size) 618 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) 619 620 dest_file = None 621 try: 622 dest_file = io.open(dest_full_filename, 'wb') 623 src_bytes = view.tobytes() 624 dest_file.write(src_bytes) 625 dest_file.close() 626 except (OSError, PermissionError) as ex: 627 logging.error( 628 'Error saving file %s from camera %s. Error %s: %s', 629 os.path.join(dir_name, file_name), self.display_name, ex.errno, ex.strerror 630 ) 631 if dest_file is not None: 632 dest_file.close() 633 raise CameraProblemEx(code=CameraErrorCode.write, py_exception=ex) 634 635 if return_file_bytes: 636 return src_bytes 637 638 def get_thumbnail(self, dir_name: str, 639 file_name: str, 640 ignore_embedded_thumbnail=False, 641 cache_full_filename: Optional[str]=None) -> Optional[bytes]: 642 """ 643 :param dir_name: directory on the camera 644 :param file_name: the photo or video 645 :param ignore_embedded_thumbnail: if True, do not retrieve the 646 embedded thumbnail 647 :param cache_full_filename: full path including filename where the 648 thumbnail will be saved. If none, will not save it. 649 :return: thumbnail in bytes format, which will be full 650 resolution if the embedded thumbnail is not selected 651 """ 652 653 if self.can_fetch_thumbnails and not ignore_embedded_thumbnail: 654 get_file_type = gp.GP_FILE_TYPE_PREVIEW 655 else: 656 get_file_type = gp.GP_FILE_TYPE_NORMAL 657 658 camera_file = self._get_file( 659 dir_name, file_name, cache_full_filename, get_file_type 660 ) 661 662 try: 663 thumbnail_data = gp.check_result(gp.gp_file_get_data_and_size(camera_file)) 664 except gp.GPhoto2Error as ex: 665 logging.error( 666 'Error getting image %s from camera %s: %s', 667 os.path.join(dir_name, file_name), self.display_name, 668 gphoto2_named_error(ex.code) 669 ) 670 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) 671 672 if thumbnail_data: 673 data = memoryview(thumbnail_data) 674 return data.tobytes() 675 676 def get_THM_file(self, full_THM_name: str) -> Optional[bytes]: 677 """ 678 Get THM thumbnail from camera 679 680 :param full_THM_name: path and file name of the THM file 681 :return: THM in raw bytes 682 """ 683 dir_name, file_name = os.path.split(full_THM_name) 684 camera_file = self._get_file(dir_name, file_name) 685 try: 686 thumbnail_data = gp.check_result(gp.gp_file_get_data_and_size(camera_file)) 687 except gp.GPhoto2Error as ex: 688 logging.error( 689 'Error getting THM file %s from camera %s: %s', 690 os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code) 691 ) 692 raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) 693 694 if thumbnail_data: 695 data = memoryview(thumbnail_data) 696 return data.tobytes() 697 698 def _select_camera(self, model, port_name) -> None: 699 # Code from Jim Easterbrook's Photoini 700 # initialise camera 701 self.camera = gp.Camera() 702 # search abilities for camera model 703 abilities_list = gp.CameraAbilitiesList() 704 abilities_list.load(self.context) 705 idx = abilities_list.lookup_model(str(model)) 706 self.camera.set_abilities(abilities_list[idx]) 707 # search ports for camera port name 708 port_info_list = gp.PortInfoList() 709 port_info_list.load() 710 idx = port_info_list.lookup_path(str(port_name)) 711 self.camera.set_port_info(port_info_list[idx]) 712 713 def free_camera(self) -> None: 714 """ 715 Disconnects the camera in gphoto2. 716 """ 717 if self.camera_initialized: 718 self.camera.exit(self.context) 719 self.camera_initialized = False 720 721 def _concise_model_name(self) -> str: 722 """ 723 Workaround the fact that the standard model name generated by 724 gphoto2 can be extremely verbose, e.g. 725 "Google Inc (for LG Electronics/Samsung) Nexus 4/5/7/10 (MTP)", 726 which is what is generated for a Nexus 4!! 727 :return: the model name as detected by gphoto2's camera 728 information, e.g. in the case above, a Nexus 4. Empty string 729 if not found. 730 """ 731 if self.camera_config is None: 732 try: 733 self.camera_config = self.camera.get_config(self.context) 734 except gp.GPhoto2Error as e: 735 if e.code == gp.GP_ERROR_NOT_SUPPORTED: 736 logging.error( 737 "Getting camera configuration not supported for %s", 738 self.display_name 739 ) 740 else: 741 logging.error( 742 "Unknown error getting camera configuration for %s", 743 self.display_name 744 ) 745 return '' 746 747 # Here we really see the difference between C and python! 748 child_count = self.camera_config.count_children() 749 for i in range(child_count): 750 child1 = self.camera_config.get_child(i) 751 child_type = child1.get_type() 752 if child1.get_name() == 'status' and child_type == gp.GP_WIDGET_SECTION: 753 child1_count = child1.count_children() 754 for j in range(child1_count): 755 child2 = child1.get_child(j) 756 if child2.get_name() == 'cameramodel': 757 return child2.get_value() 758 return '' 759 760 def get_storage_media_capacity(self, refresh: bool=False) -> List[StorageSpace]: 761 """ 762 Determine the bytes free and bytes total (media capacity) 763 :param refresh: if True, get updated instead of cached values 764 :return: list of StorageSpace tuple. If could not be 765 determined due to an error, return value is None. 766 """ 767 768 self._get_storage_info(refresh) 769 storage_capacity = [] 770 for media_index in range(len(self.storage_info)): 771 info = self.storage_info[media_index] 772 if not (info.fields & gp.GP_STORAGEINFO_MAXCAPACITY and 773 info.fields & gp.GP_STORAGEINFO_FREESPACEKBYTES): 774 logging.error('Could not locate storage on %s', self.display_name) 775 else: 776 storage_capacity.append( 777 StorageSpace( 778 bytes_free=info.freekbytes * 1024, 779 bytes_total=info.capacitykbytes * 1024, 780 path=info.basedir 781 ) 782 ) 783 return storage_capacity 784 785 def get_storage_descriptions(self, refresh: bool=False) -> List[str]: 786 """ 787 Storage description is used in MTP path names by gvfs and KDE. 788 789 :param refresh: if True, get updated instead of cached values 790 :return: the storage description 791 """ 792 self._get_storage_info(refresh) 793 descriptions = [] 794 for media_index in range(len(self.storage_info)): 795 info = self.storage_info[media_index] 796 if info.fields & gp.GP_STORAGEINFO_DESCRIPTION: 797 descriptions.append(info.description) 798 return descriptions 799 800 def no_storage_media(self, refresh: bool=False) -> int: 801 """ 802 Return the number of storage media (e.g. memory cards) the 803 camera has 804 :param refresh: if True, refresh the storage information 805 :return: the number of media 806 """ 807 self._get_storage_info(refresh) 808 return len(self.storage_info) 809 810 def _get_storage_info(self, refresh: bool): 811 """ 812 Load the gphoto2 storage information 813 :param refresh: if True, refresh the storage information, i.e. 814 load it 815 """ 816 if not self.storage_info or refresh: 817 try: 818 self.storage_info = self.camera.get_storageinfo(self.context) 819 except gp.GPhoto2Error as e: 820 logging.error( 821 "Unable to determine storage info for camera %s: %s", 822 self.display_name, gphoto2_named_error(e.code) 823 ) 824 self.storage_info = [] 825 826 @property 827 def dual_slots_active(self) -> bool: 828 """ 829 :return: True if the camera has dual storage slots and both have specific 830 folders (e.g. DCIM etc.) 831 """ 832 833 if self.specific_folders is None: 834 logging.warning( 835 "dual_slots_active() called before camera's folders scanned for %s", 836 self.display_name 837 ) 838 return False 839 if not self.specific_folder_located: 840 logging.warning( 841 "dual_slots_active() called when no specific folders found for %s", 842 self.display_name 843 ) 844 return False 845 return self.no_storage_media() > 1 and self._dual_slots_active 846 847 def unlocked(self) -> bool: 848 """ 849 Smart phones can be in a locked state, such that their 850 contents cannot be accessed by gphoto2. Determine if 851 the device is unlocked by attempting to locate its 852 folders. 853 :return: True if unlocked, else False 854 """ 855 try: 856 self.camera.folder_list_folders('/', self.context) 857 except gp.GPhoto2Error as e: 858 logging.error( 859 "Unable to access camera %s: %s. Is it locked?", 860 self.display_name, gphoto2_named_error(e.code) 861 ) 862 return False 863 else: 864 return True 865 866 867def dump_camera_details() -> None: 868 import itertools 869 context = gp.Context() 870 cameras = autodetect_cameras(context) 871 for model, port in cameras: 872 c = Camera(model=model, port=port, context=context) 873 if not c.camera_initialized: 874 logging.error("Camera %s could not be initialized", model) 875 else: 876 print() 877 print(c.display_name) 878 print('=' * len(c.display_name)) 879 print() 880 if not c.specific_folder_located: 881 print("Speicifc folder was not located") 882 else: 883 print( 884 "Specific folders:", ', '.join( 885 itertools.chain.from_iterable(c.specific_folders) 886 ) 887 ) 888 print("Can fetch thumbnails:", c.can_fetch_thumbnails) 889 890 sc = c.get_storage_media_capacity() 891 if not sc: 892 print("Unable to determine storage media capacity") 893 else: 894 title = 'Storage capacity' 895 print('\n{}\n{}'.format(title, '-' * len(title))) 896 for ss in sc: 897 print( 898 '\nPath: {}\nCapacity: {}\nFree {}'.format( 899 ss.path, 900 format_size_for_user(ss.bytes_total), 901 format_size_for_user(ss.bytes_free) 902 ) 903 ) 904 sd = c.get_storage_descriptions() 905 if not sd: 906 print("Unable to determine storage descriptions") 907 else: 908 title = 'Storage description(s)' 909 print('\n{}\n{}'.format(title, '-' * len(title))) 910 for ss in sd: 911 print('\n{}'.format(ss)) 912 913 c.free_camera() 914 915 916if __name__ == "__main__": 917 print("gphoto2 python: ", python_gphoto2_version()) 918 # logging = gphoto2_python_logging() 919 920 if True: 921 dump_camera_details() 922 923 if True: 924 925 #Test stub 926 gp_context = gp.Context() 927 # Assume gphoto2 version 2.5 or greater 928 cameras = autodetect_cameras(gp_context) 929 for name, value in cameras: 930 camera = name 931 port = value 932 # print(port) 933 c = Camera(model=camera, port=port, specific_folders=['DCIM', 'MISC']) 934 # c = Camera(model=camera, port=port) 935 print(c.no_storage_media(), c.dual_slots_active, c.specific_folders) 936 937 for name, value in c.camera.folder_list_files('/', c.context): 938 print(name, value) 939 940 c.free_camera() 941 942 943 944 945