format python code with black --skip-string-normalization

This commit is contained in:
Cédric Leporcq 2021-10-15 19:56:50 +02:00
parent 1cade46307
commit a93e7accc0
12 changed files with 612 additions and 365 deletions

335
ordigi.py
View File

@ -16,27 +16,49 @@ from ordigi.summary import Summary
_logger_options = [
click.option('--debug', default=False, is_flag=True,
help='Override the value in constants.py with True.'),
click.option('--verbose', '-v', default=False, is_flag=True,
help='True if you want to see details of file processing')
click.option(
'--debug',
default=False,
is_flag=True,
help='Override the value in constants.py with True.',
),
click.option(
'--verbose',
'-v',
default=False,
is_flag=True,
help='True if you want to see details of file processing',
),
]
_dry_run_options = [
click.option('--dry-run', default=False, is_flag=True,
help='Dry run only, no change made to the filesystem.')
click.option(
'--dry-run',
default=False,
is_flag=True,
help='Dry run only, no change made to the filesystem.',
)
]
_filter_option = [
click.option('--exclude', '-e', default=set(), multiple=True,
help='Directories or files to exclude.'),
click.option('--filter-by-ext', '-f', default=set(), multiple=True,
help="""Use filename
click.option(
'--exclude',
'-e',
default=set(),
multiple=True,
help='Directories or files to exclude.',
),
click.option(
'--filter-by-ext',
'-f',
default=set(),
multiple=True,
help="""Use filename
extension to filter files for sorting. If value is '*', use
common media file extension for filtering. Ignored files remain in
the same directory structure""" ),
click.option('--glob', '-g', default='**/*',
help='Glob file selection')
the same directory structure""",
),
click.option('--glob', '-g', default='**/*', help='Glob file selection'),
]
@ -49,6 +71,7 @@ def add_options(options):
for option in reversed(options):
func = option(func)
return func
return _add_options
@ -63,31 +86,74 @@ def _get_exclude(opt, exclude):
@add_options(_logger_options)
@add_options(_dry_run_options)
@add_options(_filter_option)
@click.option('--album-from-folder', default=False, is_flag=True,
help="Use images' folders as their album names.")
@click.option('--destination', '-d', type=click.Path(file_okay=False),
default=None, help='Sort files into this directory.')
@click.option('--clean', '-C', default=False, is_flag=True,
help='Clean empty folders')
@click.option('--copy', '-c', default=False, is_flag=True,
help='True if you want files to be copied over from src_dir to\
dest_dir rather than moved')
@click.option('--ignore-tags', '-I', default=set(), multiple=True,
help='Specific tags or group that will be ignored when\
searching for file data. Example \'File:FileModifyDate\' or \'Filename\'' )
@click.option('--interactive', '-i', default=False, is_flag=True,
help="Interactive mode")
@click.option('--max-deep', '-m', default=None,
help='Maximum level to proceed. Number from 0 to desired level.')
@click.option('--remove-duplicates', '-R', default=False, is_flag=True,
help='True to remove files that are exactly the same in name\
and a file hash')
@click.option('--reset-cache', '-r', default=False, is_flag=True,
help='Regenerate the hash.json and location.json database ')
@click.option('--use-date-filename', '-f', default=False, is_flag=True,
help="Use filename date for media original date.")
@click.option('--use-file-dates', '-F', default=False, is_flag=True,
help="Use file date created or modified for media original date.")
@click.option(
'--album-from-folder',
default=False,
is_flag=True,
help="Use images' folders as their album names.",
)
@click.option(
'--destination',
'-d',
type=click.Path(file_okay=False),
default=None,
help='Sort files into this directory.',
)
@click.option('--clean', '-C', default=False, is_flag=True, help='Clean empty folders')
@click.option(
'--copy',
'-c',
default=False,
is_flag=True,
help='True if you want files to be copied over from src_dir to\
dest_dir rather than moved',
)
@click.option(
'--ignore-tags',
'-I',
default=set(),
multiple=True,
help='Specific tags or group that will be ignored when\
searching for file data. Example \'File:FileModifyDate\' or \'Filename\'',
)
@click.option(
'--interactive', '-i', default=False, is_flag=True, help="Interactive mode"
)
@click.option(
'--max-deep',
'-m',
default=None,
help='Maximum level to proceed. Number from 0 to desired level.',
)
@click.option(
'--remove-duplicates',
'-R',
default=False,
is_flag=True,
help='True to remove files that are exactly the same in name\
and a file hash',
)
@click.option(
'--reset-cache',
'-r',
default=False,
is_flag=True,
help='Regenerate the hash.json and location.json database ',
)
@click.option(
'--use-date-filename',
'-f',
default=False,
is_flag=True,
help="Use filename date for media original date.",
)
@click.option(
'--use-file-dates',
'-F',
default=False,
is_flag=True,
help="Use file date created or modified for media original date.",
)
@click.argument('paths', required=True, nargs=-1, type=click.Path())
def sort(**kwargs):
"""Sort files or directories by reading their EXIF and organizing them
@ -135,17 +201,29 @@ def sort(**kwargs):
exclude = _get_exclude(opt, kwargs['exclude'])
filter_by_ext = set(kwargs['filter_by_ext'])
collection = Collection(destination, opt['path_format'],
kwargs['album_from_folder'], cache, opt['day_begins'], kwargs['dry_run'],
exclude, filter_by_ext, kwargs['glob'], kwargs['interactive'],
logger, max_deep, mode, kwargs['use_date_filename'],
kwargs['use_file_dates'])
collection = Collection(
destination,
opt['path_format'],
kwargs['album_from_folder'],
cache,
opt['day_begins'],
kwargs['dry_run'],
exclude,
filter_by_ext,
kwargs['glob'],
kwargs['interactive'],
logger,
max_deep,
mode,
kwargs['use_date_filename'],
kwargs['use_file_dates'],
)
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'],
opt['timeout'])
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'], opt['timeout'])
summary, result = collection.sort_files(paths, loc,
kwargs['remove_duplicates'], kwargs['ignore_tags'])
summary, result = collection.sort_files(
paths, loc, kwargs['remove_duplicates'], kwargs['ignore_tags']
)
if kwargs['clean']:
remove_empty_folders(destination, logger)
@ -158,42 +236,62 @@ def sort(**kwargs):
def remove_empty_folders(path, logger, remove_root=True):
'Function to remove empty folders'
if not os.path.isdir(path):
return
'Function to remove empty folders'
if not os.path.isdir(path):
return
# remove empty subfolders
files = os.listdir(path)
if len(files):
for f in files:
fullpath = os.path.join(path, f)
if os.path.isdir(fullpath):
remove_empty_folders(fullpath, logger)
# remove empty subfolders
files = os.listdir(path)
if len(files):
for f in files:
fullpath = os.path.join(path, f)
if os.path.isdir(fullpath):
remove_empty_folders(fullpath, logger)
# if folder empty, delete it
files = os.listdir(path)
if len(files) == 0 and remove_root:
logger.info(f"Removing empty folder: {path}")
os.rmdir(path)
# if folder empty, delete it
files = os.listdir(path)
if len(files) == 0 and remove_root:
logger.info(f"Removing empty folder: {path}")
os.rmdir(path)
@click.command('clean')
@add_options(_logger_options)
@add_options(_dry_run_options)
@add_options(_filter_option)
@click.option('--dedup-regex', '-d', default=set(), multiple=True,
help='Regex to match duplicate strings parts')
@click.option('--folders', '-f', default=False, is_flag=True,
help='Remove empty folders')
@click.option('--max-deep', '-m', default=None,
help='Maximum level to proceed. Number from 0 to desired level.')
@click.option('--path-string', '-p', default=False, is_flag=True,
help='Deduplicate path string')
@click.option('--remove-duplicates', '-R', default=False, is_flag=True,
help='True to remove files that are exactly the same in name\
and a file hash')
@click.option('--root', '-r', type=click.Path(file_okay=False),
default=None, help='Root dir of media collection. If not set, use path')
@click.option(
'--dedup-regex',
'-d',
default=set(),
multiple=True,
help='Regex to match duplicate strings parts',
)
@click.option(
'--folders', '-f', default=False, is_flag=True, help='Remove empty folders'
)
@click.option(
'--max-deep',
'-m',
default=None,
help='Maximum level to proceed. Number from 0 to desired level.',
)
@click.option(
'--path-string', '-p', default=False, is_flag=True, help='Deduplicate path string'
)
@click.option(
'--remove-duplicates',
'-R',
default=False,
is_flag=True,
help='True to remove files that are exactly the same in name and a file hash',
)
@click.option(
'--root',
'-r',
type=click.Path(file_okay=False),
default=None,
help='Root dir of media collection. If not set, use path',
)
@click.argument('path', required=True, nargs=1, type=click.Path())
def clean(**kwargs):
"""Remove empty folders
@ -221,11 +319,21 @@ def clean(**kwargs):
filter_by_ext = set(kwargs['filter_by_ext'])
if kwargs['path_string']:
collection = Collection(root, opt['path_format'], dry_run=dry_run,
exclude=exclude, filter_by_ext=filter_by_ext, glob=kwargs['glob'],
logger=logger, max_deep=kwargs['max_deep'], mode='move')
collection = Collection(
root,
opt['path_format'],
dry_run=dry_run,
exclude=exclude,
filter_by_ext=filter_by_ext,
glob=kwargs['glob'],
logger=logger,
max_deep=kwargs['max_deep'],
mode='move',
)
dedup_regex = list(kwargs['dedup_regex'])
summary, result = collection.dedup_regex(path, dedup_regex, kwargs['remove_duplicates'])
summary, result = collection.dedup_regex(
path, dedup_regex, kwargs['remove_duplicates']
)
if clean_all or folders:
remove_empty_folders(path, logger)
@ -241,12 +349,10 @@ def clean(**kwargs):
@add_options(_logger_options)
@click.argument('path', required=True, nargs=1, type=click.Path())
def init(**kwargs):
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files.
"""
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files."""
config = Config(constants.CONFIG_FILE)
opt = config.get_options()
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'],
opt['timeout'])
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'], opt['timeout'])
debug = kwargs['debug']
verbose = kwargs['verbose']
logger = log.get_logger(debug, verbose)
@ -260,12 +366,10 @@ def init(**kwargs):
@add_options(_logger_options)
@click.argument('path', required=True, nargs=1, type=click.Path())
def update(**kwargs):
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files.
"""
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files."""
config = Config(constants.CONFIG_FILE)
opt = config.get_options()
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'],
opt['timeout'])
loc = GeoLocation(opt['geocoder'], opt['prefer_english_names'], opt['timeout'])
debug = kwargs['debug']
verbose = kwargs['verbose']
logger = log.get_logger(debug, verbose)
@ -301,17 +405,40 @@ def check(**kwargs):
@add_options(_dry_run_options)
@add_options(_filter_option)
@click.option('--find-duplicates', '-f', default=False, is_flag=True)
@click.option('--output-dir', '-o', default=False, is_flag=True, help='output\
dir')
@click.option(
'--output-dir',
'-o',
default=False,
is_flag=True,
help='output dir',
)
@click.option('--remove-duplicates', '-r', default=False, is_flag=True)
@click.option('--revert-compare', '-R', default=False, is_flag=True, help='Revert\
compare')
@click.option('--root', '-r', type=click.Path(file_okay=False),
default=None, help='Root dir of media collection. If not set, use path')
@click.option('--similar-to', '-s', default=False, help='Similar to given\
image')
@click.option('--similarity', '-S', default=80, help='Similarity level for\
images')
@click.option(
'--revert-compare',
'-R',
default=False,
is_flag=True,
help='Revert compare',
)
@click.option(
'--root',
'-r',
type=click.Path(file_okay=False),
default=None,
help='Root dir of media collection. If not set, use path',
)
@click.option(
'--similar-to',
'-s',
default=False,
help='Similar to given image',
)
@click.option(
'--similarity',
'-S',
default=80,
help='Similarity level for images',
)
@click.argument('path', nargs=1, required=True)
def compare(**kwargs):
'''Compare files in directories'''
@ -333,9 +460,16 @@ def compare(**kwargs):
exclude = _get_exclude(opt, kwargs['exclude'])
filter_by_ext = set(kwargs['filter_by_ext'])
collection = Collection(root, None, exclude=exclude,
filter_by_ext=filter_by_ext, glob=kwargs['glob'],
mode='move', dry_run=dry_run, logger=logger)
collection = Collection(
root,
None,
exclude=exclude,
filter_by_ext=filter_by_ext,
glob=kwargs['glob'],
mode='move',
dry_run=dry_run,
logger=logger,
)
if kwargs['revert_compare']:
summary, result = collection.revert_compare(path)
@ -364,4 +498,3 @@ main.add_command(update)
if __name__ == '__main__':
main()

