diff --git a/README.md b/README.md index a8a853f..d4ab818 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Command line tools for photo importing/renaming/rotating * Media files scan * Time when picture was taken detection (by EXIF, by file name, by file attributes) * Media files moving/copying to configurable hierarchy - * Lossless rotations (via exiftran) + * Lossless rotations (via exiftran or jpegtran) # Photo Importer Server Standalone web server for fast media import for headless computer @@ -18,31 +18,43 @@ Standalone web server for fast media import for headless computer * Storages mount/unmount (via pmount) * The same as photo-importer but without console -## Installation +# Installation ### Requirements: * Python 3.3+ - * Debian based Linux (Other Linux versions not officially supported, but might work) + +### Supported OS: + + * Debian based Linux (other Linux versions not officially supported, but might work) + * Windows 7 and above + * MacOS X and above (with brew installed, only console version) ### Dependencies: - * PyExifTool (pip3 install PyExifTool) - * python3-progressbar - * python3-psutil - * exiftran or jpegtran - * pmount (only for server) - * pypiwin32 (only for windows) + * [PyExifTool](https://pypi.org/project/PyExifTool/) + * [progressbar](https://pypi.org/project/progressbar/) + * [psutil](https://pypi.org/project/psutil/) + * [exiftran](https://linux.die.net/man/1/exiftran) or [jpegtran](https://linux.die.net/man/1/jpegtran) + * [pmount](https://linux.die.net/man/1/pmount) (only for server) + * [pypiwin32](https://pypi.org/project/pypiwin32/) (only for windows) ### Installation Options: +#### Installing via PyPi +```bash +sudo apt install exiftran exiftool pmount pip +sudo pip install photo-importer +``` #### Installing as debian package ```bash debuild -b -sudo dpkg -i ../photo-importer_1.0.1_all.deb +sudo dpkg -i ../photo-importer_1.2.0_all.deb ``` #### Installing via setup.py ```bash +sudo apt install exiftran exiftool pmount pip +sudo pip install PyExifTool progressbar psutil sudo python3 ./setup.py install ``` @@ -56,9 +68,9 @@ https://exiftool.org/ Download and extract jpegtran to photo_importer folder http://sylvana.net/jpegcrop/jpegtran/ -Install python dependencies +Install with python dependencies ```bash -python -m pip install progressbar psutil pyexiftool pypiwin32 +python -m pip install pypiwin32 photo-importer ``` ## Usage diff --git a/debian/changelog b/debian/changelog index 8b8d4ca..4984c38 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +photo-importer (1.2.0) stable; urgency=medium + + * Add PyPi install support + * Update to new PyExifTool + * Reformat with black + * Update modules import + * Add importer test + * Improve fileprop tests + * Remove legacy modules __main__ + * MacOS support verified + + -- Alexander Bushnev Tue, 29 Nov 2022 09:03:24 +0100 + photo-importer (1.1.2) stable; urgency=medium * Update setup scripts diff --git a/photo_importer/config.py b/photo_importer/config.py index 83b9059..709499e 100755 --- a/photo_importer/config.py +++ b/photo_importer/config.py @@ -35,7 +35,7 @@ class Config(object): 'out_path': '/mnt/multimedia/NEW/', 'in_path': '', 'log_file': 'photo-importer-server.log', - } + }, } def __init__(self, filename=None, create=False): @@ -44,7 +44,11 @@ def __init__(self, filename=None, create=False): self.__config = configparser.ConfigParser() self.__config.read_dict(self.DEFAULTS) - self.__config.read([filename, ]) + self.__config.read( + [ + filename, + ] + ) if create: self.__create_if_not_exists() @@ -59,6 +63,9 @@ def __create_if_not_exists(self): def __getitem__(self, sect): return self.__config[sect] + def set(self, sect, name, val): + self.__config[sect][name] = val + if __name__ == "__main__": Config(create=True) diff --git a/photo_importer/fileprop.py b/photo_importer/fileprop.py index 35f63d6..4e3c531 100755 --- a/photo_importer/fileprop.py +++ b/photo_importer/fileprop.py @@ -17,17 +17,27 @@ class FileProp(object): - DATE_REX = [ - (re.compile('\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}'), - '%Y-%m-%d_%H-%M-%S'), - (re.compile('\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}'), - '%Y-%m-%d-%H-%M-%S'), - (re.compile('\d{4}-\d{2}-\d{2}T\d{2}.\d{2}.\d{2}'), - '%Y-%m-%dT%H.%M.%S'), - (re.compile('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'), - '%Y-%m-%dT%H:%M:%S'), - (re.compile('\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}'), - '%Y_%m_%d_%H_%M_%S'), + DATE_REGEX = [ + ( + re.compile('\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}'), + '%Y-%m-%d_%H-%M-%S', + ), + ( + re.compile('\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}'), + '%Y-%m-%d-%H-%M-%S', + ), + ( + re.compile('\d{4}-\d{2}-\d{2}T\d{2}.\d{2}.\d{2}'), + '%Y-%m-%dT%H.%M.%S', + ), + ( + re.compile('\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'), + '%Y-%m-%dT%H:%M:%S', + ), + ( + re.compile('\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}'), + '%Y_%m_%d_%H_%M_%S', + ), (re.compile('\d{8}_\d{6}'), '%Y%m%d_%H%M%S'), (re.compile('\d{14}'), '%Y%m%d%H%M%S'), (re.compile('\d{8}'), '%Y%m%d'), @@ -55,14 +65,17 @@ class FileProp(object): 'QuickTime:MediaCreateDate', 'PDF:CreateDate', 'XMP:CreateDate', + 'EXIF:CreateDate', ] def __init__(self, config): self.__config = config self.__prepare_ext_to_type() self.__out_list = set() - self.__exiftool = exiftool.ExifTool() - self.__exiftool.start() + self.__exiftool = exiftool.ExifToolHelper() + + def __del__(self): + self.__exiftool.terminate() def __prepare_ext_to_type(self): self.EXT_TO_TYPE = {} @@ -101,7 +114,7 @@ def __time(self, fullname, name, tp): return None def __time_by_name(self, fname): - for exp, fs in self.DATE_REX: + for exp, fs in self.DATE_REGEX: mat = exp.findall(fname) if len(mat): try: @@ -116,7 +129,7 @@ def __time_by_name(self, fname): def __time_by_exif(self, fullname): try: - metadata = self.__exiftool.get_metadata(fullname) + metadata = self.__exiftool.get_metadata(fullname)[0] for tag in self.DATE_TAGS: if tag in metadata: md = metadata[tag] @@ -125,8 +138,10 @@ def __time_by_exif(self, fullname): md = md[0:pos] return datetime.datetime.strptime(md, '%Y:%m:%d %H:%M:%S') - logging.warning('time by exif (%s) not found tags count: %s' % - (fullname, len(metadata))) + logging.warning( + 'time by exif (%s) not found tags count: %s' + % (fullname, len(metadata)) + ) for tag, val in metadata.items(): logging.debug('%s: %s' % (tag, val)) return None @@ -136,7 +151,8 @@ def __time_by_exif(self, fullname): def __time_by_attr(self, fullname): try: return datetime.datetime.fromtimestamp( - time.mktime(time.localtime(os.stat(fullname)[stat.ST_MTIME]))) + time.mktime(time.localtime(os.stat(fullname)[stat.ST_MTIME])) + ) except (FileNotFoundError, KeyError) as ex: logging.warning('time by attr (%s) exception: %s' % (fullname, ex)) @@ -161,13 +177,12 @@ def get(self, fullname): ftime = self.__time(fullname, fname, tp) if ftime: - out_name = ftime.strftime( - self.__config['main']['out_time_format']) + out_name = ftime.strftime(self.__config['main']['out_time_format']) else: out_name = None if out_name: - ok = fname[0:len(out_name)] == out_name + ok = fname[0 : len(out_name)] == out_name else: ok = False @@ -207,11 +222,13 @@ def out_name_full(self, path=None): path = self.__path return self.__prop_ptr._out_name_full( - path, self.__out_name, self.__ext) + path, self.__out_name, self.__ext + ) if __name__ == '__main__': import sys + sys.path.insert(0, os.path.abspath('..')) from photo_importer import log diff --git a/photo_importer/fileprop_test.py b/photo_importer/fileprop_test.py index d3b0a70..9515b44 100755 --- a/photo_importer/fileprop_test.py +++ b/photo_importer/fileprop_test.py @@ -1,20 +1,19 @@ #!/usr/bin/python3 -import os -import sys import unittest import datetime -sys.path.insert(0, os.path.abspath('..')) - -from photo_importer import config # noqa -from photo_importer import fileprop # noqa +from . import config +from . import fileprop class TestFileProp(unittest.TestCase): def setUp(self): self.conf = config.Config() self.fp = fileprop.FileProp(self.conf) + self.conf.set('main', 'time_src_image', 'name') + self.conf.set('main', 'time_src_video', 'name') + self.conf.set('main', 'time_src_audio', 'name') # photo def test_camera_photo(self): @@ -145,7 +144,3 @@ def test_garbage(self): self.assertEqual(fp.type(), fileprop.GARBAGE) self.assertEqual(fp.time(), None) self.assertEqual(fp.ok(), False) - - -if __name__ == '__main__': - unittest.main() diff --git a/photo_importer/importer.py b/photo_importer/importer.py index ddcf802..8b6be57 100755 --- a/photo_importer/importer.py +++ b/photo_importer/importer.py @@ -1,17 +1,13 @@ #!/usr/bin/python3 import os -import sys import logging import threading -sys.path.insert(0, os.path.abspath('..')) - -from photo_importer import log # noqa -from photo_importer import mover # noqa -from photo_importer import config # noqa -from photo_importer import rotator # noqa -from photo_importer import fileprop # noqa +from . import log +from . import mover +from . import rotator +from . import fileprop class Importer(threading.Thread): @@ -28,8 +24,9 @@ def __init__(self, config, input_path, output_path, dryrun): def run(self): logging.info( - 'Start: %s -> %s (dryrun: %s)' % - (self.__input_path, self.__output_path, self.__dryrun)) + 'Start: %s -> %s (dryrun: %s)' + % (self.__input_path, self.__output_path, self.__dryrun) + ) filenames, dirs = self.__scan_files(self.__input_path) @@ -48,7 +45,8 @@ def __scan_files(self, input_path): res_dir = [] res = [] for root, dirs, files in os.walk( - input_path, onerror=self.__on_walk_error): + input_path, onerror=self.__on_walk_error + ): for fname in files: res.append(os.path.join(root, fname)) @@ -72,7 +70,8 @@ def __move_files(self, filenames): self.__input_path, self.__output_path, filenames, - self.__dryrun) + self.__dryrun, + ) self.__stat['stage'] = 'move' res = self.__mov.run() @@ -88,10 +87,7 @@ def __image_filenames(self, move_result): def __rotate_files(self, filenames): logging.info('Rotating') - self.__rot = rotator.Rotator( - self.__config, - filenames, - self.__dryrun) + self.__rot = rotator.Rotator(self.__config, filenames, self.__dryrun) self.__stat['stage'] = 'rotate' self.__rot.run() @@ -117,15 +113,3 @@ def status(self): def log_text(self): return self.__log.get_text() - - -if __name__ == '__main__': - import sys - - log.initLogger() - - imp = Importer(config.Config(), sys.argv[1], sys.argv[2], False) - imp.start() - imp.join() - - print(imp.status()) diff --git a/photo_importer/importer_test.py b/photo_importer/importer_test.py new file mode 100755 index 0000000..31346e3 --- /dev/null +++ b/photo_importer/importer_test.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 + +import os +import unittest +import tempfile + +from . import config +from . import importer + + +class TestImporter(unittest.TestCase): + def test_importer(self): + with tempfile.TemporaryDirectory() as tmpdirname: + cfg = config.Config() + cfg.set('main', 'move_mode', '0') + imp = importer.Importer( + cfg, + os.path.join(os.path.dirname(__file__), 'test_data'), + tmpdirname, + False, + ) + imp.start() + imp.join() + + self.assertEqual( + imp.status(), + { + 'stage': 'done', + 'total': 2, + 'move': { + 'total': 2, + 'moved': 0, + 'copied': 2, + 'removed': 0, + 'skipped': 0, + 'processed': 2, + 'errors': 0, + }, + 'rotate': { + 'total': 2, + 'processed': 2, + 'good': 2, + 'errors': 0, + }, + }, + ) + + files = [] + for path, cd, fs in os.walk(tmpdirname): + for f in fs: + print(os.path.join(path, f)) + files.append(os.path.join(path, f)) + files.sort() + + self.assertEqual(len(files), 2) + self.assertEqual( + files[0], + os.path.join( + tmpdirname, 'Foto/2021/2021-12-19/2021-12-19_13-11-36.jpeg' + ), + ) + self.assertEqual( + files[1], + os.path.join( + tmpdirname, 'Foto/2022/2022-11-21/2022-11-21_00-42-07.JPG' + ), + ) diff --git a/photo_importer/mover.py b/photo_importer/mover.py index 03f1dc2..eb67ac3 100755 --- a/photo_importer/mover.py +++ b/photo_importer/mover.py @@ -5,7 +5,7 @@ import logging import subprocess -from photo_importer import fileprop +from . import fileprop class Mover(object): @@ -71,7 +71,9 @@ def __move_file(self, fname, prop): os.path.join( self.__output_path, self.__config['main'][self.OUT_SUBDIR_CFG[prop.type()]], - self.__config['main']['out_date_format'])) + self.__config['main']['out_date_format'], + ) + ) if not os.path.isdir(path): if not self.__dryrun: @@ -120,9 +122,8 @@ def __run(self, args): return True with subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as proc: proc.wait() info = proc.stdout.read().strip() if info: @@ -131,8 +132,9 @@ def __run(self, args): if err: logging.error(err.decode("utf-8")) elif proc.returncode != 0: - logging.error('%s failed with code %i' % - (args[0], proc.returncode)) + logging.error( + '%s failed with code %i' % (args[0], proc.returncode) + ) return proc.returncode == 0 def status(self): diff --git a/photo_importer/rotator.py b/photo_importer/rotator.py index ddfd0f5..ebd46e5 100755 --- a/photo_importer/rotator.py +++ b/photo_importer/rotator.py @@ -7,7 +7,6 @@ import subprocess import concurrent.futures -from photo_importer import config JPEGTRAN_COMMAND = { 0: None, @@ -18,7 +17,7 @@ 5: '-transpose', 6: '-rotate 90', 7: '-transverse', - 8: '-rotate 270' + 8: '-rotate 270', } ORIENTATION_TAG = 'EXIF:Orientation' @@ -39,16 +38,15 @@ def run(self): processor = self.__process_exiftran self.__exiftool = None if int(self.__config['main']['use_jpegtran']): - self.__exiftool = exiftool.ExifTool() - self.__exiftool.start() + self.__exiftool = exiftool.ExifToolHelper() processor = self.__process_jpegtran tc = 1 with concurrent.futures.ThreadPoolExecutor(max_workers=tc) as executor: futures = { - executor.submit(processor, fn): - fn for fn in self.__filenames} + executor.submit(processor, fn): fn for fn in self.__filenames + } for future in concurrent.futures.as_completed(futures): self.__processed += 1 @@ -69,24 +67,24 @@ def __process_exiftran(self, filename): if self.__dryrun: return True - p = subprocess.Popen( + error = '' + with subprocess.Popen( cmd, shell=True, universal_newlines=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE).stderr - - error = '' - while True: - line = p.readline() - if not line: - break - - if line.startswith('processing '): - ok = True - else: - ok = False - error += line + stderr=subprocess.PIPE, + ) as p: + while True: + line = p.stderr.readline() + if not line: + break + + if line.startswith('processing '): + ok = True + else: + ok = False + error += line if error != '': logging.error('exiftran (%s) error: %s' % (filename, error)) @@ -102,8 +100,9 @@ def __process_jpegtran(self, filename): if orientation_cmd is None: return True - logging.debug('rotate: jpegtran %s %s' % - (orientation_cmd, filename)) + logging.debug( + 'rotate: jpegtran %s %s' % (orientation_cmd, filename) + ) if self.__dryrun: return True @@ -111,20 +110,25 @@ def __process_jpegtran(self, filename): handle, tmpfile = tempfile.mkstemp(dir=os.path.dirname(filename)) os.close(handle) - cmd = 'jpegtran -copy all -outfile %s %s %s' % \ - (tmpfile, orientation_cmd, filename) + cmd = 'jpegtran -copy all -outfile %s %s %s' % ( + tmpfile, + orientation_cmd, + filename, + ) - p = subprocess.Popen( + with subprocess.Popen( cmd, shell=True, universal_newlines=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE).stderr - - line = p.readline() - if line: - logging.error('jpegtran (%s) failed: %s' % (filename, line)) - return False + stderr=subprocess.PIPE, + ) as p: + line = p.stderr.readline() + if line: + logging.error( + 'jpegtran (%s) failed: %s' % (filename, line) + ) + return False self.__clear_orientation_tag(tmpfile) @@ -137,18 +141,17 @@ def __process_jpegtran(self, filename): return False def __get_orientation_cmd(self, fullname): - orientation = self.__exiftool.get_tag(ORIENTATION_TAG, fullname) - if orientation is not None and \ - 0 <= orientation and orientation < len(JPEGTRAN_COMMAND): + tags = self.__exiftool.get_tags(fullname, ORIENTATION_TAG) + if ORIENTATION_TAG not in tags: + return None + orientation = tags[ORIENTATION_TAG] + if 0 <= orientation and orientation < len(JPEGTRAN_COMMAND): return JPEGTRAN_COMMAND[orientation] else: return None def __clear_orientation_tag(self, fullname): - res = self.__exiftool.set_tags( - {ORIENTATION_TAG: 1}, fullname).decode('utf-8') - if not exiftool.check_ok(res): - raise SystemError('exiftool error: ' + exiftool.format_error(res)) + self.__exiftool.set_tags(fullname, {ORIENTATION_TAG: 1}) try: os.remove(fullname + '_original') except Exception: @@ -159,13 +162,5 @@ def status(self): 'total': len(self.__filenames), 'processed': self.__processed, 'good': self.__good, - 'errors': self.__errors} - - -if __name__ == '__main__': - import sys - - rot = Rotator(config.Config(), sys.argv[1:], False) - rot.run() - - print(rot.status()) + 'errors': self.__errors, + } diff --git a/photo_importer/run.py b/photo_importer/run.py index 63819f1..2d9cf3d 100755 --- a/photo_importer/run.py +++ b/photo_importer/run.py @@ -1,17 +1,13 @@ #!/usr/bin/python3 -import os -import sys import logging import argparse import threading import progressbar -sys.path.insert(0, os.path.abspath('..')) - -from photo_importer import log # noqa -from photo_importer import config # noqa -from photo_importer import importer # noqa +from . import log +from . import config +from . import importer class ProgressBar(threading.Thread): @@ -31,10 +27,15 @@ def __create(self, name, count): self.__pbar = progressbar.ProgressBar( maxval=count, widgets=[ - name, ' ', - progressbar.Percentage(), ' ', - progressbar.Bar(), ' ', - progressbar.ETA()]).start() + name, + ' ', + progressbar.Percentage(), + ' ', + progressbar.Bar(), + ' ', + progressbar.ETA(), + ], + ).start() def run(self): stage = '' @@ -58,8 +59,9 @@ def run(self): self.__pbar.finish() break - if (stage == 'move' or stage == 'rotate') and \ - self.__pbar is not None: + if ( + stage == 'move' or stage == 'rotate' + ) and self.__pbar is not None: self.__pbar.update(stat[stage]['processed']) @@ -80,11 +82,7 @@ def main(): log.initLogger(args.logfile) - imp = importer.Importer( - cfg, - args.in_path, - args.out_path, - args.dryrun) + imp = importer.Importer(cfg, args.in_path, args.out_path, args.dryrun) pbar = ProgressBar(imp) imp.start() diff --git a/photo_importer/server.py b/photo_importer/server.py index b95b14d..3a4a337 100755 --- a/photo_importer/server.py +++ b/photo_importer/server.py @@ -2,7 +2,6 @@ import os import re -import sys import glob import json import psutil @@ -12,11 +11,9 @@ import http.server from http import HTTPStatus -sys.path.insert(0, os.path.abspath('..')) - -from photo_importer import log # noqa -from photo_importer import config # noqa -from photo_importer import importer # noqa +from . import log +from . import config +from . import importer FIXED_IN_PATH_NAME = 'none' @@ -32,7 +29,6 @@ def __str__(self): class PhotoImporterHandler(http.server.BaseHTTPRequestHandler): - def __ok_response(self, result): self.send_response(200) self.send_header('Content-type', 'application/json') @@ -49,11 +45,13 @@ def __error_response(self, code, err): self.send_error(code, explain=str(err)) def __get_mounted_list(self): - return {os.path.basename(dp.device): (dp.device, dp.mountpoint) - for dp in psutil.disk_partitions()} + return { + os.path.basename(dp.device): (dp.device, dp.mountpoint) + for dp in psutil.disk_partitions() + } def __bytes_to_gbytes(self, b): - return round(b / 1024. / 1024. / 1024., 2) + return round(b / 1024.0 / 1024.0 / 1024.0, 2) def __get_removable_devices_posix(self): mount_list = self.__get_mounted_list() @@ -73,13 +71,13 @@ def __get_removable_devices_posix(self): res[pdev] = { 'dev_path': mount_list[pdev][0], 'mount_path': mount_list[pdev][1], - 'read_only': read_only + 'read_only': read_only, } else: res[pdev] = { 'dev_path': '/dev/' + pdev, 'mount_path': '', - 'read_only': read_only + 'read_only': read_only, } return res @@ -96,7 +94,7 @@ def __get_removable_devices_win(self): res[dev_name] = { 'dev_path': dev_name, 'mount_path': d, - 'read_only': not os.access(d, os.W_OK) + 'read_only': not os.access(d, os.W_OK), } return res @@ -113,8 +111,9 @@ def __get_removable_devices(self): res[FIXED_IN_PATH_NAME] = { 'dev_path': FIXED_IN_PATH_NAME, 'mount_path': self.server.fixed_in_path(), - 'read_only': - not os.access(self.server.fixed_in_path(), os.W_OK) + 'read_only': not os.access( + self.server.fixed_in_path(), os.W_OK + ), } return res @@ -138,13 +137,15 @@ def __mount_get_list(self): r['progress'] = 0 r['read_only'] = info['read_only'] r['allow_start'] = not ( - info['read_only'] and self.server.move_mode()) + info['read_only'] and self.server.move_mode() + ) if r['path']: stat = self.server.import_status(r['path']) du = psutil.disk_usage(r['path']) if dev == FIXED_IN_PATH_NAME: r['size'] = self.__bytes_to_gbytes( - self.__folder_size(r['path'])) + self.__folder_size(r['path']) + ) else: r['size'] = self.__bytes_to_gbytes(du.total) r['usage'] = du.percent @@ -152,12 +153,13 @@ def __mount_get_list(self): stage = stat['stage'] r['state'] = stage if stage == 'move' or stage == 'rotate': - r['progress'] = \ - round(100. * - stat[stage]['processed'] / stat['total']) + r['progress'] = round( + 100.0 * stat[stage]['processed'] / stat['total'] + ) elif stage == 'done': - cerr = stat['move']['errors'] + \ - stat['rotate']['errors'] + cerr = ( + stat['move']['errors'] + stat['rotate']['errors'] + ) if cerr != 0: r['state'] = 'error' r['total'] = cerr @@ -178,8 +180,7 @@ def __check_dev_for_mount(self, dev): raise HTTPError(HTTPStatus.BAD_REQUEST, 'empty "d" param') dev_list = self.__get_removable_devices() if dev not in dev_list: - raise HTTPError(HTTPStatus.BAD_REQUEST, - 'wrong device: %s' % dev) + raise HTTPError(HTTPStatus.BAD_REQUEST, 'wrong device: %s' % dev) device = dev_list[dev] if device['mount_path']: self.server.import_done(device['mount_path']) @@ -216,8 +217,9 @@ def __mount_request(self, params): elif action == 'umount': result = self.__mount_umount(dev) else: - raise HTTPError(HTTPStatus.BAD_REQUEST, - 'unknown action %s' % action) + raise HTTPError( + HTTPStatus.BAD_REQUEST, 'unknown action %s' % action + ) self.__ok_response(result) @@ -256,8 +258,9 @@ def __import_request(self, params): result = self.__import_get_log(in_path) self.__text_response(result) else: - raise HTTPError(HTTPStatus.BAD_REQUEST, - 'unknown action %s' % action) + raise HTTPError( + HTTPStatus.BAD_REQUEST, 'unknown action %s' % action + ) def __sysinfo_request(self, params): try: @@ -279,7 +282,8 @@ def __file_request(self, path): if (path[0]) == '/': path = path[1:] fname = os.path.normpath( - os.path.join(self.server.web_path(), path)) + os.path.join(self.server.web_path(), path) + ) if not fname.startswith(self.server.web_path()): logging.warning('incorrect path: ' + path) raise HTTPError(HTTPStatus.NOT_FOUND, path) @@ -387,10 +391,8 @@ def import_start(self, in_path, out_path): logging.info('import_start: %s', in_path) self.__importers[in_path] = importer.Importer( - self.__cfg, - in_path, - out_path, - False) + self.__cfg, in_path, out_path, False + ) self.__importers[in_path].start() diff --git a/photo_importer/test_data/img_1.JPG b/photo_importer/test_data/img_1.JPG new file mode 100644 index 0000000..c39e50a Binary files /dev/null and b/photo_importer/test_data/img_1.JPG differ diff --git a/photo_importer/test_data/img_2.jpeg b/photo_importer/test_data/img_2.jpeg new file mode 100644 index 0000000..5f01fce Binary files /dev/null and b/photo_importer/test_data/img_2.jpeg differ diff --git a/setup.py b/setup.py index cf8a023..60a9349 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,9 @@ def get_long_description(): this_directory = os.path.abspath(os.path.dirname(__file__)) - with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: + with open( + os.path.join(this_directory, 'README.md'), encoding='utf-8' + ) as f: long_description = f.read() return long_description @@ -12,7 +14,7 @@ def get_long_description(): setup( name='photo-importer', - version='1.1.2', + version='1.2.0', description='Photo importer tool', author='Alexander Bushnev', author_email='Alexander@Bushnev.pro', @@ -32,6 +34,7 @@ def get_long_description(): 'Development Status :: 5 - Production/Stable', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', + 'Operating System :: MacOS', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', @@ -40,6 +43,7 @@ def get_long_description(): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Topic :: Scientific/Engineering :: Image Processing', 'Topic :: Multimedia :: Video', 'Topic :: Utilities',