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