View File

@ -24,14 +24,27 @@ from ordigi.summary import Summary
from ordigi import utils
class Collection(object):
class Collection:
"""Class of the media collection."""
def __init__(self, root, path_format, album_from_folder=False,
cache=False, day_begins=0, dry_run=False, exclude=set(),
filter_by_ext=set(), glob='**/*', interactive=False,
logger=logging.getLogger(), max_deep=None, mode='copy',
use_date_filename=False, use_file_dates=False):
def __init__(
self,
root,
path_format,
album_from_folder=False,
cache=False,
day_begins=0,
dry_run=False,
exclude=set(),
filter_by_ext=set(),
glob='**/*',
interactive=False,
logger=logging.getLogger(),
max_deep=None,
mode='copy',
use_date_filename=False,
use_file_dates=False,
):
# Attributes
self.root = Path(root).expanduser().absolute()
@ -76,31 +89,33 @@ class Collection(object):
def get_items(self):
return {
'album': '{album}',
'basename': '{basename}',
'camera_make': '{camera_make}',
'camera_model': '{camera_model}',
'city': '{city}',
'custom': '{".*"}',
'country': '{country}',
# 'folder': '{folder[<>]?[-+]?[1-9]?}',
'ext': '{ext}',
'folder': '{folder}',
'folders': r'{folders(\[[0-9:]{0,3}\])?}',
'location': '{location}',
'name': '{name}',
'original_name': '{original_name}',
'state': '{state}',
'title': '{title}',
'date': '{(%[a-zA-Z][^a-zA-Z]*){1,8}}' # search for date format string
}
'album': '{album}',
'basename': '{basename}',
'camera_make': '{camera_make}',
'camera_model': '{camera_model}',
'city': '{city}',
'custom': '{".*"}',
'country': '{country}',
# 'folder': '{folder[<>]?[-+]?[1-9]?}',
'ext': '{ext}',
'folder': '{folder}',
'folders': r'{folders(\[[0-9:]{0,3}\])?}',
'location': '{location}',
'name': '{name}',
'original_name': '{original_name}',
'state': '{state}',
'title': '{title}',
'date': '{(%[a-zA-Z][^a-zA-Z]*){1,8}}', # search for date format string
}
def _check_for_early_morning_photos(self, date):
"""check for early hour photos to be grouped with previous day"""
if date.hour < self.day_begins:
self.logger.info("moving this photo to the previous day for classification purposes")
self.logger.info(
"moving this photo to the previous day for classification purposes"
)
# push it to the day before for classification purposes
date = date - timedelta(hours=date.hour+1)
date = date - timedelta(hours=date.hour + 1)
return date
@ -181,8 +196,17 @@ class Collection(object):
folders = self._get_folders(folders, mask)
part = os.path.join(*folders)
elif item in ('album','camera_make', 'camera_model', 'city', 'country',
'location', 'original_name', 'state', 'title'):
elif item in (
'album',
'camera_make',
'camera_model',
'city',
'country',
'location',
'original_name',
'state',
'title',
):
if item == 'location':
mask = 'default'
@ -245,8 +269,10 @@ class Collection(object):
if this_part:
# Check if all masks are substituted
if True in [c in this_part for c in '{}']:
self.logger.error(f'Format path part invalid: \
{this_part}')
self.logger.error(
f'Format path part invalid: \
{this_part}'
)
sys.exit(1)
path.append(this_part.strip())
@ -255,7 +281,7 @@ class Collection(object):
# Else we continue for fallbacks
if path == []:
path = [ metadata['filename'] ]
path = [metadata['filename']]
elif len(path[-1]) == 0 or re.match(r'^\..*', path[-1]):
path[-1] = metadata['filename']
@ -270,15 +296,16 @@ class Collection(object):
return None
def _checkcomp(self, dest_path, src_checksum):
"""Check file.
"""
"""Check file."""
if self.dry_run:
return True
dest_checksum = utils.checksum(dest_path)
if dest_checksum != src_checksum:
self.logger.info(f'Source checksum and destination checksum are not the same')
self.logger.info(
f'Source checksum and destination checksum are not the same'
)
return False
return True
@ -303,7 +330,7 @@ class Collection(object):
if self.album_from_folder:
media.set_album_from_folder()
updated = True
if media.metadata['original_name'] in (False, ''):
if media.metadata['original_name'] in (False, ''):
media.set_value('original_name', self.filename)
updated = True
if self.album_from_folder:
@ -332,8 +359,7 @@ class Collection(object):
checksum = utils.checksum(dest_path)
media.metadata['checksum'] = checksum
media.metadata['file_path'] = os.path.relpath(dest_path,
self.root)
media.metadata['file_path'] = os.path.relpath(dest_path, self.root)
self._add_db_data(media.metadata)
if self.mode == 'move':
# Delete file path entry in db when file is moved inside collection
@ -367,7 +393,7 @@ class Collection(object):
dry_run = self.dry_run
# check for collisions
if(src_path == dest_path):
if src_path == dest_path:
self.logger.info(f'File {dest_path} already sorted')
return None
elif dest_path.is_dir():
@ -377,17 +403,21 @@ class Collection(object):
self.logger.warning(f'File {dest_path} already exist')
if remove_duplicates:
if filecmp.cmp(src_path, dest_path):
self.logger.info(f'File in source and destination are identical. Duplicate will be ignored.')
if(mode == 'move'):
self.logger.info(
f'File in source and destination are identical. Duplicate will be ignored.'
)
if mode == 'move':
self.remove(src_path)
return None
else: # name is same, but file is different
self.logger.warning(f'File in source and destination are different.')
self.logger.warning(
f'File in source and destination are different.'
)
return False
else:
return False
else:
if(mode == 'move'):
if mode == 'move':
if not dry_run:
# Move the processed file into the destination directory
shutil.move(src_path, dest_path)
@ -411,7 +441,7 @@ class Collection(object):
# Add appendix to the name
suffix = dest_path.suffix
if n > 1:
stem = dest_path.stem.rsplit('_' + str(n-1))[0]
stem = dest_path.stem.rsplit('_' + str(n - 1))[0]
else:
stem = dest_path.stem
dest_path = dest_path.parent / (stem + '_' + str(n) + suffix)
@ -447,7 +477,7 @@ class Collection(object):
if part[0] in '-_ .':
if n > 0:
# move the separator to previous item
parts[n-1] = parts[n-1] + part[0]
parts[n - 1] = parts[n - 1] + part[0]
items.append(part[1:])
else:
items.append(part)
@ -490,7 +520,8 @@ class Collection(object):
:returns: Path file_path, Path subdirs
"""
for path0 in path.glob(glob):
if path0.is_dir(): continue
if path0.is_dir():
continue
else:
file_path = path0
parts = file_path.parts
@ -501,10 +532,12 @@ class Collection(object):
level = len(subdirs.parts)
if subdirs.parts != ():
if subdirs.parts[0] == '.ordigi': continue
if subdirs.parts[0] == '.ordigi':
continue
if maxlevel is not None:
if level > maxlevel: continue
if level > maxlevel:
continue
matched = False
for exclude in self.exclude:
@ -512,12 +545,13 @@ class Collection(object):
matched = True
break
if matched: continue
if matched:
continue
if (
extensions == set()
or PurePath(file_path).suffix.lower() in extensions
):
extensions == set()
or PurePath(file_path).suffix.lower() in extensions
):
# return file_path and subdir
yield file_path
@ -529,15 +563,17 @@ class Collection(object):
"""
parts = directory_path.relative_to(self.root).parts
for i, part in enumerate(parts):
dir_path = self.root / Path(*parts[0:i+1])
dir_path = self.root / Path(*parts[0 : i + 1])
if dir_path.is_file():
self.logger.warning(f'Target directory {dir_path} is a file')
# Rename the src_file
if self.interactive:
prompt = [
inquirer.Text('file_path', message="New name for"\
f"'{dir_path.name}' file"),
]
inquirer.Text(
'file_path',
message="New name for" f"'{dir_path.name}' file",
),
]
answers = inquirer.prompt(prompt, theme=self.theme)
file_path = dir_path.parent / answers['file_path']
else:
@ -569,11 +605,12 @@ class Collection(object):
return path
def set_utime_from_metadata(self, date_media, file_path):
""" Set the modification time on the file based on the file name.
"""
"""Set the modification time on the file based on the file name."""
# Initialize date taken to what's returned from the metadata function.
os.utime(file_path, (int(datetime.now().timestamp()), int(date_media.timestamp())))
os.utime(
file_path, (int(datetime.now().timestamp()), int(date_media.timestamp()))
)
def dedup_regex(self, path, dedup_regex, remove_duplicates=False):
# cycle throught files
@ -586,14 +623,14 @@ class Collection(object):
# Numeric date regex
if len(dedup_regex) == 0:
date_num2 = re.compile(fr'([^0-9]{d}{delim}{d}{delim}|{delim}{d}{delim}{d}[^0-9])')
date_num3 = re.compile(fr'([^0-9]{d}{delim}{d}{delim}{d}{delim}|{delim}{d}{delim}{d}{delim}{d}[^0-9])')
date_num2 = re.compile(
fr'([^0-9]{d}{delim}{d}{delim}|{delim}{d}{delim}{d}[^0-9])'
)
date_num3 = re.compile(
fr'([^0-9]{d}{delim}{d}{delim}{d}{delim}|{delim}{d}{delim}{d}{delim}{d}[^0-9])'
)
default = re.compile(r'([^-_ .]+[-_ .])')
dedup_regex = [
date_num3,
date_num2,
default
]
dedup_regex = [date_num3, date_num2, default]
conflict_file_list = []
self.src_list = [x for x in self._get_files_in_path(path, glob=self.glob)]
@ -645,13 +682,14 @@ class Collection(object):
:params: list
:return: list
"""
message="Bellow the file selection list, modify selection if needed"
message = "Bellow the file selection list, modify selection if needed"
questions = [
inquirer.Checkbox('selection',
message=message,
choices=self.src_list,
default=self.src_list,
),
inquirer.Checkbox(
'selection',
message=message,
choices=self.src_list,
default=self.src_list,
),
]
return inquirer.prompt(questions, theme=self.theme)['selection']
@ -693,12 +731,16 @@ class Collection(object):
def init(self, loc, ignore_tags=set()):
record = True
for file_path in self._get_all_files():
media = Media(file_path, self.root, ignore_tags=ignore_tags,
logger=self.logger, use_date_filename=self.use_date_filename,
use_file_dates=self.use_file_dates)
media = Media(
file_path,
self.root,
ignore_tags=ignore_tags,
logger=self.logger,
use_date_filename=self.use_date_filename,
use_file_dates=self.use_file_dates,
)
metadata = media.get_metadata(self.root, loc, self.db, self.cache)
media.metadata['file_path'] = os.path.relpath(file_path,
self.root)
media.metadata['file_path'] = os.path.relpath(file_path, self.root)
self._add_db_data(media.metadata)
self.summary.append((file_path, file_path))
@ -731,9 +773,14 @@ class Collection(object):
relpath = os.path.relpath(file_path, self.root)
# If file not in database
if relpath not in db_rows:
media = Media(file_path, self.root, ignore_tags=ignore_tags,
logger=self.logger, use_date_filename=self.use_date_filename,
use_file_dates=self.use_file_dates)
media = Media(
file_path,
self.root,
ignore_tags=ignore_tags,
logger=self.logger,
use_date_filename=self.use_date_filename,
use_file_dates=self.use_file_dates,
)
metadata = media.get_metadata(self.root, loc, self.db, self.cache)
media.metadata['file_path'] = relpath
# Check if file checksum is in invalid rows
@ -758,8 +805,7 @@ class Collection(object):
return self.summary
def sort_files(self, paths, loc, remove_duplicates=False,
ignore_tags=set()):
def sort_files(self, paths, loc, remove_duplicates=False, ignore_tags=set()):
"""
Sort files into appropriate folder
"""
@ -774,8 +820,12 @@ class Collection(object):
self.dest_list = []
path = self._check_path(path)
conflict_file_list = []
self.src_list = [x for x in self._get_files_in_path(path,
glob=self.glob, extensions=self.filter_by_ext)]
self.src_list = [
x
for x in self._get_files_in_path(
path, glob=self.glob, extensions=self.filter_by_ext
)
]
if self.interactive:
self.src_list = self._modify_selection()
print('Processing...')
@ -783,9 +833,16 @@ class Collection(object):
# Get medias and paths
for src_path in self.src_list:
# Process files
media = Media(src_path, path, self.album_from_folder,
ignore_tags, self.interactive, self.logger,
self.use_date_filename, self.use_file_dates)
media = Media(
src_path,
path,
self.album_from_folder,
ignore_tags,
self.interactive,
self.logger,
self.use_date_filename,
self.use_file_dates,
)
metadata = media.get_metadata(self.root, loc, self.db, self.cache)
# Get the destination path according to metadata
relpath = Path(self.get_path(metadata))
@ -805,7 +862,6 @@ class Collection(object):
result = self.sort_file(src_path, dest_path, remove_duplicates)
record = False
if result is True:
record = self._record_file(src_path, dest_path, media)
@ -836,8 +892,9 @@ class Collection(object):
"""
:returns: iter
"""
for src_path in self._get_files_in_path(path, glob=self.glob,
extensions=self.filter_by_ext):
for src_path in self._get_files_in_path(
path, glob=self.glob, extensions=self.filter_by_ext
):
dirname = src_path.parent.name
if dirname.find('similar_to') == 0:
@ -857,7 +914,7 @@ class Collection(object):
result = True
path = self._check_path(path)
images = set([ x for x in self._get_images(path) ])
images = set([x for x in self._get_images(path)])
i = Images(images, logger=self.logger)
nb_row_ini = self.db.len('metadata')
for image in images:
@ -920,8 +977,9 @@ class Collection(object):
dirnames = set()
moved_files = set()
nb_row_ini = self.db.len('metadata')
for src_path in self._get_files_in_path(path, glob=self.glob,
extensions=self.filter_by_ext):
for src_path in self._get_files_in_path(
path, glob=self.glob, extensions=self.filter_by_ext
):
dirname = src_path.parent.name
if dirname.find('similar_to') == 0:
dirnames.add(src_path.parent)
@ -954,5 +1012,3 @@ class Collection(object):
result = self.check_db()
return self.summary, result

View File

@ -60,7 +60,7 @@ class Config:
options = {}
geocoder = self.get_option('geocoder', 'Geolocation')
if geocoder and geocoder in ('Nominatim', ):
if geocoder and geocoder in ('Nominatim',):
options['geocoder'] = geocoder
else:
options['geocoder'] = constants.default_geocoder
@ -89,4 +89,3 @@ class Config:
options['exclude'] = [value for key, value in self.conf.items('Exclusions')]
return options

View File

@ -7,7 +7,7 @@ from os import environ, path
#: If True, debug messages will be printed.
debug = False
#Ordigi settings directory.
# Ordigi settings directory.
if 'XDG_CONFIG_HOME' in environ:
confighome = environ['XDG_CONFIG_HOME']
elif 'APPDATA' in environ:

View File

@ -1,4 +1,3 @@
from datetime import datetime
import json
import os
@ -29,11 +28,7 @@ class Sqlite:
pass
self.db_type = 'SQLite format 3'
self.types = {
'text': (str, datetime),
'integer': (int,),
'real': (float,)
}
self.types = {'text': (str, datetime), 'integer': (int,), 'real': (float,)}
self.filename = Path(db_dir, target_dir.name + '.db')
self.con = sqlite3.connect(self.filename)
@ -53,10 +48,10 @@ class Sqlite:
'DateModified': 'text',
'CameraMake': 'text',
'CameraModel': 'text',
'OriginalName':'text',
'OriginalName': 'text',
'SrcPath': 'text',
'Subdirs': 'text',
'Filename': 'text'
'Filename': 'text',
}
location_header = {
@ -67,18 +62,15 @@ class Sqlite:
'City': 'text',
'State': 'text',
'Country': 'text',
'Default': 'text'
'Default': 'text',
}
self.tables = {
'metadata': {
'header': metadata_header,
'primary_keys': ('FilePath',)
},
'metadata': {'header': metadata_header, 'primary_keys': ('FilePath',)},
'location': {
'header': location_header,
'primary_keys': ('Latitude', 'Longitude')
}
'primary_keys': ('Latitude', 'Longitude'),
},
}
self.primary_metadata_keys = self.tables['metadata']['primary_keys']
@ -91,7 +83,7 @@ class Sqlite:
def is_Sqlite3(self, filename):
if not os.path.isfile(filename):
return False
if os.path.getsize(filename) < 100: # SQLite database file header is 100 bytes
if os.path.getsize(filename) < 100: # SQLite database file header is 100 bytes
return False
with open(filename, 'rb') as fd:
@ -104,7 +96,9 @@ class Sqlite:
try:
# get the count of tables with the name
self.cur.execute(f"select count(name) from sqlite_master where type='table' and name='{table}'")
self.cur.execute(
f"select count(name) from sqlite_master where type='table' and name='{table}'"
)
except sqlite3.DatabaseError as e:
# raise type(e)(e.message + ' :{self.filename} %s' % arg1)
raise sqlite3.DatabaseError(f"{self.filename} is not valid database")
@ -156,8 +150,10 @@ class Sqlite:
"""
header = self.tables[table]['header']
if len(row_data) != len(header):
raise ValueError(f'''Table {table} length mismatch: row_data
{row_data}, header {header}''')
raise ValueError(
f'''Table {table} length mismatch: row_data
{row_data}, header {header}'''
)
columns = ', '.join(row_data.keys())
placeholders = ', '.join('?' * len(row_data))
@ -204,8 +200,9 @@ class Sqlite:
:returns: bool
"""
if not self.tables[table]['header']:
result = self.build_table(table, row_data,
self.tables[table]['primary_keys'])
result = self.build_table(
table, row_data, self.tables[table]['primary_keys']
)
if not result:
return False
@ -236,8 +233,7 @@ class Sqlite:
def _get_table(self, table):
self.cur.execute(f'SELECT * FROM {table}').fetchall()
def get_location_nearby(self, latitude, longitude, Column,
threshold_m=3000):
def get_location_nearby(self, latitude, longitude, Column, threshold_m=3000):
"""Find a name for a location in the database.
:param float latitude: Latitude of the location.
@ -250,10 +246,9 @@ class Sqlite:
value = None
self.cur.execute('SELECT * FROM location')
for row in self.cur:
distance = distance_between_two_points(latitude, longitude,
row[0], row[1])
distance = distance_between_two_points(latitude, longitude, row[0], row[1])
# Use if closer then threshold_km reuse lookup
if(distance < shorter_distance and distance <= threshold_m):
if distance < shorter_distance and distance <= threshold_m:
shorter_distance = distance
value = row[Column]

