mirror of
https://github.com/sashacmc/photo-importer.git
synced 2025-02-22 18:42:16 +02:00
Merge pull request #9 from sashacmc/1.2.0
This commit is contained in:
commit
32b832dcbc
36
README.md
36
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
|
||||
|
13
debian/changelog
vendored
13
debian/changelog
vendored
@ -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 <Alexander@Bushnev.pro> Tue, 29 Nov 2022 09:03:24 +0100
|
||||
|
||||
photo-importer (1.1.2) stable; urgency=medium
|
||||
|
||||
* Update setup scripts
|
||||
|
@ -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 @@ class Config(object):
|
||||
|
||||
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 @@ class Config(object):
|
||||
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)
|
||||
|
@ -17,17 +17,27 @@ GARBAGE = 4
|
||||
|
||||
|
||||
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 @@ class FileProp(object):
|
||||
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 @@ class FileProp(object):
|
||||
|
||||
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 @@ class FileProp(object):
|
||||
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 @@ class FileProp(object):
|
||||
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 @@ class FileProp(object):
|
||||
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 @@ class FilePropRes(object):
|
||||
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
|
||||
|
@ -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 @@ class TestFileProp(unittest.TestCase):
|
||||
self.assertEqual(fp.type(), fileprop.GARBAGE)
|
||||
self.assertEqual(fp.time(), None)
|
||||
self.assertEqual(fp.ok(), False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -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 @@ class Importer(threading.Thread):
|
||||
|
||||
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 @@ class Importer(threading.Thread):
|
||||
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 @@ class Importer(threading.Thread):
|
||||
self.__input_path,
|
||||
self.__output_path,
|
||||
filenames,
|
||||
self.__dryrun)
|
||||
self.__dryrun,
|
||||
)
|
||||
self.__stat['stage'] = 'move'
|
||||
|
||||
res = self.__mov.run()
|
||||
@ -88,10 +87,7 @@ class Importer(threading.Thread):
|
||||
|
||||
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 @@ class Importer(threading.Thread):
|
||||
|
||||
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())
|
||||
|
67
photo_importer/importer_test.py
Executable file
67
photo_importer/importer_test.py
Executable file
@ -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'
|
||||
),
|
||||
)
|
@ -5,7 +5,7 @@ import shutil
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from photo_importer import fileprop
|
||||
from . import fileprop
|
||||
|
||||
|
||||
class Mover(object):
|
||||
@ -71,7 +71,9 @@ class Mover(object):
|
||||
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 @@ class Mover(object):
|
||||
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 @@ class Mover(object):
|
||||
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):
|
||||
|
@ -7,7 +7,6 @@ import tempfile
|
||||
import subprocess
|
||||
import concurrent.futures
|
||||
|
||||
from photo_importer import config
|
||||
|
||||
JPEGTRAN_COMMAND = {
|
||||
0: None,
|
||||
@ -18,7 +17,7 @@ JPEGTRAN_COMMAND = {
|
||||
5: '-transpose',
|
||||
6: '-rotate 90',
|
||||
7: '-transverse',
|
||||
8: '-rotate 270'
|
||||
8: '-rotate 270',
|
||||
}
|
||||
|
||||
ORIENTATION_TAG = 'EXIF:Orientation'
|
||||
@ -39,16 +38,15 @@ class Rotator(object):
|
||||
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 @@ class Rotator(object):
|
||||
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
|
||||
stderr=subprocess.PIPE,
|
||||
) as p:
|
||||
while True:
|
||||
line = p.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
error = ''
|
||||
while True:
|
||||
line = p.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
if line.startswith('processing '):
|
||||
ok = True
|
||||
else:
|
||||
ok = False
|
||||
error += line
|
||||
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 @@ class Rotator(object):
|
||||
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 @@ class Rotator(object):
|
||||
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 @@ class Rotator(object):
|
||||
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 @@ class Rotator(object):
|
||||
'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,
|
||||
}
|
||||
|
@ -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 @@ class ProgressBar(threading.Thread):
|
||||
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 @@ class ProgressBar(threading.Thread):
|
||||
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()
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import glob
|
||||
import json
|
||||
import psutil
|
||||
@ -12,11 +11,9 @@ import argparse
|
||||
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 @@ class HTTPError(Exception):
|
||||
|
||||
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterHandler(http.server.BaseHTTPRequestHandler):
|
||||
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 @@ class PhotoImporterServer(http.server.HTTPServer):
|
||||
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()
|
||||
|
||||
|
BIN
photo_importer/test_data/img_1.JPG
Normal file
BIN
photo_importer/test_data/img_1.JPG
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
photo_importer/test_data/img_2.jpeg
Normal file
BIN
photo_importer/test_data/img_2.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
8
setup.py
8
setup.py
@ -4,7 +4,9 @@ import os
|
||||
|
||||
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 @@ setup(
|
||||
'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 @@ setup(
|
||||
'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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user