Skip to content

Commit

Permalink
merge in a jmathai elodie version with modified stuff from pull reque…
Browse files Browse the repository at this point in the history
  • Loading branch information
sylikc committed Jul 18, 2019
1 parent 86d7244 commit 9c15d42
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 10 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ Date (Timezone) | Version | Comment
07/17/2019 01:20:15 AM (PDT) | 0.1.3 | Merge with slight modifications to variable names for clarity (sylikc) [Pull request #27 "Add "shell" keyword argument to ExifTool initialization"](https://github.com/smarnach/pyexiftool/pull/27) by [Douglas Lassance (douglaslassance) Los Angeles, CA](https://github.com/douglaslassance) on 5/29/2019<br>*On Windows this will allow to run exiftool without showing the DOS shell.*<br>**This might break Linux but I don't know for sure**<br>Alternative source location with only this patch: https://github.com/blurstudio/pyexiftool/tree/shell-option
07/17/2019 01:24:32 AM (PDT) | 0.1.4 | Merge [Pull request #19 "Correct dependency for building an RPM."](https://github.com/smarnach/pyexiftool/pull/19) by [Achim Herwig (Achimh3011) Munich, Germany](https://github.com/Achimh3011) on Aug 25, 2016<br>**I'm not sure if this is entirely necessary, but merging it anyways**
07/17/2019 02:09:40 AM (PDT) | 0.1.5 | Merge [Pull request #15 "handling Errno:11 Resource temporarily unavailable"](https://github.com/smarnach/pyexiftool/pull/15) by [shoyebi](https://github.com/shoyebi) on Jun 12, 2015

07/18/2019 03:40:39 AM (PDT) | 0.1.6 | Merge in the first set of changes by Leo Broska related to [Pull request #5 "add set_tags_batch, set_tags + constructor takes added options"](https://github.com/smarnach/pyexiftool/pull/5) by [halloleo](https://github.com/halloleo) on Aug 1, 2012<br> but this is sourced from [jmathai/elodie's 6114328 Jun 22,2016 commit](https://github.com/jmathai/elodie/blob/6114328f325660287d1998338a6d5e6ba4ccf069/elodie/external/pyexiftool.py)

162 changes: 153 additions & 9 deletions exiftool.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# PyExifTool <http://github.com/smarnach/pyexiftool>
# Copyright 2012 Sven Marnach
# Copyright 2012 Sven Marnach.
# More contributors in the CHANGELOG for the pull requests

# This file is part of PyExifTool.
#
Expand Down Expand Up @@ -61,6 +62,7 @@
import os
import json
import warnings
import logging
import codecs

try: # Py3k compatibility
Expand All @@ -84,6 +86,10 @@
# some cases.
block_size = 4096

# constants related to keywords manipulations
KW_TAGNAME = "IPTC:Keywords"
KW_REPLACE, KW_ADD, KW_REMOVE = range(3)

# This code has been adapted from Lib/os.py in the Python source tree
# (sha1 265e36e277f3)
def _fscodec():
Expand Down Expand Up @@ -113,6 +119,38 @@ def fsencode(filename):
fsencode = _fscodec()
del _fscodec


#string helper
def strip_nl (s):
return ' '.join(s.splitlines())


# Error checking function
# Note: They are quite fragile, beacsue teh just parse the output text from exiftool
def check_ok (result):
"""Evaluates the output from a exiftool write operation (e.g. `set_tags`)
The argument is the result from the execute method.
The result is True or False.
"""
return not result is None and (not "due to errors" in result)

def format_error (result):
"""Evaluates the output from a exiftool write operation (e.g. `set_tags`)
The argument is the result from the execute method.
The result is a human readable one-line string.
"""
if check_ok (result):
return 'exiftool finished probably properly. ("%s")' % strip_nl(result)
else:
if result is None:
return "exiftool operation can't be evaluated: No result given"
else:
return 'exiftool finished with error: "%s"' % strip_nl(result)

class ExifTool(object):
"""Run the `exiftool` command-line tool and communicate to it.
Expand All @@ -121,9 +159,12 @@ class ExifTool(object):
may be slower. If print conversion is enabled, appending ``#`` to a tag
name disables the print conversion for this particular tag.
You can pass the file name of the ``exiftool`` executable as an
argument to the constructor. The default value ``exiftool`` will
only work if the executable is in your ``PATH``.
You can pass two arguments to the constructor:
- ``addedargs`` (list of strings): contains additional paramaters for
the stay-open instance of exiftool
- ``executable`` (string): file name of the ``exiftool`` executable.
The default value ``exiftool`` will only work if the executable
is in your ``PATH``
Most methods of this class are only available after calling
:py:meth:`start()`, which will actually launch the subprocess. To
Expand Down Expand Up @@ -154,15 +195,24 @@ class ExifTool(object):
associated with a running subprocess.
"""

def __init__(self, executable_=None, win_shell=True, print_conversion=False):
def __init__(self, executable_=None, addedargs=None, win_shell=True, print_conversion=False):

self.win_shell = win_shell
self.print_conversion = print_conversion

if executable_ is None:
self.executable = executable
else:
self.executable = executable_
self.running = False

if addedargs is None:
self.addedargs = []
elif type(addedargs) is list:
self.addedargs = addedargs
else:
raise TypeError("addedargs not a list of strings")

def start(self):
"""Start an ``exiftool`` process in batch mode for this instance.
Expand All @@ -176,9 +226,13 @@ def start(self):
warnings.warn("ExifTool already running; doing nothing.")
return

command = [self.executable, "-stay_open", "True", "-@", "-", "-common_args", "-G"]
procargs = [self.executable, "-stay_open", "True", "-@", "-", "-common_args", "-G"]
# may remove this and just have it added to extra args
if not self.print_conversion:
command.append("-n")
procargs.append("-n")

procargs.extend(self.addedargs)
logging.debug(procargs)

with open(os.devnull, "w") as devnull:
startup_info = subprocess.STARTUPINFO()
Expand All @@ -189,7 +243,7 @@ def start(self):
startup_info.dwFlags |= 11

self._process = subprocess.Popen(
command,
procargs,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=devnull, startupinfo=startup_info)
self.running = True
Expand Down Expand Up @@ -238,7 +292,8 @@ def execute(self, *params):
"""
if not self.running:
raise ValueError("ExifTool instance not running.")
self._process.stdin.write(b"\n".join(params + (b"-execute\n",)))
cmd_text = b"\n".join(params + (b"-execute\n",))
self._process.stdin.write(cmd_text.encode("utf-8"))
self._process.stdin.flush()
output = b""
fd = self._process.stdout.fileno()
Expand Down Expand Up @@ -353,3 +408,92 @@ def get_tag(self, tag, filename):
def copy_tags(self, fromFilename, toFilename):
"""Copy all tags from one file to another."""
self.execute("-overwrite_original", "-TagsFromFile", fromFilename, toFilename)


def set_tags_batch(self, tags, filenames):
"""Writes the values of the specified tags for the given files.
The first argument is a dictionary of tags and values. The tag names may
include group names, as usual in the format <group>:<tag>.
The second argument is an iterable of file names.
The format of the return value is the same as for
:py:meth:`execute()`.
It can be passed into `check_ok()` and `format_error()`.
"""
# Explicitly ruling out strings here because passing in a
# string would lead to strange and hard-to-find errors
if isinstance(tags, basestring):
raise TypeError("The argument 'tags' must be dictionary "
"of strings")
if isinstance(filenames, basestring):
raise TypeError("The argument 'filenames' must be "
"an iterable of strings")

params = []
for tag, value in tags.items():
params.append(u'-%s=%s' % (tag, value))

params.extend(filenames)
return self.execute(*params)

def set_tags(self, tags, filename):
"""Writes the values of the specified tags for the given file.
This is a convenience function derived from `set_tags_batch()`.
Only difference is that it takes as last arugemnt only one file name
as a string.
"""
return self.set_tags_batch(tags, [filename])

def set_keywords_batch(self, mode, keywords, filenames):
"""Modifies the keywords tag for the given files.
The first argument is the operation mode:
KW_REPLACE: Replace (i.e. set) the full keywords tag with `keywords`.
KW_ADD: Add `keywords` to the keywords tag.
If a keyword is present, just keep it.
KW_REMOVE: Remove `keywords` from the keywords tag.
If a keyword wasn't present, just leave it.
The second argument is an iterable of key words.
The third argument is an iterable of file names.
The format of the return value is the same as for
:py:meth:`execute()`.
It can be passed into `check_ok()` and `format_error()`.
"""
# Explicitly ruling out strings here because passing in a
# string would lead to strange and hard-to-find errors
if isinstance(keywords, basestring):
raise TypeError("The argument 'keywords' must be "
"an iterable of strings")
if isinstance(filenames, basestring):
raise TypeError("The argument 'filenames' must be "
"an iterable of strings")

params = []

kw_operation = {KW_REPLACE:"-%s=%s",
KW_ADD:"-%s+=%s",
KW_REMOVE:"-%s-=%s"}[mode]

kw_params = [ kw_operation % (KW_TAGNAME, w) for w in keywords ]

params.extend(kw_params)
params.extend(filenames)
logging.debug (params)
return self.execute(*params)

def set_keywords(self, mode, keywords, filename):
"""Modifies the keywords tag for the given file.
This is a convenience function derived from `set_keywords_batch()`.
Only difference is that it takes as last argument only one file name
as a string.
"""
return self.set_keywords_batch(mode, keywords, [filename])

0 comments on commit 9c15d42

Please sign in to comment.