View File

@ -28,14 +28,14 @@ def exiftool_is_running():
@atexit.register
def terminate_exiftool():
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool """
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""
for proc in EXIFTOOL_PROCESSES:
proc._stop_proc()
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
"""return path of exiftool, cache result"""
exiftool_path = shutil.which("exiftool")
if exiftool_path:
return exiftool_path.rstrip()
@ -51,7 +51,7 @@ class _ExifToolProc:
Creates a singleton object"""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
@ -77,7 +77,7 @@ class _ExifToolProc:
@property
def process(self):
""" return the exiftool subprocess """
"""return the exiftool subprocess"""
if self._process_running:
return self._process
else:
@ -86,16 +86,16 @@ class _ExifToolProc:
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def exiftool(self):
""" return path to exiftool process """
"""return path to exiftool process"""
return self._exiftool
def _start_proc(self):
""" start exiftool in batch mode """
"""start exiftool in batch mode"""
if self._process_running:
self.logger.warning("exiftool already running: {self._process}")
@ -123,7 +123,7 @@ class _ExifToolProc:
EXIFTOOL_PROCESSES.append(self)
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
"""stop the exiftool process if it's running, otherwise, do nothing"""
if not self._process_running:
return
@ -146,9 +146,16 @@ class _ExifToolProc:
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
"""Basic exiftool interface for reading and writing EXIF tags"""
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None, logger=logging.getLogger()):
def __init__(
self,
filepath,
exiftool=None,
overwrite=True,
flags=None,
logger=logging.getLogger(),
):
"""Create ExifTool object
Args:
@ -318,12 +325,12 @@ class ExifTool:
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def version(self):
""" returns exiftool version """
"""returns exiftool version"""
ver, _, _ = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
@ -361,12 +368,12 @@ class ExifTool:
return exifdict
def json(self):
""" returns JSON string containing all EXIF tags and values from exiftool """
"""returns JSON string containing all EXIF tags and values from exiftool"""
json, _, _ = self.run_commands("-json")
return json
def _read_exif(self):
""" read exif data from file """
"""read exif data from file"""
data = self.asdict()
self.data = {k: v for k, v in data.items()}
@ -387,18 +394,19 @@ class ExifTool:
class ExifToolCaching(ExifTool):
""" Basic exiftool interface for reading and writing EXIF tags, with caching.
Use this only when you know the file's EXIF data will not be changed by any external process.
"""Basic exiftool interface for reading and writing EXIF tags, with caching.
Use this only when you know the file's EXIF data will not be changed by any external process.
Creates a singleton cached ExifTool instance """
Creates a singleton cached ExifTool instance"""
_singletons = {}
def __new__(cls, filepath, exiftool=None, logger=logging.getLogger()):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if filepath not in cls._singletons:
cls._singletons[filepath] = _ExifToolCaching(filepath,
exiftool=exiftool, logger=logger)
cls._singletons[filepath] = _ExifToolCaching(
filepath, exiftool=exiftool, logger=logger
)
return cls._singletons[filepath]
@ -415,8 +423,9 @@ class _ExifToolCaching(ExifTool):
"""
self._json_cache = None
self._asdict_cache = {}
super().__init__(filepath, exiftool=exiftool, overwrite=False,
flags=None, logger=logger)
super().__init__(
filepath, exiftool=exiftool, overwrite=False, flags=None, logger=logger
)
def run_commands(self, *commands, no_file=False):
if commands[0] not in ["-json", "-ver"]:
@ -453,7 +462,6 @@ class _ExifToolCaching(ExifTool):
return self._asdict_cache[tag_groups][normalized]
def flush_cache(self):
""" Clear cached data so that calls to json or asdict return fresh data """
"""Clear cached data so that calls to json or asdict return fresh data"""
self._json_cache = None
self._asdict_cache = {}

