1# Copyright (C) 2011-2020 Damon Lynch <damonlynch@gmail.com> 2 3# This file is part of Rapid Photo Downloader. 4# 5# Rapid Photo Downloader is free software: you can redistribute it and/or 6# modify it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Rapid Photo Downloader is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Rapid Photo Downloader. If not, 17# see <http://www.gnu.org/licenses/>. 18 19__author__ = 'Damon Lynch' 20__copyright__ = "Copyright 2011-2020, Damon Lynch" 21 22import os 23import time 24from datetime import datetime 25import uuid 26import logging 27import mimetypes 28from collections import Counter, UserDict 29import locale 30from typing import Optional, List, Tuple, Union, Any 31 32import gi 33 34gi.require_version('GLib', '2.0') 35from gi.repository import GLib 36 37import raphodo.exiftool as exiftool 38from raphodo.constants import ( 39 DownloadStatus, FileType, FileExtension, FileSortPriority, ThumbnailCacheStatus, Downloaded, 40 DeviceTimestampTZ, ThumbnailCacheDiskStatus, ExifSource, 41) 42 43from raphodo.storage import get_uri, CameraDetails 44import raphodo.metadataphoto as metadataphoto 45import raphodo.metadatavideo as metadatavideo 46import raphodo.metadataexiftool as metadataexiftool 47from raphodo.utilities import thousands, make_internationalized_list, datetime_roughly_equal 48from raphodo.problemnotification import Problem, make_href 49import raphodo.fileformats as fileformats 50 51 52def get_sort_priority(extension: FileExtension, file_type: FileType) -> FileSortPriority: 53 """ 54 Classifies the extension by sort priority. 55 56 :param extension: the extension's category 57 :param file_type: whether photo or video 58 :return: priority 59 """ 60 if file_type == FileType.photo: 61 if extension in (FileExtension.raw, FileExtension.jpeg): 62 return FileSortPriority.high 63 else: 64 return FileSortPriority.low 65 else: 66 return FileSortPriority.high 67 68 69def get_rpdfile(name: str, 70 path: str, 71 size: int, 72 prev_full_name: Optional[str], 73 prev_datetime: Optional[datetime], 74 device_timestamp_type: DeviceTimestampTZ, 75 mtime: float, 76 mdatatime: float, 77 thumbnail_cache_status: ThumbnailCacheDiskStatus, 78 thm_full_name: Optional[str], 79 audio_file_full_name: Optional[str], 80 xmp_file_full_name: Optional[str], 81 log_file_full_name: Optional[str], 82 scan_id: bytes, 83 file_type: FileType, 84 from_camera: bool, 85 camera_details: Optional[CameraDetails], 86 camera_memory_card_identifiers: Optional[List[int]], 87 never_read_mdatatime: bool, 88 device_display_name: str, 89 device_uri: str, 90 raw_exif_bytes: Optional[bytes], 91 exif_source: Optional[ExifSource], 92 problem: Optional[Problem]): 93 if file_type == FileType.video: 94 return Video( 95 name=name, 96 path=path, 97 size=size, 98 prev_full_name=prev_full_name, 99 prev_datetime=prev_datetime, 100 device_timestamp_type=device_timestamp_type, 101 mtime=mtime, 102 mdatatime=mdatatime, 103 thumbnail_cache_status=thumbnail_cache_status, 104 thm_full_name=thm_full_name, 105 audio_file_full_name=audio_file_full_name, 106 xmp_file_full_name=xmp_file_full_name, 107 log_file_full_name=log_file_full_name, 108 scan_id=scan_id, 109 from_camera=from_camera, 110 camera_details=camera_details, 111 camera_memory_card_identifiers=camera_memory_card_identifiers, 112 never_read_mdatatime=never_read_mdatatime, 113 device_display_name=device_display_name, 114 device_uri=device_uri, 115 raw_exif_bytes=raw_exif_bytes, 116 problem=problem 117 ) 118 else: 119 return Photo( 120 name=name, 121 path=path, 122 size=size, 123 prev_full_name=prev_full_name, 124 prev_datetime=prev_datetime, 125 device_timestamp_type=device_timestamp_type, 126 mtime=mtime, 127 mdatatime=mdatatime, 128 thumbnail_cache_status=thumbnail_cache_status, 129 thm_full_name=thm_full_name, 130 audio_file_full_name=audio_file_full_name, 131 xmp_file_full_name=xmp_file_full_name, 132 log_file_full_name=log_file_full_name, 133 scan_id=scan_id, 134 from_camera=from_camera, 135 camera_details=camera_details, 136 camera_memory_card_identifiers=camera_memory_card_identifiers, 137 never_read_mdatatime=never_read_mdatatime, 138 device_display_name=device_display_name, 139 device_uri=device_uri, 140 raw_exif_bytes=raw_exif_bytes, 141 exif_source=exif_source, 142 problem=problem 143 ) 144 145 146def file_types_by_number(no_photos: int, no_videos: int) -> str: 147 """ 148 Generate a string show number of photos and videos 149 150 :param no_photos: number of photos 151 :param no_videos: number of videos 152 """ 153 if (no_videos > 0) and (no_photos > 0): 154 v = _('photos and videos') 155 elif (no_videos == 0) and (no_photos == 0): 156 v = _('photos or videos') 157 elif no_videos > 0: 158 if no_videos > 1: 159 v = _('videos') 160 else: 161 v = _('video') 162 else: 163 if no_photos > 1: 164 v = _('photos') 165 else: 166 v = _('photo') 167 return v 168 169 170def make_key(file_t: FileType, path: str) -> str: 171 return '{}:{}'.format(path, file_t.value) 172 173 174class FileSizeSum(UserDict): 175 """ Sum size in bytes of photos and videos """ 176 177 def __missing__(self, key): 178 self[key] = 0 179 return self[key] 180 181 def sum(self, basedir: Optional[str] = None) -> int: 182 if basedir is not None: 183 return self[make_key(FileType.photo, basedir)] + self[make_key(FileType.video, basedir)] 184 else: 185 return self[FileType.photo] + self[FileType.video] 186 187 188class FileTypeCounter(Counter): 189 r""" 190 Track the number of photos and videos in a scan or for some other 191 function, and display the results to the user. 192 193 >>> locale.setlocale(locale.LC_ALL, ('en_US', 'utf-8')) 194 'en_US.UTF-8' 195 >>> f = FileTypeCounter() 196 >>> f.summarize_file_count() 197 ('0 photos or videos', 'photos or videos') 198 >>> f.file_types_present_details() 199 '' 200 >>> f[FileType.photo] += 1 201 >>> f.summarize_file_count() 202 ('1 photo', 'photo') 203 >>> f.file_types_present_details() 204 '1 Photo' 205 >>> f.file_types_present_details(singular_natural=True) 206 'a photo' 207 >>> f[FileType.photo] = 0 208 >>> f[FileType.video] = 1 209 >>> f.file_types_present_details(singular_natural=True) 210 'a video' 211 >>> f[FileType.photo] += 1 212 >>> f.file_types_present_details(singular_natural=True) 213 'a photo and a video' 214 >>> f[FileType.video] += 2 215 >>> f 216 FileTypeCounter({<FileType.video: 2>: 3, <FileType.photo: 1>: 1}) 217 >>> f.file_types_present_details() 218 '1 Photo and 3 Videos' 219 >>> f[FileType.photo] += 5 220 >>> f 221 FileTypeCounter({<FileType.photo: 1>: 6, <FileType.video: 2>: 3}) 222 >>> f.summarize_file_count() 223 ('9 photos and videos', 'photos and videos') 224 >>> f.file_types_present_details() 225 '6 Photos and 3 Videos' 226 >>> f2 = FileTypeCounter({FileType.photo:11, FileType.video: 12}) 227 >>> f2.file_types_present_details() 228 '11 Photos and 12 Videos' 229 """ 230 231 def file_types_present(self) -> str: 232 """ 233 Display the types of files present in the scan 234 :return a string to be displayed to the user that can be used 235 to show if a value refers to photos or videos or both, or just 236 one of each 237 """ 238 239 return file_types_by_number(self[FileType.photo], self[FileType.video]) 240 241 def summarize_file_count(self) -> Tuple[str, str]: 242 """ 243 Summarizes the total number of photos and/or videos that can be 244 downloaded. Displayed in the progress bar at the top of the 245 main application window after a scan is finished. 246 247 :return tuple with (1) number of files, e.g. 248 "433 photos and videos" or "23 videos". and (2) file types 249 present e.g. "photos and videos" 250 """ 251 file_types_present = self.file_types_present() 252 # Translators: %(variable)s represents Python code, not a plural of the term 253 # variable. You must keep the %(variable)s untranslated, or the program will 254 # crash. 255 file_count_summary = _("%(number)s %(filetypes)s") % dict( 256 number=thousands(self[FileType.photo] + self[FileType.video]), 257 filetypes=file_types_present 258 ) 259 return file_count_summary, file_types_present 260 261 def file_types_present_details(self, title_case=True, singular_natural=False) -> str: 262 """ 263 Displays details about how many files are selected or ready to be downloaded. 264 265 :param title_case: whether the details should use title case or not. 266 :param singular_natural: if True, instead of '1 photo', return 'A photo'. If True, 267 title_case parameter is treated as always False. 268 :return: 269 """ 270 271 p = self[FileType.photo] 272 v = self[FileType.video] 273 274 if v > 1: 275 # Translators: %(variable)s represents Python code, not a plural of the term 276 # variable. You must keep the %(variable)s untranslated, or the program will 277 # crash. 278 videos = _('%(no_videos)s Videos') % dict(no_videos=thousands(v)) 279 elif v == 1: 280 if singular_natural: 281 # translators: natural language expression signifying a single video 282 videos = _('a video') 283 else: 284 videos = _('1 Video') 285 286 if p > 1: 287 # Translators: %(variable)s represents Python code, not a plural of the term 288 # variable. You must keep the %(variable)s untranslated, or the program will 289 # crash. 290 photos = _('%(no_photos)s Photos') % dict(no_photos=thousands(p)) 291 elif p == 1: 292 if singular_natural: 293 # translators: natural language expression signifying a single photo 294 photos = _('a photo') 295 else: 296 photos = _('1 Photo') 297 298 if (p > 0) and (v > 0): 299 s = make_internationalized_list([photos, videos]) 300 elif (p == 0) and (v == 0): 301 return '' 302 elif v > 0: 303 s = videos 304 else: 305 s = photos 306 307 if title_case or singular_natural: 308 return s 309 else: 310 return s.lower() 311 312 313class RPDFile: 314 """ 315 Base class for photo or video file, with metadata 316 """ 317 318 title = '' 319 title_capitalized = '' 320 321 def __init__(self, name: str, 322 path: str, 323 size: int, 324 prev_full_name: Optional[str], 325 prev_datetime: Optional[datetime], 326 device_timestamp_type: DeviceTimestampTZ, 327 mtime: float, 328 mdatatime: float, 329 thumbnail_cache_status: ThumbnailCacheDiskStatus, 330 thm_full_name: Optional[str], 331 audio_file_full_name: Optional[str], 332 xmp_file_full_name: Optional[str], 333 log_file_full_name: Optional[str], 334 scan_id: bytes, 335 from_camera: bool, 336 never_read_mdatatime: bool, 337 device_display_name: str, 338 device_uri: str, 339 camera_details: Optional[CameraDetails] = None, 340 camera_memory_card_identifiers: Optional[List[int]] = None, 341 raw_exif_bytes: Optional[bytes] = None, 342 exif_source: Optional[ExifSource] = None, 343 problem: Optional[Problem] = None) -> None: 344 """ 345 346 :param name: filename, including the extension, without its path 347 :param path: path of the file 348 :param size: file size 349 :param device_timestamp_type: the method with which the device 350 records timestamps. 351 :param mtime: file modification time 352 :param mdatatime: file time recorded in metadata 353 :param thumbnail_cache_status: whether there is an entry in the thumbnail 354 cache or not 355 :param prev_full_name: the name and path the file was 356 previously downloaded with, else None 357 :param prev_datetime: when the file was previously downloaded, 358 else None 359 :param thm_full_name: name and path of and associated thumbnail 360 file 361 :param audio_file_full_name: name and path of any associated 362 audio file 363 :param xmp_file_full_name: name and path of any associated XMP 364 file 365 :param log_file_full_name: name and path of any associated LOG 366 file 367 :param scan_id: id of the scan 368 :param from_camera: whether the file is being downloaded from a 369 camera 370 :param never_read_mdatatime: whether to ignore the metadata 371 date time when determining a photo or video's creation time, 372 and rely only on the file modification time 373 :param device_display_name: display name of the device the file was found on 374 :param device_uri: the uri of the device the file was found on 375 :param camera_details: details about the camera, such as model name, 376 port, etc. 377 :param camera_memory_card_identifiers: if downloaded from a 378 camera, and the camera has more than one memory card, a list 379 of numeric identifiers (i.e. 1 or 2) identifying which memory 380 card the file came from 381 :param raw_exif_bytes: excerpt of the file's metadata in bytes format 382 :param exif_source: source of photo metadata 383 :param problem: any problems encountered 384 """ 385 386 self.from_camera = from_camera 387 self.camera_details = camera_details 388 389 self.device_display_name = device_display_name 390 self.device_uri = device_uri 391 392 if camera_details is not None: 393 self.camera_model = camera_details.model 394 self.camera_port = camera_details.port 395 self.camera_display_name = camera_details.display_name 396 self.is_mtp_device = camera_details.is_mtp == True 397 self.camera_storage_descriptions = camera_details.storage_desc 398 else: 399 self.camera_model = self.camera_port = self.camera_display_name = None 400 self.camera_storage_descriptions = None 401 self.is_mtp_device = False 402 403 self.path = path 404 405 self.name = name 406 407 self.prev_full_name = prev_full_name 408 self.prev_datetime = prev_datetime 409 self.previously_downloaded = prev_full_name is not None 410 411 self.full_file_name = os.path.join(path, name) 412 413 # Used in sample RPD files 414 self.raw_exif_bytes = raw_exif_bytes 415 self.exif_source = exif_source 416 417 # Indicate whether file is a photo or video 418 self._assign_file_type() 419 420 # Remove the period from the extension and make it lower case 421 self.extension = fileformats.extract_extension(name) 422 # Classify file based on its type e.g. jpeg, raw or tiff etc. 423 self.extension_type = fileformats.extension_type(self.extension) 424 425 self.mime_type = mimetypes.guess_type(name)[0] 426 427 assert size > 0 428 self.size = size 429 430 # Cached version of call to metadata.date_time() 431 self._datetime = None # type: Optional[datetime] 432 433 ############################ 434 # self._no_datetime_metadata 435 ############################ 436 # If True, tried to read the date time metadata, and failed 437 # If None, haven't tried yet 438 # If False, no problems encountered, got it (or it was assigned from mtime 439 # when never_read_mdatatime is True) 440 self._no_datetime_metadata = None # type: Optional[bool] 441 442 self.never_read_mdatatime = never_read_mdatatime 443 if never_read_mdatatime: 444 assert self.extension == 'dng' 445 446 self.device_timestamp_type = device_timestamp_type 447 448 ########### 449 # self.ctime 450 ########### 451 # 452 # self.ctime is the photo or video's creation time. It's value depends 453 # on the values in self.modification_time and self.mdatatime. It's value 454 # is set by the setter functions below. 455 # 456 # Ideally the file's metadata contains the date/time that the file 457 # was created. However the metadata may not have been read yet (it's a slow 458 # operation), or it may not exist or be invalid. In that case, need to rely on 459 # the file modification time as a proxy, as reported by the file system or device. 460 # 461 # However that can also be misleading. On my Canon DSLR, for instance, if I'm in the 462 # timezone UTC + 5, and I take a photo at 5pm, then the time stamp on the memory card 463 # shows the photo being taken at 10pm when I look at it on the computer. The timestamp 464 # written to the memory card should with this camera be read as 465 # datetime.utcfromtimestamp(mtime), which would return a time zone naive value of 5pm. 466 # In other words, the timestamp on the memory card is written as if it were always in 467 # UTC, regardless of which timezone the photo was taken in. 468 # 469 # Yet this is not the case with a cellphone, where the file modification time knows 470 # nothing about UTC and just saves it as a naive local time. 471 472 self.mdatatime_caused_ctime_change = False 473 474 # file modification time 475 self.modification_time = mtime 476 # date time recorded in metadata 477 if never_read_mdatatime: 478 self.mdatatime = mtime 479 else: 480 self.mdatatime = mdatatime 481 self.mdatatime_caused_ctime_change = False 482 483 # If a camera has more than one memory card, store a simple numeric 484 # identifier to indicate which memory card it came from 485 self.camera_memory_card_identifiers = camera_memory_card_identifiers 486 487 # full path and name of thumbnail file that is associated with some 488 # videos 489 self.thm_full_name = thm_full_name 490 491 # full path and name of audio file that is associated with some photos 492 # and maybe one day videos, e.g. found with the Canon 1D series of 493 # cameras 494 self.audio_file_full_name = audio_file_full_name 495 496 self.xmp_file_full_name = xmp_file_full_name 497 # log files: see https://wiki.magiclantern.fm/userguide#movie_logging 498 self.log_file_full_name = log_file_full_name 499 500 self.status = DownloadStatus.not_downloaded 501 self.problem = problem 502 503 self.scan_id = int(scan_id) 504 self.uid = uuid.uuid4().bytes 505 506 self.job_code = None 507 508 # freedesktop.org cache thumbnails 509 # http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html 510 self.thumbnail_status = ThumbnailCacheStatus.not_ready # type: ThumbnailCacheStatus 511 self.fdo_thumbnail_128_name = '' 512 self.fdo_thumbnail_256_name = '' 513 # PNG data > 128x128 <= 256x256 514 self.fdo_thumbnail_256 = None # type: Optional[bytes] 515 516 # Thee status of the file in the Rapid Photo Downloader thumbnail cache 517 self.thumbnail_cache_status = thumbnail_cache_status 518 519 # generated values 520 521 self.cache_full_file_name = '' 522 # temporary file used only for video metadata extraction: 523 self.temp_sample_full_file_name = None # type: Optional[str] 524 # if True, the file is a complete copy of the original 525 self.temp_sample_is_complete_file = False 526 self.temp_full_file_name = '' 527 self.temp_thm_full_name = '' 528 self.temp_audio_full_name = '' 529 self.temp_xmp_full_name = '' 530 self.temp_log_full_name = '' 531 self.temp_cache_full_file_chunk = '' 532 533 self.download_start_time = None 534 535 self.download_folder = '' 536 self.download_subfolder = '' 537 self.download_path = '' # os.path.join(download_folder, download_subfolder) 538 self.download_name = '' 539 self.download_full_file_name = '' # filename with path 540 self.download_full_base_name = '' # filename with path but no extension 541 self.download_thm_full_name = '' # name of THM (thumbnail) file with path 542 self.download_xmp_full_name = '' # name of XMP sidecar with path 543 self.download_log_full_name = '' # name of LOG associate file with path 544 self.download_audio_full_name = '' # name of the WAV or MP3 audio file with path 545 546 self.thm_extension = '' 547 self.audio_extension = '' 548 self.xmp_extension = '' 549 self.log_extension = '' 550 551 self.metadata = None # type: Optional[Union[metadataphoto.MetaData, metadatavideo.MetaData, metadataexiftool.MetadataExiftool]] 552 self.metadata_failure = False # type: bool 553 554 # User preference values used for name generation 555 self.subfolder_pref_list = [] # type: List[str] 556 self.name_pref_list = [] # type: List[str] 557 self.generate_extension_case = '' # type: str 558 559 self.modified_via_daemon_process = False 560 561 # If true, there was a name generation problem 562 self.name_generation_problem = False 563 564 def should_write_fdo(self) -> bool: 565 """ 566 :return: True if a FDO thumbnail should be written for this file 567 """ 568 return (self.thumbnail_status != ThumbnailCacheStatus.generation_failed and 569 (self.is_raw() or self.is_tiff())) 570 571 @property 572 def modification_time(self) -> float: 573 return self._mtime 574 575 @modification_time.setter 576 def modification_time(self, value: Union[float, int]) -> None: 577 """ 578 See notes on self.ctime above 579 """ 580 581 if not isinstance(value, float): 582 value = float(value) 583 if self.device_timestamp_type == DeviceTimestampTZ.is_utc: 584 self._mtime = datetime.utcfromtimestamp(value).timestamp() 585 else: 586 self._mtime = value 587 self._raw_mtime = value 588 589 if not hasattr(self, '_mdatatime'): 590 self.ctime = self._mtime 591 592 @property 593 def mdatatime(self) -> float: 594 return self._mdatatime 595 596 @mdatatime.setter 597 def mdatatime(self, value: float) -> None: 598 599 # Do not allow the value to be set to anything other than the modification time 600 # if we are instructed to never read the metadata date time 601 if self.never_read_mdatatime: 602 value = self._mtime 603 604 self._mdatatime = value 605 606 # Only set the creation time if there is a value to set 607 if value: 608 self.mdatatime_caused_ctime_change = not datetime_roughly_equal(self.ctime, value) 609 self.ctime = value 610 if not self._datetime: 611 self._datetime = datetime.fromtimestamp(value) 612 self._no_datetime_metadata = False 613 614 def ctime_mtime_differ(self) -> bool: 615 """ 616 :return: True if the creation time and file system date 617 modified time are not roughly the same. If the creation 618 date is unknown (zero), the result will be False. 619 """ 620 621 if not self._mdatatime: 622 return False 623 624 return not datetime_roughly_equal(self._mdatatime, self._mtime) 625 626 def date_time(self, missing: Optional[Any] = None) -> datetime: 627 """ 628 Returns the date time as found in the file's metadata, and caches it 629 for later use. 630 631 Will return the file's modification time if self.never_read_mdatatime 632 is True. 633 634 Expects the metadata to have already been loaded. 635 636 :return: the metadata's date time value, else missing if not found or error 637 """ 638 639 if self.never_read_mdatatime: 640 # the value must have been set during the scan stage 641 assert self._mdatatime == self._mtime 642 return self._datetime 643 644 if self._no_datetime_metadata: 645 return missing 646 if self._no_datetime_metadata is not None: 647 return self._datetime 648 649 # Have not yet tried to access the datetime metadata 650 self._datetime = self.metadata.date_time(missing=None) 651 self._no_datetime_metadata = self._datetime is None 652 653 if self._no_datetime_metadata: 654 return missing 655 656 self.mdatatime = self._datetime.timestamp() 657 return self._datetime 658 659 def timestamp(self, missing: Optional[Any] = None) -> float: 660 """ 661 Returns the time stamp as found in the file's metadata, and 662 caches it for later use. 663 664 Will return the file's modification time if self.never_read_mdatatime 665 is True. 666 667 Expects the metadata to have already been loaded. 668 669 :return: the metadata's date time value, else missing if not found or error 670 """ 671 672 dt = self.date_time(missing=missing) 673 if self._no_datetime_metadata: 674 return missing 675 676 return dt.timestamp() 677 678 def is_jpeg(self) -> bool: 679 """ 680 Uses guess from mimetypes module 681 :return:True if the image is a jpeg image 682 """ 683 return self.mime_type == 'image/jpeg' 684 685 def is_jpeg_type(self) -> bool: 686 """ 687 :return:True if the image is a jpeg or MPO image 688 """ 689 return self.mime_type == 'image/jpeg' or self.extension == 'mpo' 690 691 def is_loadable(self) -> bool: 692 """ 693 :return: True if the image can be loaded directly using Qt 694 """ 695 return self.mime_type in ['image/jpeg', 'image/tiff'] 696 697 def is_raw(self) -> bool: 698 """ 699 Inspects file extenstion to determine if a RAW file. 700 701 :return: True if the image is a RAW file 702 """ 703 return self.extension in fileformats.RAW_EXTENSIONS 704 705 def is_heif(self) -> bool: 706 """ 707 Inspects file extension to determine if an HEIF / HEIC file 708 :return: 709 """ 710 return self.extension in fileformats.HEIF_EXTENTIONS 711 712 def is_tiff(self) -> bool: 713 """ 714 :return: True if the file is a tiff file 715 """ 716 return self.mime_type == 'image/tiff' 717 718 def has_audio(self) -> bool: 719 """ 720 :return:True if the file has an associated audio file, else False 721 """ 722 return self.audio_file_full_name is not None 723 724 def get_current_full_file_name(self) -> str: 725 """ 726 :return: full file name which depending on download status will be the 727 source file or the destination file 728 """ 729 730 if self.status in Downloaded: 731 return self.download_full_file_name 732 else: 733 return self.full_file_name 734 735 def get_current_sample_full_file_name(self) -> str: 736 """ 737 Sample files can be temporary extracts on the file system, or source 738 or destination files on the file system 739 740 :return: full file name assuming the current file is a sample file. 741 """ 742 743 # take advantage of Python's left to right evaluation: 744 return self.temp_sample_full_file_name or self.get_current_full_file_name() 745 746 def get_current_name(self) -> str: 747 """ 748 :return: file name which depending on download status will be the 749 source file or the destination file 750 """ 751 752 if self.status in Downloaded: 753 return self.download_name 754 else: 755 return self.name 756 757 def get_uri(self, desktop_environment: Optional[bool] = True) -> str: 758 """ 759 Generate and return the URI for the file 760 761 :param desktop_environment: if True, will to generate a URI accepted 762 by Gnome and KDE desktops, which means adjusting the URI if it appears to be an 763 MTP mount. Includes the port too. 764 :return: the URI 765 """ 766 767 if self.status in Downloaded: 768 path = self.download_full_file_name 769 camera_details = None 770 else: 771 path = self.full_file_name 772 camera_details = self.camera_details 773 return get_uri( 774 full_file_name=path, camera_details=camera_details, 775 desktop_environment=desktop_environment 776 ) 777 778 def get_souce_href(self) -> str: 779 return make_href( 780 name=self.name, 781 uri=get_uri( 782 full_file_name=self.full_file_name, camera_details=self.camera_details 783 ) 784 ) 785 786 def get_current_href(self) -> str: 787 return make_href(name=self.get_current_name(), uri=self.get_uri()) 788 789 def get_display_full_name(self) -> str: 790 """ 791 Generate a full name indicating the file source. 792 793 If it's not a camera, it will merely be the full name. 794 If it's a camera, it will include the camera name 795 :return: full name 796 """ 797 798 if self.from_camera: 799 # Translators: %(variable)s represents Python code, not a plural of the term variable. 800 # You must keep the %(variable)s untranslated, or the program will crash. 801 return _('%(path)s on %(camera)s') % dict( 802 path=self.full_file_name, camera=self.camera_display_name 803 ) 804 else: 805 return self.full_file_name 806 807 def _assign_file_type(self): 808 self.file_type = None 809 810 def __repr__(self): 811 return "{}\t{}\t{}".format( 812 self.name, datetime.fromtimestamp(self.modification_time).strftime('%Y-%m-%d %H:%M:%S'), 813 self.get_current_sample_full_file_name() 814 ) 815 816 817class Photo(RPDFile): 818 title = _("photo") 819 title_capitalized = _("Photo") 820 821 def _assign_file_type(self): 822 self.file_type = FileType.photo 823 824 def load_metadata(self, full_file_name: Optional[str] = None, 825 raw_bytes: Optional[bytearray] = None, 826 app1_segment: Optional[bytearray] = None, 827 et_process: exiftool.ExifTool = None, 828 force_exiftool: Optional[bool] = False) -> bool: 829 """ 830 Use GExiv2 or ExifTool to read the photograph's metadata. 831 832 :param full_file_name: full path of file from which file to read 833 the metadata. 834 :param raw_bytes: portion of a non-jpeg file from which the 835 metadata can be extracted 836 :param app1_segment: the app1 segment of a jpeg file, from which 837 the metadata can be read 838 :param et_process: optional daemon ExifTool process 839 :param force_exiftool: whether ExifTool must be used to load the 840 metadata 841 :return: True if successful, False otherwise 842 """ 843 844 if force_exiftool or fileformats.use_exiftool_on_photo( 845 self.extension, preview_extraction_irrelevant=True): 846 847 self.metadata = metadataexiftool.MetadataExiftool( 848 full_file_name=full_file_name, et_process=et_process, file_type=self.file_type 849 ) 850 return True 851 else: 852 try: 853 self.metadata = metadataphoto.MetaData( 854 full_file_name=full_file_name, raw_bytes=raw_bytes, 855 app1_segment=app1_segment, et_process=et_process, 856 ) 857 except GLib.GError as e: 858 logging.warning("Could not read metadata from %s. %s", self.full_file_name, e) 859 self.metadata_failure = True 860 return False 861 except: 862 logging.warning("Could not read metadata from %s", self.full_file_name) 863 self.metadata_failure = True 864 return False 865 else: 866 return True 867 868 869class Video(RPDFile): 870 title = _("video") 871 title_capitalized = _("Video") 872 873 def _assign_file_type(self): 874 self.file_type = FileType.video 875 876 def load_metadata(self, full_file_name: Optional[str] = None, 877 et_process: exiftool.ExifTool = None) -> bool: 878 """ 879 Use ExifTool to read the video's metadata 880 :param full_file_name: full path of file from which file to read 881 the metadata. 882 :param et_process: optional deamon exiftool process 883 :return: Always returns True. Return value is needed to keep 884 consistency with class Photo, where the value actually makes sense. 885 """ 886 if full_file_name is None: 887 if self.download_full_file_name: 888 full_file_name = self.download_full_file_name 889 elif self.cache_full_file_name: 890 full_file_name = self.cache_full_file_name 891 else: 892 full_file_name = self.full_file_name 893 self.metadata = metadatavideo.MetaData(full_file_name, et_process) 894 return True 895 896 897class SamplePhoto(Photo): 898 def __init__(self, sample_name='IMG_1234.CR2', sequences=None): 899 mtime = time.time() 900 super().__init__( 901 name=sample_name, 902 path='/media/EOS_DIGITAL/DCIM/100EOS5D', 903 size=23516764, 904 prev_full_name=None, 905 prev_datetime=None, 906 device_timestamp_type=DeviceTimestampTZ.is_local, 907 mtime=mtime, 908 mdatatime=mtime, 909 thumbnail_cache_status=ThumbnailCacheDiskStatus.not_found, 910 thm_full_name=None, 911 audio_file_full_name=None, 912 xmp_file_full_name=None, 913 log_file_full_name=None, 914 scan_id=b'0', 915 from_camera=False, 916 never_read_mdatatime=False, 917 device_display_name=_('Photos'), 918 device_uri='file:///media/EOS_DIGITAL/' 919 ) 920 self.sequences = sequences 921 self.metadata = metadataphoto.DummyMetaData() 922 self.download_start_time = datetime.now() 923 924 925class SampleVideo(Video): 926 def __init__(self, sample_name='MVI_1234.MOV', sequences=None): 927 mtime = time.time() 928 super().__init__( 929 name=sample_name, 930 path='/media/EOS_DIGITAL/DCIM/100EOS5D', 931 size=823513764, 932 prev_full_name=None, 933 prev_datetime=None, 934 device_timestamp_type=DeviceTimestampTZ.is_local, 935 mtime=mtime, 936 mdatatime=mtime, 937 thumbnail_cache_status=ThumbnailCacheDiskStatus.not_found, 938 thm_full_name=None, 939 audio_file_full_name=None, 940 xmp_file_full_name=None, 941 log_file_full_name=None, 942 scan_id=b'0', 943 from_camera=False, 944 never_read_mdatatime=False, 945 device_display_name=_('Videos'), 946 device_uri='file:///media/EOS_DIGITAL/' 947 ) 948 self.sequences = sequences 949 self.metadata = metadatavideo.DummyMetaData(sample_name, None) 950 self.download_start_time = datetime.now() 951