Ability to retrieve metadata from Sqlite database and fixes
This commit is contained in:
parent
86d88b72c8
commit
8e8afe9a89
|
@ -3,9 +3,10 @@ General file system methods.
|
||||||
"""
|
"""
|
||||||
from builtins import object
|
from builtins import object
|
||||||
|
|
||||||
|
from copy import copy
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import filecmp
|
import filecmp
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
import hashlib
|
|
||||||
import inquirer
|
import inquirer
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -13,7 +14,6 @@ from pathlib import Path, PurePath
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from ordigi import media
|
from ordigi import media
|
||||||
from ordigi.database import Sqlite
|
from ordigi.database import Sqlite
|
||||||
|
@ -21,7 +21,7 @@ from ordigi.media import Media, get_all_subclasses
|
||||||
from ordigi.images import Image, Images
|
from ordigi.images import Image, Images
|
||||||
from ordigi import request
|
from ordigi import request
|
||||||
from ordigi.summary import Summary
|
from ordigi.summary import Summary
|
||||||
from ordigi.utils import get_date_regex, camel2snake
|
from ordigi import utils
|
||||||
|
|
||||||
|
|
||||||
class Collection(object):
|
class Collection(object):
|
||||||
|
@ -35,7 +35,7 @@ class Collection(object):
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
self.root = Path(root).expanduser().absolute()
|
self.root = Path(root).expanduser().absolute()
|
||||||
if not os.path.exists(self.root):
|
if not self.root.exists():
|
||||||
logger.error(f'Directory {self.root} does not exist')
|
logger.error(f'Directory {self.root} does not exist')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@ -61,6 +61,8 @@ class Collection(object):
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.max_deep = max_deep
|
self.max_deep = max_deep
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
# List to store media metadata
|
||||||
|
self.medias = []
|
||||||
self.summary = Summary()
|
self.summary = Summary()
|
||||||
self.use_date_filename = use_date_filename
|
self.use_date_filename = use_date_filename
|
||||||
self.use_file_dates = use_file_dates
|
self.use_file_dates = use_file_dates
|
||||||
|
@ -140,7 +142,7 @@ class Collection(object):
|
||||||
# select matched folders
|
# select matched folders
|
||||||
return folders[begin:end]
|
return folders[begin:end]
|
||||||
|
|
||||||
def get_part(self, item, mask, metadata, subdirs):
|
def get_part(self, item, mask, metadata):
|
||||||
"""Parse a specific folder's name given a mask and metadata.
|
"""Parse a specific folder's name given a mask and metadata.
|
||||||
|
|
||||||
:param item: Name of the item as defined in the path (i.e. date from %date)
|
:param item: Name of the item as defined in the path (i.e. date from %date)
|
||||||
|
@ -160,7 +162,7 @@ class Collection(object):
|
||||||
elif item == 'name':
|
elif item == 'name':
|
||||||
# Remove date prefix added to the name.
|
# Remove date prefix added to the name.
|
||||||
part = basename
|
part = basename
|
||||||
for i, rx in get_date_regex(basename):
|
for i, rx in utils.get_date_regex(basename):
|
||||||
part = re.sub(rx, '', part)
|
part = re.sub(rx, '', part)
|
||||||
elif item == 'date':
|
elif item == 'date':
|
||||||
date = metadata['date_media']
|
date = metadata['date_media']
|
||||||
|
@ -169,10 +171,10 @@ class Collection(object):
|
||||||
date = self._check_for_early_morning_photos(date)
|
date = self._check_for_early_morning_photos(date)
|
||||||
part = date.strftime(mask)
|
part = date.strftime(mask)
|
||||||
elif item == 'folder':
|
elif item == 'folder':
|
||||||
part = os.path.basename(subdirs)
|
part = os.path.basename(metadata['subdirs'])
|
||||||
|
|
||||||
elif item == 'folders':
|
elif item == 'folders':
|
||||||
folders = subdirs.parts
|
folders = Path(metadata['subdirs']).parts
|
||||||
folders = self._get_folders(folders, mask)
|
folders = self._get_folders(folders, mask)
|
||||||
part = os.path.join(*folders)
|
part = os.path.join(*folders)
|
||||||
|
|
||||||
|
@ -189,14 +191,13 @@ class Collection(object):
|
||||||
|
|
||||||
return part
|
return part
|
||||||
|
|
||||||
def get_path_part(self, this_part, metadata, subdirs):
|
def get_path_part(self, this_part, metadata):
|
||||||
"""Build path part
|
"""Build path part
|
||||||
:returns: part (string)"""
|
:returns: part (string)"""
|
||||||
for item, regex in self.items.items():
|
for item, regex in self.items.items():
|
||||||
matched = re.search(regex, this_part)
|
matched = re.search(regex, this_part)
|
||||||
if matched:
|
if matched:
|
||||||
part = self.get_part(item, matched.group()[1:-1], metadata,
|
part = self.get_part(item, matched.group()[1:-1], metadata)
|
||||||
subdirs)
|
|
||||||
|
|
||||||
part = part.strip()
|
part = part.strip()
|
||||||
|
|
||||||
|
@ -215,9 +216,15 @@ class Collection(object):
|
||||||
else:
|
else:
|
||||||
this_part = re.sub(regex, part, this_part)
|
this_part = re.sub(regex, part, this_part)
|
||||||
|
|
||||||
|
# Delete separator char at the begining of the string if any:
|
||||||
|
if this_part:
|
||||||
|
regex = '[-_ .]'
|
||||||
|
if re.match(regex, this_part[0]):
|
||||||
|
this_part = this_part[1:]
|
||||||
|
|
||||||
return this_part
|
return this_part
|
||||||
|
|
||||||
def get_path(self, metadata, subdirs, whitespace_sub='_'):
|
def get_path(self, metadata, whitespace_sub='_'):
|
||||||
"""path_format: {%Y-%d-%m}/%u{city}/{album}
|
"""path_format: {%Y-%d-%m}/%u{city}/{album}
|
||||||
|
|
||||||
Returns file path.
|
Returns file path.
|
||||||
|
@ -230,7 +237,7 @@ class Collection(object):
|
||||||
for path_part in path_parts:
|
for path_part in path_parts:
|
||||||
this_parts = path_part.split('|')
|
this_parts = path_part.split('|')
|
||||||
for this_part in this_parts:
|
for this_part in this_parts:
|
||||||
this_part = self.get_path_part(this_part, metadata, subdirs)
|
this_part = self.get_path_part(this_part, metadata)
|
||||||
|
|
||||||
if this_part:
|
if this_part:
|
||||||
# Check if all masks are substituted
|
# Check if all masks are substituted
|
||||||
|
@ -244,7 +251,9 @@ class Collection(object):
|
||||||
break
|
break
|
||||||
# Else we continue for fallbacks
|
# Else we continue for fallbacks
|
||||||
|
|
||||||
if len(path[-1]) == 0 or re.match(r'^\..*', path[-1]):
|
if path == []:
|
||||||
|
path = [ metadata['filename'] ]
|
||||||
|
elif len(path[-1]) == 0 or re.match(r'^\..*', path[-1]):
|
||||||
path[-1] = metadata['filename']
|
path[-1] = metadata['filename']
|
||||||
|
|
||||||
path_string = os.path.join(*path)
|
path_string = os.path.join(*path)
|
||||||
|
@ -257,80 +266,72 @@ class Collection(object):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def checksum(self, file_path, blocksize=65536):
|
def _checkcomp(self, dest_path, src_checksum):
|
||||||
"""Create a hash value for the given file.
|
|
||||||
|
|
||||||
See http://stackoverflow.com/a/3431835/1318758.
|
|
||||||
|
|
||||||
:param str file_path: Path to the file to create a hash for.
|
|
||||||
:param int blocksize: Read blocks of this size from the file when
|
|
||||||
creating the hash.
|
|
||||||
:returns: str or None
|
|
||||||
"""
|
|
||||||
hasher = hashlib.sha256()
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
buf = f.read(blocksize)
|
|
||||||
|
|
||||||
while len(buf) > 0:
|
|
||||||
hasher.update(buf)
|
|
||||||
buf = f.read(blocksize)
|
|
||||||
return hasher.hexdigest()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def checkcomp(self, dest_path, src_checksum):
|
|
||||||
"""Check file.
|
"""Check file.
|
||||||
"""
|
"""
|
||||||
# src_checksum = self.checksum(src_path)
|
|
||||||
|
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
return src_checksum
|
return True
|
||||||
|
|
||||||
dest_checksum = self.checksum(dest_path)
|
dest_checksum = utils.checksum(dest_path)
|
||||||
|
|
||||||
if dest_checksum != src_checksum:
|
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 False
|
||||||
|
|
||||||
return src_checksum
|
return True
|
||||||
|
|
||||||
def _get_row_data(self, table, metadata):
|
def _format_row_data(self, table, metadata):
|
||||||
row_data = {}
|
row_data = {}
|
||||||
for title in self.db.tables[table]['header']:
|
for title in self.db.tables[table]['header']:
|
||||||
key = camel2snake(title)
|
key = utils.camel2snake(title)
|
||||||
|
# Convert Path type to str
|
||||||
row_data[title] = metadata[key]
|
row_data[title] = metadata[key]
|
||||||
|
|
||||||
return row_data
|
return row_data
|
||||||
|
|
||||||
def _add_db_data(self, dest_path, metadata):
|
def _add_db_data(self, dest_path, metadata):
|
||||||
loc_values = self._get_row_data('location', metadata)
|
loc_values = self._format_row_data('location', metadata)
|
||||||
metadata['location_id'] = self.db.add_row('location', loc_values)
|
metadata['location_id'] = self.db.add_row('location', loc_values)
|
||||||
|
|
||||||
row_data = self._get_row_data('metadata', metadata)
|
row_data = self._format_row_data('metadata', metadata)
|
||||||
self.db.add_row('metadata', row_data)
|
self.db.add_row('metadata', row_data)
|
||||||
|
|
||||||
def _update_exif_data(self, dest_path, media):
|
def _update_exif_data(self, dest_path, media):
|
||||||
|
updated = False
|
||||||
if self.album_from_folder:
|
if self.album_from_folder:
|
||||||
media.file_path = dest_path
|
|
||||||
media.set_album_from_folder()
|
media.set_album_from_folder()
|
||||||
|
updated = True
|
||||||
|
if media.metadata['original_name'] in (False, ''):
|
||||||
|
media.set_value('original_name', self.filename)
|
||||||
|
updated = True
|
||||||
|
if self.album_from_folder:
|
||||||
|
album = media.metadata['album']
|
||||||
|
if album and album != '':
|
||||||
|
media.set_value('album', album)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def record_file(self, src_path, dest_path, src_checksum, media):
|
def record_file(self, src_path, dest_path, media):
|
||||||
"""Check file and record the file to db"""
|
"""Check file and record the file to db"""
|
||||||
|
|
||||||
# Check if file remain the same
|
# Check if file remain the same
|
||||||
checksum = self.checkcomp(dest_path, src_checksum)
|
|
||||||
has_errors = False
|
has_errors = False
|
||||||
if checksum:
|
checksum = media.metadata['checksum']
|
||||||
|
if self._checkcomp(dest_path, checksum):
|
||||||
|
# change media file_path to dest_path
|
||||||
|
media.file_path = dest_path
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
updated = self._update_exif_data(dest_path, media)
|
updated = self._update_exif_data(dest_path, media)
|
||||||
if updated:
|
if updated:
|
||||||
dest_checksum = self.checksum(dest_path)
|
checksum = utils.checksum(dest_path)
|
||||||
|
media.metadata['checksum'] = checksum
|
||||||
|
|
||||||
media.metadata['file_path'] = os.path.relpath(dest_path,
|
media.metadata['file_path'] = os.path.relpath(dest_path,
|
||||||
self.root)
|
self.root)
|
||||||
media.metadata['checksum'] = checksum
|
|
||||||
self._add_db_data(dest_path, media.metadata)
|
self._add_db_data(dest_path, media.metadata)
|
||||||
|
|
||||||
self.summary.append((src_path, dest_path))
|
self.summary.append((src_path, dest_path))
|
||||||
|
@ -349,7 +350,13 @@ class Collection(object):
|
||||||
self.logger.info(f'remove: {file_path}')
|
self.logger.info(f'remove: {file_path}')
|
||||||
|
|
||||||
def sort_file(self, src_path, dest_path, remove_duplicates=False):
|
def sort_file(self, src_path, dest_path, remove_duplicates=False):
|
||||||
'''Copy or move file to dest_path.'''
|
'''
|
||||||
|
Copy or move file to dest_path.
|
||||||
|
Return True if success, None is no filesystem action, False if
|
||||||
|
conflicts.
|
||||||
|
:params: str, str, bool
|
||||||
|
:returns: bool or None
|
||||||
|
'''
|
||||||
|
|
||||||
mode = self.mode
|
mode = self.mode
|
||||||
dry_run = self.dry_run
|
dry_run = self.dry_run
|
||||||
|
@ -358,7 +365,10 @@ class Collection(object):
|
||||||
if(src_path == dest_path):
|
if(src_path == dest_path):
|
||||||
self.logger.info(f'File {dest_path} already sorted')
|
self.logger.info(f'File {dest_path} already sorted')
|
||||||
return None
|
return None
|
||||||
elif os.path.isfile(dest_path):
|
elif dest_path.is_dir():
|
||||||
|
self.logger.warning(f'File {dest_path} is a existing directory')
|
||||||
|
return False
|
||||||
|
elif dest_path.is_file():
|
||||||
self.logger.warning(f'File {dest_path} already exist')
|
self.logger.warning(f'File {dest_path} already exist')
|
||||||
if remove_duplicates:
|
if remove_duplicates:
|
||||||
if filecmp.cmp(src_path, dest_path):
|
if filecmp.cmp(src_path, dest_path):
|
||||||
|
@ -383,40 +393,36 @@ class Collection(object):
|
||||||
self.logger.info(f'copy: {src_path} -> {dest_path}')
|
self.logger.info(f'copy: {src_path} -> {dest_path}')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _solve_conflicts(self, conflict_file_list, media, remove_duplicates):
|
def _solve_conflicts(self, conflict_file_list, remove_duplicates):
|
||||||
has_errors = False
|
has_errors = False
|
||||||
unresolved_conflicts = []
|
unresolved_conflicts = []
|
||||||
while conflict_file_list != []:
|
while conflict_file_list != []:
|
||||||
file_paths = conflict_file_list.pop()
|
src_path, dest_path, media = conflict_file_list.pop()
|
||||||
src_path = file_paths['src_path']
|
|
||||||
src_checksum = file_paths['src_checksum']
|
|
||||||
dest_path = file_paths['dest_path']
|
|
||||||
# Try to sort the file
|
# Try to sort the file
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
# remove to conflict file list if file as be successfully copied or ignored
|
# remove to conflict file list if file as be successfully copied or ignored
|
||||||
n = 1
|
n = 1
|
||||||
while result is False and n < 100:
|
while result is False and n < 100:
|
||||||
# Add appendix to the name
|
# Add appendix to the name
|
||||||
pre, ext = os.path.splitext(dest_path)
|
suffix = dest_path.suffix
|
||||||
if n > 1:
|
if n > 1:
|
||||||
regex = '_' + str(n-1) + ext
|
stem = dest_path.stem.rsplit('_' + str(n-1))[0]
|
||||||
pre = re.split(regex, dest_path)[0]
|
else:
|
||||||
dest_path = pre + '_' + str(n) + ext
|
stem = dest_path.stem
|
||||||
# file_list[item]['dest_path'] = dest_path
|
dest_path = dest_path.parent / (stem + '_' + str(n) + suffix)
|
||||||
file_paths['dest_path'] = dest_path
|
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
n = n + 1
|
n = n + 1
|
||||||
|
|
||||||
if result is False:
|
if result is False:
|
||||||
# n > 100:
|
# n > 100:
|
||||||
unresolved_conflicts.append(file_paths)
|
unresolved_conflicts.append((src_path, dest_path, media))
|
||||||
self.logger.error(f'{self.mode}: too many append for {dest_path}...')
|
self.logger.error(f'{self.mode}: too many append for {dest_path}...')
|
||||||
self.summary.append((src_path, False))
|
self.summary.append((src_path, False))
|
||||||
has_errors = True
|
has_errors = True
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.summary, has_errors = self.record_file(src_path,
|
self.summary, has_errors = self.record_file(src_path,
|
||||||
dest_path, src_checksum, media)
|
dest_path, media)
|
||||||
|
|
||||||
if has_errors:
|
if has_errors:
|
||||||
return False
|
return False
|
||||||
|
@ -468,13 +474,8 @@ class Collection(object):
|
||||||
:param: Path
|
:param: Path
|
||||||
:return: int
|
:return: int
|
||||||
"""
|
"""
|
||||||
# if isinstance(path, str):
|
|
||||||
# # To remove trailing '/' chars
|
|
||||||
# path = Path(path)
|
|
||||||
# path = str(path)
|
|
||||||
return len(path.parts) - 1
|
return len(path.parts) - 1
|
||||||
|
|
||||||
# TODO move to utils.. or CPath..
|
|
||||||
def _get_files_in_path(self, path, glob='**/*', maxlevel=None, extensions=set()):
|
def _get_files_in_path(self, path, glob='**/*', maxlevel=None, extensions=set()):
|
||||||
"""Recursively get files which match a path and extension.
|
"""Recursively get files which match a path and extension.
|
||||||
|
|
||||||
|
@ -493,7 +494,8 @@ class Collection(object):
|
||||||
else:
|
else:
|
||||||
level = len(subdirs.parts)
|
level = len(subdirs.parts)
|
||||||
|
|
||||||
if file_path.parts[0] == '.ordigi': continue
|
if subdirs.parts != ():
|
||||||
|
if subdirs.parts[0] == '.ordigi': continue
|
||||||
|
|
||||||
if maxlevel is not None:
|
if maxlevel is not None:
|
||||||
if level > maxlevel: continue
|
if level > maxlevel: continue
|
||||||
|
@ -513,25 +515,43 @@ class Collection(object):
|
||||||
# return file_path and subdir
|
# return file_path and subdir
|
||||||
yield file_path
|
yield file_path
|
||||||
|
|
||||||
def _create_directory(self, directory_path):
|
def _create_directory(self, directory_path, path, media):
|
||||||
"""Create a directory if it does not already exist.
|
"""Create a directory if it does not already exist.
|
||||||
|
|
||||||
:param Path: A fully qualified path of the to create.
|
:param Path: A fully qualified path of the to create.
|
||||||
:returns: bool
|
:returns: bool
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if directory_path.exists():
|
parts = directory_path.relative_to(path).parts
|
||||||
return True
|
except ValueError:
|
||||||
else:
|
# directory_path is not the subpath of path
|
||||||
if not self.dry_run:
|
|
||||||
directory_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
self.logger.info(f'Create {directory_path}')
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
# OSError is thrown for cases like no permission
|
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
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"),
|
||||||
|
]
|
||||||
|
answers = inquirer.prompt(prompt, theme=self.theme)
|
||||||
|
file_path = dir_path.parent / answers['file_path']
|
||||||
|
else:
|
||||||
|
file_path = dir_path.parent / (dir_path.name + '_file')
|
||||||
|
|
||||||
return False
|
self.logger.warning(f'Renaming {dir_path} to {file_path}')
|
||||||
|
shutil.move(dir_path, file_path)
|
||||||
|
for media in medias:
|
||||||
|
if media.file_path == dir_path:
|
||||||
|
media.file_path = file_path
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.dry_run:
|
||||||
|
directory_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.logger.info(f'Create {directory_path}')
|
||||||
|
|
||||||
def create_directory(self, directory_path):
|
def create_directory(self, directory_path):
|
||||||
"""Create a directory if it does not already exist.
|
"""Create a directory if it does not already exist.
|
||||||
|
@ -608,7 +628,8 @@ class Collection(object):
|
||||||
conflict_file_list = []
|
conflict_file_list = []
|
||||||
file_list = [x for x in self._get_files_in_path(path, glob=self.glob)]
|
file_list = [x for x in self._get_files_in_path(path, glob=self.glob)]
|
||||||
for src_path in file_list:
|
for src_path in file_list:
|
||||||
src_checksum = self.checksum(src_path)
|
# TODO to test it
|
||||||
|
media = Media(src_path, path, logger=self.logger)
|
||||||
path_parts = src_path.relative_to(self.root).parts
|
path_parts = src_path.relative_to(self.root).parts
|
||||||
dedup_path = []
|
dedup_path = []
|
||||||
for path_part in path_parts:
|
for path_part in path_parts:
|
||||||
|
@ -624,22 +645,18 @@ class Collection(object):
|
||||||
|
|
||||||
# Dedup path
|
# Dedup path
|
||||||
dest_path = self.root.joinpath(*dedup_path)
|
dest_path = self.root.joinpath(*dedup_path)
|
||||||
self._create_directory(dest_path.parent.name)
|
self._create_directory(dest_path.parent.name, path, media)
|
||||||
|
|
||||||
src_path = str(src_path)
|
|
||||||
dest_path = str(dest_path)
|
|
||||||
|
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
if result:
|
if result:
|
||||||
self.summary, has_errors = self.record_file(src_path,
|
self.summary, has_errors = self.record_file(src_path,
|
||||||
dest_path, src_checksum, media)
|
dest_path, media)
|
||||||
elif result is False:
|
elif result is False:
|
||||||
# There is conflict files
|
# There is conflict files
|
||||||
conflict_file_list.append({'src_path': src_path,
|
conflict_file_list.append(src_path, dest_path, copy(media))
|
||||||
'src_checksum': src_checksum, 'dest_path': dest_path})
|
|
||||||
|
|
||||||
if conflict_file_list != []:
|
if conflict_file_list != []:
|
||||||
result = self._solve_conflicts(conflict_file_list, media, remove_duplicates)
|
result = self._solve_conflicts(conflict_file_list, remove_duplicates)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
has_errors = True
|
has_errors = True
|
||||||
|
@ -667,6 +684,8 @@ class Collection(object):
|
||||||
Sort files into appropriate folder
|
Sort files into appropriate folder
|
||||||
"""
|
"""
|
||||||
has_errors = False
|
has_errors = False
|
||||||
|
result = False
|
||||||
|
files_data = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
path = self._check_path(path)
|
path = self._check_path(path)
|
||||||
conflict_file_list = []
|
conflict_file_list = []
|
||||||
|
@ -675,43 +694,47 @@ class Collection(object):
|
||||||
if self.interactive:
|
if self.interactive:
|
||||||
file_list = self._modify_selection(file_list)
|
file_list = self._modify_selection(file_list)
|
||||||
print('Processing...')
|
print('Processing...')
|
||||||
|
|
||||||
|
# Get medias and paths
|
||||||
for src_path in file_list:
|
for src_path in file_list:
|
||||||
subdirs = src_path.relative_to(path).parent
|
|
||||||
# Process files
|
# Process files
|
||||||
src_checksum = self.checksum(src_path)
|
|
||||||
media = Media(src_path, path, self.album_from_folder,
|
media = Media(src_path, path, self.album_from_folder,
|
||||||
ignore_tags, self.interactive, self.logger,
|
ignore_tags, self.interactive, self.logger,
|
||||||
self.use_date_filename, self.use_file_dates)
|
self.use_date_filename, self.use_file_dates)
|
||||||
if media:
|
if media:
|
||||||
metadata = media.get_metadata(loc, self.db, self.cache)
|
metadata = media.get_metadata(self.root, loc, self.db, self.cache)
|
||||||
# Get the destination path according to metadata
|
# Get the destination path according to metadata
|
||||||
file_path = Path(self.get_path(metadata, subdirs))
|
relpath = Path(self.get_path(metadata))
|
||||||
else:
|
else:
|
||||||
# Keep same directory structure
|
# Keep same directory structure
|
||||||
file_path = src_path.relative_to(path)
|
relpath = src_path.relative_to(path)
|
||||||
|
|
||||||
dest_directory = self.root / file_path.parent
|
files_data.append((copy(media), relpath))
|
||||||
self._create_directory(dest_directory)
|
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
for media, relpath in files_data:
|
||||||
|
dest_directory = self.root / relpath.parent
|
||||||
|
self._create_directory(dest_directory, path, media)
|
||||||
|
|
||||||
|
# sort files and solve conflicts
|
||||||
|
for media, relpath in files_data:
|
||||||
# Convert paths to string
|
# Convert paths to string
|
||||||
src_path = str(src_path)
|
src_path = media.file_path
|
||||||
dest_path = str(self.root / file_path)
|
dest_path = self.root / relpath
|
||||||
|
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.summary, has_errors = self.record_file(src_path,
|
self.summary, has_errors = self.record_file(src_path,
|
||||||
dest_path, src_checksum, media)
|
dest_path, media)
|
||||||
elif result is False:
|
elif result is False:
|
||||||
# There is conflict files
|
# There is conflict files
|
||||||
conflict_file_list.append({'src_path': src_path,
|
conflict_file_list.append((src_path, dest_path, media))
|
||||||
'src_checksum': src_checksum, 'dest_path': dest_path})
|
|
||||||
|
|
||||||
if conflict_file_list != []:
|
if conflict_file_list != []:
|
||||||
result = self._solve_conflicts(conflict_file_list, media,
|
result = self._solve_conflicts(conflict_file_list, remove_duplicates)
|
||||||
remove_duplicates)
|
|
||||||
|
|
||||||
if not result:
|
if result is False:
|
||||||
has_errors = True
|
has_errors = True
|
||||||
|
|
||||||
return self.summary, has_errors
|
return self.summary, has_errors
|
||||||
|
@ -719,7 +742,7 @@ class Collection(object):
|
||||||
def set_hash(self, result, src_path, dest_path, src_checksum):
|
def set_hash(self, result, src_path, dest_path, src_checksum):
|
||||||
if result:
|
if result:
|
||||||
# Check if file remain the same
|
# Check if file remain the same
|
||||||
result = self.checkcomp(dest_path, src_checksum)
|
result = self._checkcomp(dest_path, src_checksum)
|
||||||
has_errors = False
|
has_errors = False
|
||||||
if result:
|
if result:
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
|
@ -776,7 +799,7 @@ class Collection(object):
|
||||||
for image in img_paths:
|
for image in img_paths:
|
||||||
if not os.path.isfile(image):
|
if not os.path.isfile(image):
|
||||||
continue
|
continue
|
||||||
checksum1 = self.checksum(image)
|
checksum1 = utils.checksum(image)
|
||||||
# Process files
|
# Process files
|
||||||
# media = Media(src_path, False, self.logger)
|
# media = Media(src_path, False, self.logger)
|
||||||
# TODO compare metadata
|
# TODO compare metadata
|
||||||
|
@ -786,7 +809,7 @@ class Collection(object):
|
||||||
moved_imgs = set()
|
moved_imgs = set()
|
||||||
for img_path in i.find_similar(image, similarity):
|
for img_path in i.find_similar(image, similarity):
|
||||||
similar = True
|
similar = True
|
||||||
checksum2 = self.checksum(img_path)
|
checksum2 = utils.checksum(img_path)
|
||||||
# move image into directory
|
# move image into directory
|
||||||
name = os.path.splitext(os.path.basename(image))[0]
|
name = os.path.splitext(os.path.basename(image))[0]
|
||||||
directory_name = 'similar_to_' + name
|
directory_name = 'similar_to_' + name
|
||||||
|
@ -836,7 +859,7 @@ class Collection(object):
|
||||||
img_path = os.path.join(dirname, subdir, file_name)
|
img_path = os.path.join(dirname, subdir, file_name)
|
||||||
if os.path.isdir(img_path):
|
if os.path.isdir(img_path):
|
||||||
continue
|
continue
|
||||||
checksum = self.checksum(img_path)
|
checksum = utils.checksum(img_path)
|
||||||
dest_path = os.path.join(dirname, os.path.basename(img_path))
|
dest_path = os.path.join(dirname, os.path.basename(img_path))
|
||||||
result = self.move_file(img_path, dest_path, checksum)
|
result = self.move_file(img_path, dest_path, checksum)
|
||||||
if not result:
|
if not result:
|
||||||
|
|
|
@ -45,6 +45,7 @@ class Sqlite:
|
||||||
'FilePath': 'text not null',
|
'FilePath': 'text not null',
|
||||||
'Checksum': 'text',
|
'Checksum': 'text',
|
||||||
'Album': 'text',
|
'Album': 'text',
|
||||||
|
'Title': 'text',
|
||||||
'LocationId': 'integer',
|
'LocationId': 'integer',
|
||||||
'DateMedia': 'text',
|
'DateMedia': 'text',
|
||||||
'DateOriginal': 'text',
|
'DateOriginal': 'text',
|
||||||
|
@ -52,6 +53,7 @@ class Sqlite:
|
||||||
'DateModified': 'text',
|
'DateModified': 'text',
|
||||||
'CameraMake': 'text',
|
'CameraMake': 'text',
|
||||||
'CameraModel': 'text',
|
'CameraModel': 'text',
|
||||||
|
'OriginalName':'text',
|
||||||
'SrcPath': 'text',
|
'SrcPath': 'text',
|
||||||
'Subdirs': 'text',
|
'Subdirs': 'text',
|
||||||
'Filename': 'text'
|
'Filename': 'text'
|
||||||
|
@ -114,13 +116,13 @@ class Sqlite:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _run(self, query, n=0):
|
def _run(self, query, n=0):
|
||||||
result = None
|
result = False
|
||||||
result = self.cur.execute(query).fetchone()
|
result = self.cur.execute(query).fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return result[n]
|
return result[n]
|
||||||
else:
|
else:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
def _run_many(self, query):
|
def _run_many(self, query):
|
||||||
self.cur.executemany(query, table_list)
|
self.cur.executemany(query, table_list)
|
||||||
|
@ -223,7 +225,7 @@ class Sqlite:
|
||||||
return self._run(query)
|
return self._run(query)
|
||||||
|
|
||||||
def get_location_data(self, LocationId, data):
|
def get_location_data(self, LocationId, data):
|
||||||
query = f"select {data} from location where ROWID='{LocationId}'"
|
query = f"select '{data}' from location where ROWID='{LocationId}'"
|
||||||
return self._run(query)
|
return self._run(query)
|
||||||
|
|
||||||
def get_location(self, Latitude, Longitude, column):
|
def get_location(self, Latitude, Longitude, column):
|
||||||
|
@ -277,3 +279,5 @@ class Sqlite:
|
||||||
sql = f'delete from {table}'
|
sql = f'delete from {table}'
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
158
ordigi/media.py
158
ordigi/media.py
|
@ -6,13 +6,14 @@ import inquirer
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
# import pprint
|
# import pprint
|
||||||
|
|
||||||
# load modules
|
# load modules
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
import re
|
|
||||||
from ordigi.exiftool import ExifTool, ExifToolCaching
|
from ordigi.exiftool import ExifTool, ExifToolCaching
|
||||||
from ordigi.utils import get_date_from_string
|
from ordigi import utils
|
||||||
from ordigi import request
|
from ordigi import request
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,17 +35,14 @@ class Media():
|
||||||
|
|
||||||
extensions = PHOTO + AUDIO + VIDEO
|
extensions = PHOTO + AUDIO + VIDEO
|
||||||
|
|
||||||
def __init__(self, file_path, root, album_from_folder=False,
|
def __init__(self, file_path, src_path, album_from_folder=False,
|
||||||
ignore_tags=set(), interactive=False, logger=logging.getLogger(),
|
ignore_tags=set(), interactive=False, logger=logging.getLogger(),
|
||||||
use_date_filename=False, use_file_dates=False):
|
use_date_filename=False, use_file_dates=False):
|
||||||
"""
|
"""
|
||||||
:params: Path, Path, bool, set, bool, Logger
|
:params: Path, Path, bool, set, bool, Logger
|
||||||
"""
|
"""
|
||||||
self.file_path = str(file_path)
|
self.file_path = file_path
|
||||||
self.root = str(root)
|
self.src_path = src_path
|
||||||
self.subdirs = str(file_path.relative_to(root).parent)
|
|
||||||
self.folder = str(file_path.parent.name)
|
|
||||||
self.filename = str(file_path.name)
|
|
||||||
|
|
||||||
self.album_from_folder = album_from_folder
|
self.album_from_folder = album_from_folder
|
||||||
self.exif_metadata = None
|
self.exif_metadata = None
|
||||||
|
@ -222,7 +220,7 @@ class Media():
|
||||||
answers = inquirer.prompt(choices_list, theme=self.theme)
|
answers = inquirer.prompt(choices_list, theme=self.theme)
|
||||||
if not answers['date_list']:
|
if not answers['date_list']:
|
||||||
answers = inquirer.prompt(prompt, theme=self.theme)
|
answers = inquirer.prompt(prompt, theme=self.theme)
|
||||||
return get_date_from_string(answers['date_custom'])
|
return utils.get_date_from_string(answers['date_custom'])
|
||||||
else:
|
else:
|
||||||
return answers['date_list']
|
return answers['date_list']
|
||||||
|
|
||||||
|
@ -237,9 +235,9 @@ class Media():
|
||||||
basename = os.path.splitext(self.metadata['filename'])[0]
|
basename = os.path.splitext(self.metadata['filename'])[0]
|
||||||
date_original = self.metadata['date_original']
|
date_original = self.metadata['date_original']
|
||||||
if self.metadata['original_name']:
|
if self.metadata['original_name']:
|
||||||
date_filename = get_date_from_string(self.metadata['original_name'])
|
date_filename = utils.get_date_from_string(self.metadata['original_name'])
|
||||||
else:
|
else:
|
||||||
date_filename = get_date_from_string(basename)
|
date_filename = utils.get_date_from_string(basename)
|
||||||
|
|
||||||
date_original = self.metadata['date_original']
|
date_original = self.metadata['date_original']
|
||||||
date_created = self.metadata['date_created']
|
date_created = self.metadata['date_created']
|
||||||
|
@ -324,76 +322,99 @@ class Media():
|
||||||
else:
|
else:
|
||||||
return answers['album']
|
return answers['album']
|
||||||
|
|
||||||
def get_metadata(self, loc=None, db=None, cache=False):
|
def get_metadata(self, root, loc=None, db=None, cache=False):
|
||||||
"""Get a dictionary of metadata from exif.
|
"""Get a dictionary of metadata from exif.
|
||||||
All keys will be present and have a value of None if not obtained.
|
All keys will be present and have a value of None if not obtained.
|
||||||
|
|
||||||
:returns: dict
|
:returns: dict
|
||||||
"""
|
"""
|
||||||
self.get_exif_metadata()
|
|
||||||
|
|
||||||
self.metadata = {}
|
self.metadata = {}
|
||||||
# Retrieve selected metadata to dict
|
self.metadata['checksum'] = utils.checksum(self.file_path)
|
||||||
if not self.exif_metadata:
|
|
||||||
return self.metadata
|
|
||||||
|
|
||||||
for key in self.tags_keys:
|
db_checksum = False
|
||||||
|
location_id = None
|
||||||
|
|
||||||
|
if cache and db:
|
||||||
|
# Check if file_path is a subpath of root
|
||||||
|
if str(self.file_path).startswith(str(root)):
|
||||||
|
relpath = os.path.relpath(self.file_path, root)
|
||||||
|
db_checksum = db.get_checksum(relpath)
|
||||||
|
file_checksum = self.metadata['checksum']
|
||||||
|
# Check if checksum match
|
||||||
|
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')
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
if db_checksum:
|
||||||
|
# Get metadata from db
|
||||||
formated_data = None
|
formated_data = None
|
||||||
for value in self._get_key_values(key):
|
for key in self.tags_keys:
|
||||||
|
if key in ('latitude', 'longitude', 'latitude_ref',
|
||||||
|
'longitude_ref', 'file_path'):
|
||||||
|
continue
|
||||||
|
label = utils.snake2camel(key)
|
||||||
|
value = db.get_metadata_data(relpath, label)
|
||||||
if 'date' in key:
|
if 'date' in key:
|
||||||
formated_data = self.get_date_format(value)
|
formated_data = self.get_date_format(value)
|
||||||
elif key in ('latitude', 'longitude'):
|
|
||||||
formated_data = self.get_coordinates(key, value)
|
|
||||||
else:
|
else:
|
||||||
if value is not None and value != '':
|
formated_data = value
|
||||||
formated_data = value
|
self.metadata[key] = formated_data
|
||||||
|
for key in 'src_path', 'subdirs', 'filename':
|
||||||
|
label = utils.snake2camel(key)
|
||||||
|
formated_data = db.get_metadata_data(relpath, label)
|
||||||
|
self.metadata[key] = formated_data
|
||||||
|
|
||||||
|
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['filename'] = self.file_path.name
|
||||||
|
# Get metadata from exif
|
||||||
|
|
||||||
|
self.get_exif_metadata()
|
||||||
|
|
||||||
|
# Retrieve selected metadata to dict
|
||||||
|
if not self.exif_metadata:
|
||||||
|
return self.metadata
|
||||||
|
|
||||||
|
for key in self.tags_keys:
|
||||||
|
formated_data = None
|
||||||
|
for value in self._get_key_values(key):
|
||||||
|
if 'date' in key:
|
||||||
|
formated_data = self.get_date_format(value)
|
||||||
|
elif key in ('latitude', 'longitude'):
|
||||||
|
formated_data = self.get_coordinates(key, value)
|
||||||
else:
|
else:
|
||||||
formated_data = None
|
if value is not None and value != '':
|
||||||
if formated_data:
|
formated_data = value
|
||||||
# Use this data and break
|
else:
|
||||||
break
|
formated_data = None
|
||||||
|
if formated_data:
|
||||||
|
# Use this data and break
|
||||||
|
break
|
||||||
|
|
||||||
self.metadata[key] = formated_data
|
self.metadata[key] = formated_data
|
||||||
|
|
||||||
self.metadata['src_path'] = self.root
|
|
||||||
self.metadata['subdirs'] = self.subdirs
|
|
||||||
self.metadata['filename'] = self.filename
|
|
||||||
|
|
||||||
original_name = self.metadata['original_name']
|
|
||||||
if not original_name or original_name == '':
|
|
||||||
self.set_value('original_name', self.filename)
|
|
||||||
|
|
||||||
self.metadata['date_media'] = self.get_date_media()
|
self.metadata['date_media'] = self.get_date_media()
|
||||||
|
self.metadata['location_id'] = location_id
|
||||||
|
|
||||||
if self.album_from_folder:
|
loc_keys = ('latitude', 'longitude', 'latitude_ref', 'longitude_ref', 'city', 'state', 'country', 'default')
|
||||||
album = self.metadata['album']
|
|
||||||
folder = self.folder
|
|
||||||
if album and album != '':
|
|
||||||
if self.interactive:
|
|
||||||
answer = self._set_album(album, folder)
|
|
||||||
if answer == 'c':
|
|
||||||
self.metadata['album'] = input('album=')
|
|
||||||
self.set_value('album', folder)
|
|
||||||
if answer == 'a':
|
|
||||||
self.metadata['album'] = album
|
|
||||||
elif answer == 'f':
|
|
||||||
self.metadata['album'] = folder
|
|
||||||
|
|
||||||
if not album or album == '':
|
|
||||||
self.metadata['album'] = folder
|
|
||||||
self.set_value('album', folder)
|
|
||||||
|
|
||||||
loc_keys = ('latitude', 'longitude', 'city', 'state', 'country', 'default')
|
|
||||||
location_id = None
|
|
||||||
if cache and db:
|
|
||||||
location_id = db.get_metadata_data(self.file_path, 'LocationId')
|
|
||||||
|
|
||||||
if location_id:
|
if location_id:
|
||||||
for key in loc_keys:
|
for key in loc_keys:
|
||||||
# use str to convert non string format data like latitude and
|
# use str to convert non string format data like latitude and
|
||||||
# longitude
|
# longitude
|
||||||
self.metadata[key] = str(db.get_location(location_id, key.capitalize()))
|
self.metadata[key] = str(db.get_location_data(location_id,
|
||||||
|
utils.snake2camel(key)))
|
||||||
elif loc:
|
elif loc:
|
||||||
|
for key in 'latitude', 'longitude', 'latitude_ref', 'longitude_ref':
|
||||||
|
self.metadata[key] = None
|
||||||
|
|
||||||
place_name = loc.place_name(
|
place_name = loc.place_name(
|
||||||
self.metadata['latitude'],
|
self.metadata['latitude'],
|
||||||
self.metadata['longitude'],
|
self.metadata['longitude'],
|
||||||
|
@ -411,7 +432,22 @@ class Media():
|
||||||
for key in loc_keys:
|
for key in loc_keys:
|
||||||
self.metadata[key] = None
|
self.metadata[key] = None
|
||||||
|
|
||||||
self.metadata['location_id'] = location_id
|
|
||||||
|
if self.album_from_folder:
|
||||||
|
album = self.metadata['album']
|
||||||
|
folder = self.file_path.parent.name
|
||||||
|
if album and album != '':
|
||||||
|
if self.interactive:
|
||||||
|
answer = self._set_album(album, folder)
|
||||||
|
if answer == 'c':
|
||||||
|
self.metadata['album'] = input('album=')
|
||||||
|
if answer == 'a':
|
||||||
|
self.metadata['album'] = album
|
||||||
|
elif answer == 'f':
|
||||||
|
self.metadata['album'] = folder
|
||||||
|
|
||||||
|
if not album or album == '':
|
||||||
|
self.metadata['album'] = folder
|
||||||
|
|
||||||
return self.metadata
|
return self.metadata
|
||||||
|
|
||||||
|
@ -496,7 +532,7 @@ class Media():
|
||||||
|
|
||||||
:returns: bool
|
:returns: bool
|
||||||
"""
|
"""
|
||||||
return self.set_value('album', self.folder)
|
return self.set_value('album', self.file_path.parent.name)
|
||||||
|
|
||||||
|
|
||||||
def get_all_subclasses(cls=None):
|
def get_all_subclasses(cls=None):
|
||||||
|
|
|
@ -1,7 +1,31 @@
|
||||||
|
|
||||||
from math import radians, cos, sqrt
|
from math import radians, cos, sqrt
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def checksum(file_path, blocksize=65536):
|
||||||
|
"""Create a hash value for the given file.
|
||||||
|
|
||||||
|
See http://stackoverflow.com/a/3431835/1318758.
|
||||||
|
|
||||||
|
:param str file_path: Path to the file to create a hash for.
|
||||||
|
:param int blocksize: Read blocks of this size from the file when
|
||||||
|
creating the hash.
|
||||||
|
:returns: str or None
|
||||||
|
"""
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
buf = f.read(blocksize)
|
||||||
|
|
||||||
|
while len(buf) > 0:
|
||||||
|
hasher.update(buf)
|
||||||
|
buf = f.read(blocksize)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def distance_between_two_points(lat1, lon1, lat2, lon2):
|
def distance_between_two_points(lat1, lon1, lat2, lon2):
|
||||||
# As threshold is quite small use simple math
|
# 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
|
# From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # noqa
|
||||||
|
@ -37,6 +61,7 @@ def get_date_regex(string, user_regex=None):
|
||||||
for i, rx in regex.items():
|
for i, rx in regex.items():
|
||||||
yield i, rx
|
yield i, rx
|
||||||
|
|
||||||
|
|
||||||
def get_date_from_string(string, user_regex=None):
|
def get_date_from_string(string, user_regex=None):
|
||||||
# If missing datetime from EXIF data check if filename is in datetime format.
|
# If missing datetime from EXIF data check if filename is in datetime format.
|
||||||
# For this use a user provided regex if possible.
|
# For this use a user provided regex if possible.
|
||||||
|
@ -75,17 +100,14 @@ def get_date_from_string(string, user_regex=None):
|
||||||
|
|
||||||
return date
|
return date
|
||||||
|
|
||||||
|
|
||||||
# Conversion functions
|
# Conversion functions
|
||||||
# source:https://rodic.fr/blog/camelcase-and-snake_case-strings-conversion-with-python/
|
# source:https://rodic.fr/blog/camelcase-and-snake_case-strings-conversion-with-python/
|
||||||
|
|
||||||
def snake2camel(name):
|
def snake2camel(name):
|
||||||
return re.sub(r'(?:^|_)([a-z])', lambda x: x.group(1).upper(), name)
|
return re.sub(r'(?:^|_)([a-z])', lambda x: x.group(1).upper(), name)
|
||||||
|
|
||||||
def snake2camelback(name):
|
|
||||||
return re.sub(r'_([a-z])', lambda x: x.group(1).upper(), name)
|
|
||||||
|
|
||||||
def camel2snake(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:])
|
||||||
|
|
||||||
def camelback2snake(name):
|
|
||||||
return re.sub(r'[A-Z]', lambda x: '_' + x.group(0).lower(), name)
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from ordigi.exiftool import ExifToolCaching, exiftool_is_running, terminate_exif
|
||||||
from ordigi.collection import Collection
|
from ordigi.collection import Collection
|
||||||
from ordigi.geolocation import GeoLocation
|
from ordigi.geolocation import GeoLocation
|
||||||
from ordigi.media import Media
|
from ordigi.media import Media
|
||||||
from ordigi.utils import get_date_regex
|
from ordigi import utils
|
||||||
|
|
||||||
|
|
||||||
class TestCollection:
|
class TestCollection:
|
||||||
|
@ -75,8 +75,7 @@ class TestCollection:
|
||||||
for mask in masks:
|
for mask in masks:
|
||||||
matched = re.search(regex, mask)
|
matched = re.search(regex, mask)
|
||||||
if matched:
|
if matched:
|
||||||
part = collection.get_part(item, mask[1:-1],
|
part = collection.get_part(item, mask[1:-1], metadata)
|
||||||
metadata, subdirs)
|
|
||||||
# check if part is correct
|
# check if part is correct
|
||||||
assert isinstance(part, str), file_path
|
assert isinstance(part, str), file_path
|
||||||
if item == 'basename':
|
if item == 'basename':
|
||||||
|
@ -93,7 +92,7 @@ class TestCollection:
|
||||||
assert part == file_path.suffix[1:], file_path
|
assert part == file_path.suffix[1:], file_path
|
||||||
elif item == 'name':
|
elif item == 'name':
|
||||||
expected_part = file_path.stem
|
expected_part = file_path.stem
|
||||||
for i, rx in get_date_regex(expected_part):
|
for i, rx in utils.get_date_regex(expected_part):
|
||||||
part = re.sub(rx, '', expected_part)
|
part = re.sub(rx, '', expected_part)
|
||||||
assert part == expected_part, file_path
|
assert part == expected_part, file_path
|
||||||
elif item == 'custom':
|
elif item == 'custom':
|
||||||
|
@ -151,11 +150,11 @@ class TestCollection:
|
||||||
src_path = Path(self.src_path, 'test_exif', 'photo.png')
|
src_path = Path(self.src_path, 'test_exif', 'photo.png')
|
||||||
name = 'photo_' + mode + '.png'
|
name = 'photo_' + mode + '.png'
|
||||||
dest_path = Path(tmp_path, name)
|
dest_path = Path(tmp_path, name)
|
||||||
src_checksum = collection.checksum(src_path)
|
src_checksum = utils.checksum(src_path)
|
||||||
result_copy = collection.sort_file(src_path, dest_path)
|
result_copy = collection.sort_file(src_path, dest_path)
|
||||||
assert result_copy
|
assert result_copy
|
||||||
# Ensure files remain the same
|
# Ensure files remain the same
|
||||||
assert collection.checkcomp(dest_path, src_checksum)
|
assert collection._checkcomp(dest_path, src_checksum)
|
||||||
|
|
||||||
if mode == 'copy':
|
if mode == 'copy':
|
||||||
assert src_path.exists()
|
assert src_path.exists()
|
||||||
|
|
|
@ -18,13 +18,15 @@ class TestSqlite:
|
||||||
'FilePath': 'file_path',
|
'FilePath': 'file_path',
|
||||||
'Checksum': 'checksum',
|
'Checksum': 'checksum',
|
||||||
'Album': 'album',
|
'Album': 'album',
|
||||||
|
'Title': 'title',
|
||||||
'LocationId': 2,
|
'LocationId': 2,
|
||||||
'DateTaken': datetime(2012, 3, 27),
|
'DateMedia': datetime(2012, 3, 27),
|
||||||
'DateOriginal': datetime(2013, 3, 27),
|
'DateOriginal': datetime(2013, 3, 27),
|
||||||
'DateCreated': 'date_created',
|
'DateCreated': 'date_created',
|
||||||
'DateModified': 'date_modified',
|
'DateModified': 'date_modified',
|
||||||
'CameraMake': 'camera_make',
|
'CameraMake': 'camera_make',
|
||||||
'CameraModel': 'camera_model',
|
'CameraModel': 'camera_model',
|
||||||
|
'OriginalName':'original_name',
|
||||||
'SrcPath': 'src_path',
|
'SrcPath': 'src_path',
|
||||||
'Subdirs': 'subdirs',
|
'Subdirs': 'subdirs',
|
||||||
'Filename': 'filename'
|
'Filename': 'filename'
|
||||||
|
@ -62,7 +64,7 @@ class TestSqlite:
|
||||||
def test_add_metadata_data(self):
|
def test_add_metadata_data(self):
|
||||||
result = tuple(self.sqlite.cur.execute("""select * from metadata where
|
result = tuple(self.sqlite.cur.execute("""select * from metadata where
|
||||||
rowid=1""").fetchone())
|
rowid=1""").fetchone())
|
||||||
assert result == ('file_path', 'checksum', 'album', 2, '2012-03-27 00:00:00', '2013-03-27 00:00:00', 'date_created', 'date_modified', 'camera_make', 'camera_model', 'src_path', 'subdirs', 'filename')
|
assert result == ('file_path', 'checksum', 'album', 'title', 2, '2012-03-27 00:00:00', '2013-03-27 00:00:00', 'date_created', 'date_modified', 'camera_make', 'camera_model', 'original_name', 'src_path', 'subdirs', 'filename')
|
||||||
|
|
||||||
def test_get_checksum(self):
|
def test_get_checksum(self):
|
||||||
assert not self.sqlite.get_checksum('invalid')
|
assert not self.sqlite.get_checksum('invalid')
|
||||||
|
|
|
@ -28,48 +28,50 @@ class TestMetadata:
|
||||||
self.exif_data = ExifTool(file_path).asdict()
|
self.exif_data = ExifTool(file_path).asdict()
|
||||||
yield file_path, Media(file_path, self.src_path, album_from_folder=True, ignore_tags=self.ignore_tags)
|
yield file_path, Media(file_path, self.src_path, album_from_folder=True, ignore_tags=self.ignore_tags)
|
||||||
|
|
||||||
def test_get_metadata(self):
|
def test_get_metadata(self, tmp_path):
|
||||||
for file_path, media in self.get_media():
|
for file_path, media in self.get_media():
|
||||||
result = media.get_metadata()
|
# test get metadata from cache or exif
|
||||||
assert result
|
for root in self.src_path, tmp_path:
|
||||||
assert isinstance(media.metadata, dict), media.metadata
|
result = media.get_metadata(root)
|
||||||
#check if all tags key are present
|
assert result
|
||||||
for tags_key, tags in media.tags_keys.items():
|
assert isinstance(media.metadata, dict), media.metadata
|
||||||
assert tags_key in media.metadata
|
#check if all tags key are present
|
||||||
for tag in tags:
|
for tags_key, tags in media.tags_keys.items():
|
||||||
for tag_regex in self.ignore_tags:
|
assert tags_key in media.metadata
|
||||||
assert not re.match(tag_regex, tag)
|
for tag in tags:
|
||||||
# Check for valid type
|
for tag_regex in self.ignore_tags:
|
||||||
for key, value in media.metadata.items():
|
assert not re.match(tag_regex, tag)
|
||||||
if value or value == '':
|
# Check for valid type
|
||||||
if 'date' in key:
|
for key, value in media.metadata.items():
|
||||||
assert isinstance(value, datetime)
|
if value or value == '':
|
||||||
elif key in ('latitude', 'longitude'):
|
if 'date' in key:
|
||||||
assert isinstance(value, float)
|
assert isinstance(value, datetime)
|
||||||
|
elif key in ('latitude', 'longitude'):
|
||||||
|
assert isinstance(value, float)
|
||||||
|
else:
|
||||||
|
assert isinstance(value, str)
|
||||||
else:
|
else:
|
||||||
assert isinstance(value, str)
|
assert value is None
|
||||||
else:
|
|
||||||
assert value is None
|
|
||||||
|
|
||||||
if key == 'album':
|
if key == 'album':
|
||||||
for album in media._get_key_values('album'):
|
for album in media._get_key_values('album'):
|
||||||
if album is not None and album != '':
|
if album is not None and album != '':
|
||||||
assert value == album
|
assert value == album
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert value == file_path.parent.name
|
||||||
|
|
||||||
|
# Check if has_exif_data() is True if 'date_original' key is
|
||||||
|
# present, else check if it's false
|
||||||
|
has_exif_data = False
|
||||||
|
for tag in media.tags_keys['date_original']:
|
||||||
|
if tag in media.exif_metadata:
|
||||||
|
if media.get_date_format(media.exif_metadata[tag]):
|
||||||
|
has_exif_data = True
|
||||||
|
assert media.has_exif_data()
|
||||||
break
|
break
|
||||||
else:
|
if has_exif_data == False:
|
||||||
assert value == file_path.parent.name
|
assert not media.has_exif_data()
|
||||||
|
|
||||||
# Check if has_exif_data() is True if 'date_original' key is
|
|
||||||
# present, else check if it's false
|
|
||||||
has_exif_data = False
|
|
||||||
for tag in media.tags_keys['date_original']:
|
|
||||||
if tag in media.exif_metadata:
|
|
||||||
if media.get_date_format(media.exif_metadata[tag]):
|
|
||||||
has_exif_data = True
|
|
||||||
assert media.has_exif_data()
|
|
||||||
break
|
|
||||||
if has_exif_data == False:
|
|
||||||
assert not media.has_exif_data()
|
|
||||||
|
|
||||||
def test_get_date_media(self):
|
def test_get_date_media(self):
|
||||||
# collection = Collection(tmp_path, self.path_format,
|
# collection = Collection(tmp_path, self.path_format,
|
||||||
|
@ -78,7 +80,7 @@ class TestMetadata:
|
||||||
exif_data = ExifToolCaching(str(file_path)).asdict()
|
exif_data = ExifToolCaching(str(file_path)).asdict()
|
||||||
media = Media(file_path, self.src_path, use_date_filename=True,
|
media = Media(file_path, self.src_path, use_date_filename=True,
|
||||||
use_file_dates=True)
|
use_file_dates=True)
|
||||||
metadata = media.get_metadata()
|
metadata = media.get_metadata(self.src_path)
|
||||||
date_media = media.get_date_media()
|
date_media = media.get_date_media()
|
||||||
|
|
||||||
date_filename = None
|
date_filename = None
|
||||||
|
|
Loading…
Reference in New Issue