View File

@ -1,4 +1,3 @@
from os import path
import geopy
@ -13,7 +12,12 @@ __KEY__ = None
class GeoLocation:
"""Look up geolocation information for media objects."""
def __init__(self, geocoder='Nominatim', prefer_english_names=False, timeout=options.default_timeout):
def __init__(
self,
geocoder='Nominatim',
prefer_english_names=False,
timeout=options.default_timeout,
):
self.geocoder = geocoder
self.prefer_english_names = prefer_english_names
self.timeout = timeout
@ -21,10 +25,10 @@ class GeoLocation:
def coordinates_by_name(self, name, db, timeout=options.default_timeout):
# Try to get cached location first
cached_coordinates = db.get_location_coordinates(name)
if(cached_coordinates is not None):
if cached_coordinates is not None:
return {
'latitude': cached_coordinates[0],
'longitude': cached_coordinates[1]
'longitude': cached_coordinates[1],
}
# If the name is not cached then we go ahead with an API lookup
@ -35,22 +39,24 @@ class GeoLocation:
if geolocation_info is not None:
return {
'latitude': geolocation_info.latitude,
'longitude': geolocation_info.longitude
'longitude': geolocation_info.longitude,
}
else:
raise NameError(geocoder)
return None
def place_name(self, lat, lon, logger=logging.getLogger(), timeout=options.default_timeout):
def place_name(
self, lat, lon, logger=logging.getLogger(), timeout=options.default_timeout
):
lookup_place_name_default = {'default': None}
if(lat is None or lon is None):
if lat is None or lon is None:
return lookup_place_name_default
# Convert lat/lon to floats
if(not isinstance(lat, float)):
if not isinstance(lat, float):
lat = float(lat)
if(not isinstance(lon, float)):
if not isinstance(lon, float):
lon = float(lon)
lookup_place_name = {}
@ -60,33 +66,34 @@ class GeoLocation:
else:
raise NameError(geocoder)
if(geolocation_info is not None and 'address' in geolocation_info):
if geolocation_info is not None and 'address' in geolocation_info:
address = geolocation_info['address']
# gh-386 adds support for town
# taking precedence after city for backwards compatability
for loc in ['city', 'town', 'village', 'state', 'country']:
if(loc in address):
if loc in address:
lookup_place_name[loc] = address[loc]
# In many cases the desired key is not available so we
# set the most specific as the default.
if('default' not in lookup_place_name):
if 'default' not in lookup_place_name:
lookup_place_name['default'] = address[loc]
if('default' not in lookup_place_name):
if 'default' not in lookup_place_name:
lookup_place_name = lookup_place_name_default
return lookup_place_name
def lookup_osm(self, lat, lon, logger=logging.getLogger(), timeout=options.default_timeout):
def lookup_osm(
self, lat, lon, logger=logging.getLogger(), timeout=options.default_timeout
):
try:
locator = Nominatim(user_agent='myGeocoder', timeout=timeout)
coords = (lat, lon)
if(self.prefer_english_names):
lang='en'
if self.prefer_english_names:
lang = 'en'
else:
lang='local'
lang = 'local'
locator_reverse = locator.reverse(coords, language=lang)
if locator_reverse is not None:
return locator_reverse.raw
@ -99,5 +106,3 @@ class GeoLocation:
except (TypeError, ValueError) as e:
logger.error(e)
return None

