예제 #1
0
class ImportMedia(object):
    """
    Import new photos: create JPEG, XMP and thumbnail files, add to database
    """

    PROXY_FULLSIZE = 0
    PROXY_THUMBNAIL = 1
    PROXY_WEBSIZED = 2

    # outrageously slow, exec()s ufraw-batch in newer versions
    def _create_proxy_wand(self, source, dest, mode):
        # load image if not yet done
        if not self.image:
            try:
                self.image = Image(filename=source)
            except Exception as e:
                logging.error('cannot read {}: {}'.format(source, e.args[0]))
                raise e

        # copy bitmap, we may need it again
        image = self.image.convert('jpeg')

        # resize
        if mode == self.PROXY_FULLSIZE:
            pass
        elif mode == self.PROXY_THUMBNAIL:
            image.transform(resize=settings.THUMBNAILSIZE)
        elif mode == self.PROXY_WEBSIZED:
            image.transform(resize=settings.WEBSIZE)

        try:
            # Wand does auto-rotate, but doesn't fix EXIF data... m(
            image.strip()
            image.save(filename=dest)
        except (Exception, IOError) as e:
            logging.error('Error: cannot write {}: {}'.format(dest, e.args[0]))
            raise e

    def _create_proxy_rawpy(self, source, dest, mode):
        # maybe Pillow supports this file type directly?
        if not self.image:
            try:
                self.image = Image.open(source)
            except IOError:
                pass
            except Exception as e:
                logging.error('cannot read {}: {}'.format(source, e.args[0]))
                raise e

        # obviously not, try decoding as Raw
        if not self.image:
            try:
                raw = rawpy.imread(source)
                rgb = raw.postprocess(use_camera_wb=True, no_auto_bright=True)
                self.image = Image.fromarray(rgb)
            except Exception as e:
                logging.error('cannot read {}: {}'.format(source, e.args[0]))
                raise e

        image = self.image.copy()
        if mode == self.PROXY_FULLSIZE:
            pass
        elif mode == self.PROXY_THUMBNAIL:
            image.thumbnail(settings.THUMBNAILSIZE)
        elif mode == self.PROXY_WEBSIZED:
            image.thumbnail(settings.WEBSIZE)

        try:
            image.save(dest)
        except Exception as e:
            logging.error('cannot write {}: {}'.format(dest, e.args[0]))

    def create_proxy(self, source, dest, mode):
        logging.info('called for {}, {}, {}'.format(source, dest, mode))
        if os.path.isfile(dest) and not self.force:
            return

        # just link fullsize image if JPEG and orientation is normal
        is_jpeg = os.path.splitext(source)[1].lower() in ('.jpg', '.jpeg')
        orientation = self.exif.get_orientation()
        logging.debug('EXIF Orientation: {}'.format(orientation.value_nick))

        if mode == self.PROXY_FULLSIZE and is_jpeg and orientation == orientation.NORMAL:
            try:
                os.unlink(dest)
            except Exception as e:
                pass

            try:
                os.link(source, dest)
            except Exception as e:
                logging.error('cannot link {} to {}: {}'.format(source, dest, e.args[0]))
                raise e

            return

        if settings.IMAGE_LIB == 'wand':
            self._create_proxy_wand(source, dest, mode)
        elif settings.IMAGE_LIB == 'rawpy':
            self._create_proxy_rawpy(source, dest, mode)
        else:
            logging.error('configuration error: unknown or missing IMAGE_LIB {}'.format(settings.IMAGE_LIB))

    def create_video_proxy(self, source, thumbnail, websized):
        logging.info('called for {}, {}, {}'.format(source, thumbnail, websized))
        if os.path.isfile(thumbnail) and os.path.isfile(websized) and not self.force:
            return


        for keyframe in 10, 1:
            ffmpeg_filter="[0:v] select='eq(pict_type,I)*lte(t,30)' [iframes];" \
              "[iframes] select='gte(n,{})',split=2 [sel1][sel2];" \
              "[sel1] scale=128:-1[tns]; [tns][1:v] overlay=main_w-overlay_w-5:main_h-overlay_h-5 [tn];" \
              "[sel2] scale=768:-1[prxs]; [prxs][2:v]overlay=main_w-overlay_w-10:main_h-overlay_h-10 [proxy]".format(keyframe)

            call([settings.FFMPEG_COMMAND, '-i', source,
              '-i', settings.THUMBNAIL_VIDEO_PLAY_BUTTON,
              '-i', settings.PREVIEW_VIDEO_PLAY_BUTTON,
              '-filter_complex', ffmpeg_filter,
              '-y', '-v', 'quiet',
              '-map', '[tn]',
              '-frames:v', '1',
              thumbnail,
              '-map', '[proxy]',
              '-frames:v', '1',
              websized],
             shell=False)

            if os.path.isfile(thumbnail) and os.path.isfile(websized):
                return


    def write_xmp_sidecar(self, sourcefile):
        destxmppath = os.path.splitext(sourcefile)[0] + '.xmp'
        try:
            # how brain-damaged is this?!
            fd = open(destxmppath, mode='w+')
            fd.write('<?xml version="1.0" encoding="UTF-8"?><x:xmpmeta xmlns:x="adobe:ns:meta/"></x:xmpmeta>')
            fd.close()
            self.exif.save_file(destxmppath)
        except Exception as e:
            logging.error('cannot write {}: {}'.format(destxmppath, e))

        return destxmppath

    def get_timestamp(self, source_file, use_exif=True):
        # first try to get it from EXIF data
        if use_exif:
            timestamp = self.exif.get_tag_string('Exif.Photo.DateTimeOriginal')
            if not timestamp:
                timestamp = self.exif.get_tag_string('Exif.Image.DateTime')
        else:
            timestamp = None

        # try timestamp of file
        if not timestamp:
            st = os.stat(source_file)
            timestruct = tz.time.gmtime(int(st.st_ctime))
            # camera clock not set, eh?
            if timestruct.tm_year < 2000:
                timestruct = tz.time.gmtime()
            return timestruct

        return tz.time.strptime(timestamp, '%Y:%m:%d %H:%M:%S')

    def create_filename(self, source_file, timestruct):
        datestring = tz.time.strftime("%Y%m%d", timestruct)
        mediabasename = os.path.basename(source_file).lower()
        filename = datestring + "-" + mediabasename
        count = 1
        while count < 1000000:
            try:
                MediaFile.objects.get(filename=filename)
            except Exception as e:
                break
            filename = '{}-{:06d}-{}'.format(datestring, count, mediabasename)
            count += 1

        if count == 1000000:
            logging.error('Cannot create unique export filename')
            return None

        return filename

    def update_image_parameters(self, entry):
        try:
            entry.f_number = Fraction(self.exif.get_tag_string('Exif.Photo.FNumber')) * 1.0
        except Exception:
            entry.f_number = 0

        try:
            entry.exposure_time = Fraction(self.exif.get_tag_string('Exif.Photo.ExposureTime')) * 1.0
        except Exception:
            entry.exposure_time = 0

        try:
            entry.gain_value = float(self.exif.get_tag_string('Exif.Photo.ISOSpeedRatings'))
        except Exception:
            try:
                entry.gain_value = float(self.exif.get_tag_string('Exif.Photo.GainControl'))
            except Exception:
                entry.gain_value = 0

        try:
            entry.focal_length = Fraction(self.exif.get_tag_string('Exif.Photo.FocalLength')) * 1.0
        except Exception as e:
            entry.focal_length = 0

    def update_db(self, source_file, sidecar=None, is_supported_media=True):
        entry = None
        media_dir = self.mediadir
        media_file = os.path.basename(source_file)

        try:
            entry = MediaFile.objects.get(media_dir=media_dir, media_file=media_file)
            if not self.force:
                return entry
        except MediaFile.DoesNotExist:
            timestamp = self.get_timestamp(source_file, use_exif=is_supported_media)
            filename = self.create_filename(source_file, timestamp)
            if not filename:
                return None

            catalog, created = Catalog.objects.get_or_create(name=settings.DEFAULT_CATALOG)
            mime_type, created = MimeType.objects.get_or_create(type=self.mimetype)

            try:
                entry = MediaFile(media_dir=media_dir, media_file=media_file)
            except Exception as e:
                logging.error('Cannot get object: {}'.format(e.args[0]))
                raise e

            entry.mime_type = mime_type
            entry.catalog = catalog
            entry.filename = filename
            entry.date = tz.time.strftime('%Y-%m-%dT%H:%M:%SZ', timestamp)

            if is_supported_media:
                rating = self.exif.get_tag_long('Xmp.xmp.Rating')
                if rating:
                    entry.rating = rating
                label = self.exif.get_tag_string('Xmp.xmp.Label')
                if label:
                    entry.label = label

                entry.sidecar_file = sidecar

        if is_supported_media:
            self.update_image_parameters(entry)
        return entry

    def import_image(self, source_file):
        self.image = None
        # prefer XMP "sidecar" files to save us from parsing huge RAW files more than necessary
        source_xmp_file = os.path.splitext(source_file)[0]
        havesourcesidecar = False
        for ext in (".xmp", ".XMP", "Xmp"):
            if os.path.isfile(source_xmp_file + ext):
                source_xmp_file += ext
                try:
                    havesourcesidecar = self.exif.open_path(source_xmp_file)  # returns True on success
                except Exception:
                    pass

                break

        # No sidecar, oh well...
        if not havesourcesidecar:
            self.exif.open_path(source_file)

        (mediareldir, jpegfilename) = os.path.split(os.path.relpath(source_file, settings.SOURCE_DIR))
        jpegfilename = os.path.splitext(jpegfilename)[0] + ".jpg"

        self.status.update(10, 'Writing Proxy')
        try:
            mediadir = settings.WEB_DIR + mediareldir
            jpegfullpath = mediadir + '/' + jpegfilename
            tools.mkdir(mediadir)
            self.create_proxy(source_file, jpegfullpath, self.PROXY_FULLSIZE)
        except Exception as e:
            raise e

        self.status.update(75, 'Writing Thumbnail')
        try:
            tndir = mediadir + "/" + settings.THUMBNAIL_DIR
            tnfullpath = tndir + '/' + jpegfilename
            tools.mkdir(tndir)
            self.create_proxy(source_file, tnfullpath, self.PROXY_THUMBNAIL)
        except Exception as e:
            os.unlink(jpegfullpath)
            raise e

        self.status.update(85, 'Writing Preview')
        try:
            webimgdir = mediadir + "/" + settings.PREVIEW_DIR
            webimgfullpath = webimgdir + '/' + jpegfilename
            tools.mkdir(webimgdir)
            self.create_proxy(source_file, webimgfullpath, self.PROXY_WEBSIZED)
        except Exception as e:
            os.unlink(jpegfullpath)
            os.unlink(tnfullpath)
            raise e

        # we need to write the XMP sidecar file here as we definitely do not want to parse
        # RAW files from the web app and exiv2 isn't capable to construct it's content without
        # a source file. A sidecar file is required for import of the rating and label tags
        # with lightroom, though.
        #
        # ATTN: We'll write it to SOURCE_DIR. The web app should never touch the proxy dirs
        # by itself

        if not havesourcesidecar:
            source_xmp_file = self.write_xmp_sidecar(source_file)

        entry = self.update_db(source_file, sidecar=os.path.basename(source_xmp_file))
        self.status.update(95, 'Writing Database')
        entry.save()
        self.status.update(100, 'Done')

    def import_video(self, source_file):
        if not settings.FFMPEG_COMMAND:
            raise NotImplementedError

        source_thm_file = os.path.splitext(source_file)[0]
        havesourcesidecar = False
        for ext in (".thm", ".THM", "Thm"):
            if os.path.isfile(source_thm_file + ext):
                source_thm_file += ext
                try:
                    havesourcesidecar = self.exif.open_path(source_thm_file)  # returns True on success
                except Exception:
                    pass

                break

        (mediareldir, jpegfilename) = os.path.split(os.path.relpath(source_file, settings.SOURCE_DIR))
        mediadir = settings.WEB_DIR + mediareldir
        (basename, extension) = os.path.splitext(jpegfilename)
        jpegfilename = basename + ".jpg"
        mediafilename = basename + extension.lower()

        self.status.update(10, 'Linking Source File')
        try:
            mediadir = settings.WEB_DIR + mediareldir
            mediafullpath = mediadir + '/' + mediafilename
            tools.mkdir(mediadir)
            tools.link(source_file, mediafullpath)
        except Exception as e:
            raise e

        self.status.update(50, 'Writing Thumbnail and Proxy')
        try:
            tndir = mediadir + "/" + settings.THUMBNAIL_DIR
            tnfullpath = tndir + '/' + jpegfilename
            tools.mkdir(tndir)
        except Exception as e:
            os.unlink(mediafullpath)
            raise e

        try:
            webimgdir = mediadir + "/" + settings.PREVIEW_DIR
            webimgfullpath = webimgdir + '/' + jpegfilename
            tools.mkdir(webimgdir)
        except Exception as e:
            os.unlink(mediafullpath)
            os.unlink(tnfullpath)
            raise e

        self.create_video_proxy(source_file, tnfullpath, webimgfullpath)

        self.status.update(90, 'Writing Sidecar')
        if not havesourcesidecar:
            source_sidecar = self.write_xmp_sidecar(source_file)
            self.exif.open_path(source_sidecar)
            sidecar = os.path.basename(source_sidecar)
        else:
            sidecar = os.path.basename(source_thm_file)

        entry = self.update_db(source_file, sidecar=sidecar)
        self.status.update(95, 'Writing Database')
        entry.save()
        self.status.update(100, 'Done')

    def import_other(self, source_file):
        entry = self.update_db(source_file, is_supported_media=False)
        if entry.mime_type.copy:
            self.status.update(50, 'Writing Database')
            entry.save()
        self.status.update(100, 'Done')

    def do_import(self, source_file):
        self.status = StatusWriter(statusname=settings.PROCESS_STATUS, filename=source_file, text='Start')
        source_file = os.path.abspath(source_file)

        if not source_file.startswith(settings.SOURCE_DIR):
            logging.critical('{} is not below directory {}'.format(source_file, settings.SOURCE_DIR))
            self.status.error('Broken Configuration')
            return

        if not os.path.isfile(source_file):
            logging.critical('{} does not exist or is not a file'.format(source_file))
            self.status.error('Not a File')
            return

        if self.lock:
            self.mediadir = MediaDir.objects.compare_and_lock(self.mediadir, source_file, prefix=settings.SOURCE_DIR,
                                                              name=self.name)
        else:
            self.mediadir = MediaDir.objects.get_or_create_by_full_path(source_file, prefix=settings.SOURCE_DIR)

        self.status.update(5, 'Examining File Type')
        self.mimetype = magic.from_file(filename=source_file, mime=True)
        extension = os.path.splitext(source_file)[1].lower()

        # image, but skip video thumbnail (handled by do_import_video and exporter)
        if self.mimetype.startswith('image/') and extension != '.thm':
            try:
                self.import_image(source_file)
                return
            except Exception as e:
                logging.warning('Importer: ', exc_info=True)
                self.status.update(0, 'Script Error')
                pass

        # video, yay.
        if self.mimetype.startswith('video/'):
            try:
                self.import_video(source_file)
                return
            except Exception as e:
                logging.warning('Importer: ', exc_info=True)
                self.status.update(0, 'Script Error')
                pass

        # sidecar gets handled explicitly by exporter
        if extension == '.xmp':
            self.status.update(100, 'Done')
            return

        self.import_other(source_file)

    def close(self):
        if self.status:
            self.status.close()
            self.status = None
        if self.mediadir and self.lock:
            self.mediadir.unlock()
            self.mediadir = None

    def __del__(self):
        self.close()

    def __init__(self, force=False, lock=True, name=None):
        self.exif = GExiv2.Metadata()
        self.force = force
        self.lock = lock
        self.name = name
        self.mediadir = None
        self.image = None
        self.status = None