View File

@ -18,6 +18,7 @@ import time
PYHEIF = False
try:
from pyheif_pillow_opener import register_heif_opener
PYHEIF = True
# Allow to open HEIF/HEIC image from pillow
register_heif_opener()
@ -25,8 +26,7 @@ except ImportError as e:
logging.info(e)
class Image():
class Image:
def __init__(self, img_path, hash_size=8):
self.img_path = img_path
@ -55,7 +55,7 @@ class Image():
except (IOError, UnidentifiedImageError):
return False
if(im.format is None):
if im.format is None:
return False
return True
@ -68,7 +68,7 @@ class Image():
return None
class Images():
class Images:
"""A image object.
@ -76,7 +76,18 @@ class Images():
"""
#: Valid extensions for image files.
extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
extensions = (
'arw',
'cr2',
'dng',
'gif',
'heic',
'jpeg',
'jpg',
'nef',
'png',
'rw2',
)
def __init__(self, images=set(), hash_size=8, logger=logging.getLogger()):
@ -104,7 +115,11 @@ class Images():
duplicates = []
for temp_hash in get_images_hashes():
if temp_hash in hashes:
self.logger.info("Duplicate {} \nfound for image {}\n".format(img_path, hashes[temp_hash]))
self.logger.info(
"Duplicate {} \nfound for image {}\n".format(
img_path, hashes[temp_hash]
)
)
duplicates.append(img_path)
else:
hashes[temp_hash] = img_path
@ -121,7 +136,7 @@ class Images():
def remove_duplicates_interactive(self, duplicates):
if len(duplicates) != 0:
answer = input(f"Do you want to delete these {duplicates} images? Y/n: ")
if(answer.strip().lower() == 'y'):
if answer.strip().lower() == 'y':
self.remove_duplicates(duplicates)
self.logger.info(f'{duplicate} deleted successfully!')
else:
@ -131,7 +146,7 @@ class Images():
return np.count_nonzero(hash1 != hash2)
def similarity(self, img_diff):
threshold_img = img_diff / (self.hash_size**2)
threshold_img = img_diff / (self.hash_size ** 2)
similarity_img = round((1 - threshold_img) * 100)
return similarity_img
@ -148,8 +163,8 @@ class Images():
self.logger.info(f'Finding similar images to {image.img_path}')
threshold = 1 - similarity/100
diff_limit = int(threshold*(self.hash_size**2))
threshold = 1 - similarity / 100
diff_limit = int(threshold * (self.hash_size ** 2))
for img in self.images:
if not img.img_path.is_file():
@ -164,7 +179,7 @@ class Images():
img_diff = self.diff(hash1, hash2)
if img_diff <= diff_limit:
similarity_img = self.similarity(img_diff)
self.logger.info(f'{img.img_path} image found {similarity_img}% similar to {image}')
self.logger.info(
f'{img.img_path} image found {similarity_img}% similar to {image}'
)
yield img.img_path

View File

@ -1,5 +1,6 @@
import logging
def get_logger(verbose, debug):
if debug:
level = logging.DEBUG
@ -13,4 +14,3 @@ def get_logger(verbose, debug):
logger = logging.getLogger('ordigi')
logger.level = level
return logger

View File

@ -8,6 +8,7 @@ import mimetypes
import os
import re
import sys
# import pprint
# load modules
@ -17,17 +18,14 @@ from ordigi import utils
from ordigi import request
class Media():
class Media:
"""The media class for all media objects.
:param str file_path: The fully qualified path to the media file.
"""
d_coordinates = {
'latitude': 'latitude_ref',
'longitude': 'longitude_ref'
}
d_coordinates = {'latitude': 'latitude_ref', 'longitude': 'longitude_ref'}
PHOTO = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
AUDIO = ('m4a',)
@ -35,9 +33,17 @@ class Media():
extensions = PHOTO + AUDIO + VIDEO
def __init__(self, file_path, src_path, album_from_folder=False,
ignore_tags=set(), interactive=False, logger=logging.getLogger(),
use_date_filename=False, use_file_dates=False):
def __init__(
self,
file_path,
src_path,
album_from_folder=False,
ignore_tags=set(),
interactive=False,
logger=logging.getLogger(),
use_date_filename=False,
use_file_dates=False,
):
"""
:params: Path, Path, bool, set, bool, Logger
"""
@ -61,19 +67,16 @@ class Media():
tags_keys['date_original'] = [
'EXIF:DateTimeOriginal',
'H264:DateTimeOriginal',
'QuickTime:ContentCreateDate'
'QuickTime:ContentCreateDate',
]
tags_keys['date_created'] = [
'EXIF:CreateDate',
'QuickTime:CreationDate',
'QuickTime:CreateDate',
'QuickTime:CreationDate-und-US',
'QuickTime:MediaCreateDate'
]
tags_keys['date_modified'] = [
'File:FileModifyDate',
'QuickTime:ModifyDate'
'QuickTime:MediaCreateDate',
]
tags_keys['date_modified'] = ['File:FileModifyDate', 'QuickTime:ModifyDate']
tags_keys['camera_make'] = ['EXIF:Make', 'QuickTime:Make']
tags_keys['camera_model'] = ['EXIF:Model', 'QuickTime:Model']
tags_keys['album'] = ['XMP-xmpDM:Album', 'XMP:Album']
@ -82,13 +85,13 @@ class Media():
'EXIF:GPSLatitude',
'XMP:GPSLatitude',
# 'QuickTime:GPSLatitude',
'Composite:GPSLatitude'
'Composite:GPSLatitude',
]
tags_keys['longitude'] = [
'EXIF:GPSLongitude',
'XMP:GPSLongitude',
# 'QuickTime:GPSLongitude',
'Composite:GPSLongitude'
'Composite:GPSLongitude',
]
tags_keys['latitude_ref'] = ['EXIF:GPSLatitudeRef']
tags_keys['longitude_ref'] = ['EXIF:GPSLongitudeRef']
@ -100,7 +103,7 @@ class Media():
for key, tags in tags_keys.items():
for n, tag in enumerate(tags):
if re.match(tag_regex, tag):
del(tags_keys[key][n])
del tags_keys[key][n]
return tags_keys
@ -119,7 +122,7 @@ class Media():
:returns: str or None
"""
mimetype = mimetypes.guess_type(self.file_path)
if(mimetype is None):
if mimetype is None:
return None
return mimetype[0]
@ -143,7 +146,7 @@ class Media():
"""
if self.exif_metadata is None:
return None
if(tag not in self.exif_metadata):
if tag not in self.exif_metadata:
return None
return self.exif_metadata[tag]
@ -161,10 +164,10 @@ class Media():
try:
# correct nasty formated date
regex = re.compile(r'(\d{4}):(\d{2}):(\d{2})')
if(re.match(regex , value) is not None): # noqa
value = re.sub(regex , r'\g<1>-\g<2>-\g<3>', value)
if re.match(regex, value) is not None: # noqa
value = re.sub(regex, r'\g<1>-\g<2>-\g<3>', value)
return parse(value)
except BaseException or dateutil.parser._parser.ParserError as e:
except BaseException or dateutil.parser._parser.ParserError as e:
self.logger.warning(e.args, value)
return None
@ -207,15 +210,16 @@ class Media():
def _get_date_media_interactive(self, choices, default):
print(f"Date conflict for file: {self.file_path}")
choices_list = [
inquirer.List('date_list',
inquirer.List(
'date_list',
message=f"Choice appropriate original date",
choices=choices,
default=default
default=default,
),
]
prompt = [
inquirer.Text('date_custom', message="date"),
]
]
answers = inquirer.prompt(choices_list, theme=self.theme)
if not answers['date_list']:
@ -243,8 +247,10 @@ class Media():
date_created = self.metadata['date_created']
date_modified = self.metadata['date_modified']
if self.metadata['date_original']:
if (date_filename and date_filename != date_original):
self.logger.warning(f"{basename} time mark is different from {date_original}")
if date_filename and date_filename != date_original:
self.logger.warning(
f"{basename} time mark is different from {date_original}"
)
if self.interactive:
# Ask for keep date taken, filename time, or neither
choices = [
@ -260,9 +266,13 @@ class Media():
self.logger.warning(f"could not find original date for {self.file_path}")
if self.use_date_filename and date_filename:
self.logger.info(f"use date from filename:{date_filename} for {self.file_path}")
self.logger.info(
f"use date from filename:{date_filename} for {self.file_path}"
)
if date_created and date_filename > date_created:
self.logger.warning(f"{basename} time mark is more recent than {date_created}")
self.logger.warning(
f"{basename} time mark is more recent than {date_created}"
)
if self.interactive:
choices = [
(f"date filename:'{date_filename}'", date_filename),
@ -276,16 +286,19 @@ class Media():
elif self.use_file_dates:
if date_created:
self.logger.warning(f"use date created:{date_created} for {self.file_path}")
self.logger.warning(
f"use date created:{date_created} for {self.file_path}"
)
return date_created
elif date_modified:
self.logger.warning(f"use date modified:{date_modified} for {self.file_path}")
self.logger.warning(
f"use date modified:{date_modified} for {self.file_path}"
)
return date_modified
elif self.interactive:
choices = []
if date_filename:
choices.append((f"date filename:'{date_filename}'",
date_filename))
choices.append((f"date filename:'{date_filename}'", date_filename))
if date_created:
choices.append((f"date created:'{date_created}'", date_created))
if date_modified:
@ -296,24 +309,27 @@ class Media():
def get_exif_metadata(self):
# Get metadata from exiftool.
self.exif_metadata = ExifToolCaching(self.file_path, logger=self.logger).asdict()
self.exif_metadata = ExifToolCaching(
self.file_path, logger=self.logger
).asdict()
def _set_album(self, album, folder):
print(f"Metadata conflict for file: {self.file_path}")
choices_list = [
inquirer.List('album',
inquirer.List(
'album',
message=f"Exif album is already set to {album}, choices",
choices=[
(f"album:'{album}'", album),
(f"folder:'{folder}'", folder),
("custom", None),
],
default=f'{album}'
],
default=f'{album}',
),
]
prompt = [
inquirer.Text('custom', message="album"),
]
]
answers = inquirer.prompt(choices_list, theme=self.theme)
if not answers['album']:
@ -344,8 +360,12 @@ class Media():
if db_checksum and db_checksum != file_checksum:
self.logger.error(f'{self.file_path} checksum has changed')
self.logger.error('(modified or corrupted file).')
self.logger.error(f'file_checksum={file_checksum},\ndb_checksum={db_checksum}')
self.logger.info('Use --reset-cache, check database integrity or try to restore the file')
self.logger.error(
f'file_checksum={file_checksum},\ndb_checksum={db_checksum}'
)
self.logger.info(
'Use --reset-cache, check database integrity or try to restore the file'
)
# We d'ont want to silently ignore or correct this without
# resetting the cache as is could be due to file corruption
sys.exit(1)
@ -354,8 +374,13 @@ class Media():
# Get metadata from db
formated_data = None
for key in self.tags_keys:
if key in ('latitude', 'longitude', 'latitude_ref',
'longitude_ref', 'file_path'):
if key in (
'latitude',
'longitude',
'latitude_ref',
'longitude_ref',
'file_path',
):
continue
label = utils.snake2camel(key)
value = db.get_metadata_data(relpath, label)
@ -372,7 +397,9 @@ class Media():
location_id = db.get_metadata_data(relpath, 'LocationId')
else:
self.metadata['src_path'] = str(self.src_path)
self.metadata['subdirs'] = str(self.file_path.relative_to(self.src_path).parent)
self.metadata['subdirs'] = str(
self.file_path.relative_to(self.src_path).parent
)
self.metadata['filename'] = self.file_path.name
# Get metadata from exif
@ -400,30 +427,38 @@ class Media():
self.metadata[key] = formated_data
self.metadata['date_media'] = self.get_date_media()
self.metadata['date_media'] = self.get_date_media()
self.metadata['location_id'] = location_id
loc_keys = ('latitude', 'longitude', 'latitude_ref', 'longitude_ref', 'city', 'state', 'country', 'default')
loc_keys = (
'latitude',
'longitude',
'latitude_ref',
'longitude_ref',
'city',
'state',
'country',
'default',
)
if location_id:
for key in loc_keys:
# use str to convert non string format data like latitude and
# longitude
self.metadata[key] = str(db.get_location_data(location_id,
utils.snake2camel(key)))
self.metadata[key] = str(
db.get_location_data(location_id, utils.snake2camel(key))
)
elif loc:
for key in 'latitude', 'longitude', 'latitude_ref', 'longitude_ref':
self.metadata[key] = None
place_name = loc.place_name(
self.metadata['latitude'],
self.metadata['longitude'],
self.logger
self.metadata['latitude'], self.metadata['longitude'], self.logger
)
for key in ('city', 'state', 'country', 'default'):
# mask = 'city'
# place_name = {'default': u'Sunnyvale', 'city-random': u'Sunnyvale'}
if(key in place_name):
if key in place_name:
self.metadata[key] = place_name[key]
else:
self.metadata[key] = None
@ -432,7 +467,6 @@ class Media():
for key in loc_keys:
self.metadata[key] = None
if self.album_from_folder:
album = self.metadata['album']
folder = self.file_path.parent.name
@ -463,9 +497,10 @@ class Media():
return False
@classmethod
def get_class_by_file(cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()):
"""Static method to get a media object by file.
"""
def get_class_by_file(
cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()
):
"""Static method to get a media object by file."""
if not os.path.isfile(_file):
return None
@ -473,7 +508,7 @@ class Media():
if len(extension) > 0:
for i in classes:
if(extension in i.extensions):
if extension in i.extensions:
return i(_file, ignore_tags=ignore_tags, logger=logger)
return Media(_file, logger, ignore_tags=ignore_tags, logger=logger)
@ -491,7 +526,7 @@ class Media():
:param datetime time: datetime object of when the photo was taken
:returns: bool
"""
if(time is None):
if time is None:
return False
formatted_time = time.strftime('%Y:%m:%d %H:%M:%S')
@ -513,7 +548,7 @@ class Media():
status.append(self.set_value('latitude', latitude))
if self.metadata['longitude_ref']:
if self.metadata['longitude_ref']:
longitude = abs(longitude)
if longitude > 0:
status.append(self.set_value('latitude_ref', 'E'))
@ -536,8 +571,7 @@ class Media():
def get_all_subclasses(cls=None):
"""Module method to get all subclasses of Media.
"""
"""Module method to get all subclasses of Media."""
subclasses = set()
this_class = Media
@ -559,12 +593,12 @@ def get_media_class(_file, ignore_tags=set(), logger=logging.getLogger()):
logger.error(f'Could not find {_file}')
return False
media = Media.get_class_by_file(_file, get_all_subclasses(),
ignore_tags=set(), logger=logger)
media = Media.get_class_by_file(
_file, get_all_subclasses(), ignore_tags=set(), logger=logger
)
if not media:
logger.warning(f'File{_file} is not supported')
logger.error(f'File {_file} can\'t be imported')
return False
return media

View File

@ -2,7 +2,6 @@ from tabulate import tabulate
class Summary(object):
def __init__(self):
self.records = []
self.success = 0
@ -31,9 +30,9 @@ class Summary(object):
headers = ["Metric", "Count"]
result = [
["Success", self.success],
["Error", self.error],
]
["Success", self.success],
["Error", self.error],
]
print()
print('Summary:')

View File

@ -1,4 +1,3 @@
from math import radians, cos, sqrt
from datetime import datetime
import hashlib
@ -30,16 +29,14 @@ def distance_between_two_points(lat1, lon1, lat2, lon2):
# As threshold is quite small use simple math
# From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # noqa
# convert decimal degrees to radians
lat1, lon1, lat2, lon2 = list(map(
radians,
[lat1, lon1, lat2, lon2]
))
lat1, lon1, lat2, lon2 = list(map(radians, [lat1, lon1, lat2, lon2]))
r = 6371000 # radius of the earth in m
x = (lon2 - lon1) * cos(0.5 * (lat2 + lat1))
y = lat2 - lat1
return r * sqrt(x * x + y * y)
def get_date_regex(string, user_regex=None):
if user_regex is not None:
matches = re.findall(user_regex, string)
@ -48,15 +45,19 @@ def get_date_regex(string, user_regex=None):
# regex to match date format type %Y%m%d, %y%m%d, %d%m%Y,
# etc...
'a': re.compile(
r'.*[_-]?(?P<year>\d{4})[_-]?(?P<month>\d{2})[_-]?(?P<day>\d{2})[_-]?(?P<hour>\d{2})[_-]?(?P<minute>\d{2})[_-]?(?P<second>\d{2})'),
'b': re.compile (
r'[-_./](?P<year>\d{4})[-_.]?(?P<month>\d{2})[-_.]?(?P<day>\d{2})[-_./]'),
r'.*[_-]?(?P<year>\d{4})[_-]?(?P<month>\d{2})[_-]?(?P<day>\d{2})[_-]?(?P<hour>\d{2})[_-]?(?P<minute>\d{2})[_-]?(?P<second>\d{2})'
),
'b': re.compile(
r'[-_./](?P<year>\d{4})[-_.]?(?P<month>\d{2})[-_.]?(?P<day>\d{2})[-_./]'
),
# not very accurate
'c': re.compile (
r'[-_./](?P<year>\d{2})[-_.]?(?P<month>\d{2})[-_.]?(?P<day>\d{2})[-_./]'),
'd': re.compile (
r'[-_./](?P<day>\d{2})[-_.](?P<month>\d{2})[-_.](?P<year>\d{4})[-_./]')
}
'c': re.compile(
r'[-_./](?P<year>\d{2})[-_.]?(?P<month>\d{2})[-_.]?(?P<day>\d{2})[-_./]'
),
'd': re.compile(
r'[-_./](?P<day>\d{2})[-_.](?P<month>\d{2})[-_.](?P<year>\d{4})[-_./]'
),
}
for i, rx in regex.items():
yield i, rx
@ -104,10 +105,12 @@ def get_date_from_string(string, user_regex=None):
# Conversion functions
# source:https://rodic.fr/blog/camelcase-and-snake_case-strings-conversion-with-python/
def snake2camel(name):
return re.sub(r'(?:^|_)([a-z])', lambda x: x.group(1).upper(), name)
def camel2snake(name):
return name[0].lower() + re.sub(r'(?!^)[A-Z]', lambda x: '_' + x.group(0).lower(), name[1:])
return name[0].lower() + re.sub(
r'(?!^)[A-Z]', lambda x: '_' + x.group(0).lower(), name[1:]
)