Refactoring and fix class

This commit is contained in:
Cédric Leporcq 2021-10-23 07:51:53 +02:00
parent 374f64d8b1
commit 38d7cb6841
7 changed files with 174 additions and 82 deletions

View File

@ -202,7 +202,6 @@ def sort(**kwargs):
collection = Collection( collection = Collection(
root, root,
path_format,
kwargs['album_from_folder'], kwargs['album_from_folder'],
cache, cache,
opt['day_begins'], opt['day_begins'],
@ -296,14 +295,12 @@ def clean(**kwargs):
collection = Collection( collection = Collection(
root, root,
opt['path_format'],
dry_run=dry_run, dry_run=dry_run,
exclude=exclude, exclude=exclude,
filter_by_ext=filter_by_ext, filter_by_ext=filter_by_ext,
glob=kwargs['glob'], glob=kwargs['glob'],
logger=logger, logger=logger,
max_deep=opt['max_deep'], max_deep=opt['max_deep'],
mode='move',
) )
if kwargs['path_string']: if kwargs['path_string']:
@ -337,7 +334,7 @@ def init(**kwargs):
logger = log.get_logger(level=log_level) logger = log.get_logger(level=log_level)
loc = GeoLocation(opt['geocoder'], logger, opt['prefer_english_names'], opt['timeout']) loc = GeoLocation(opt['geocoder'], logger, opt['prefer_english_names'], opt['timeout'])
collection = Collection(root, None, exclude=opt['exclude'], mode='move', logger=logger) collection = Collection(root, exclude=opt['exclude'], logger=logger)
summary = collection.init(loc) summary = collection.init(loc)
if log_level < 30: if log_level < 30:
@ -356,7 +353,7 @@ def update(**kwargs):
logger = log.get_logger(level=log_level) logger = log.get_logger(level=log_level)
loc = GeoLocation(opt['geocoder'], logger, opt['prefer_english_names'], opt['timeout']) loc = GeoLocation(opt['geocoder'], logger, opt['prefer_english_names'], opt['timeout'])
collection = Collection(root, None, exclude=opt['exclude'], mode='move', logger=logger) collection = Collection(root, exclude=opt['exclude'], logger=logger)
summary = collection.update(loc) summary = collection.update(loc)
if log_level < 30: if log_level < 30:
@ -373,7 +370,7 @@ def check(**kwargs):
root = kwargs['path'] root = kwargs['path']
config = get_collection_config(root) config = get_collection_config(root)
opt = config.get_options() opt = config.get_options()
collection = Collection(root, None, exclude=opt['exclude'], mode='move', logger=logger) collection = Collection(root, exclude=opt['exclude'], logger=logger)
result = collection.check_db() result = collection.check_db()
if result: if result:
summary, result = collection.check_files() summary, result = collection.check_files()
@ -447,11 +444,9 @@ def compare(**kwargs):
collection = Collection( collection = Collection(
root, root,
None,
exclude=exclude, exclude=exclude,
filter_by_ext=filter_by_ext, filter_by_ext=filter_by_ext,
glob=kwargs['glob'], glob=kwargs['glob'],
mode='move',
dry_run=dry_run, dry_run=dry_run,
logger=logger, logger=logger,
) )

View File

@ -253,7 +253,6 @@ class Collection:
def __init__( def __init__(
self, self,
root, root,
path_format,
album_from_folder=False, album_from_folder=False,
cache=False, cache=False,
day_begins=0, day_begins=0,
@ -264,7 +263,7 @@ class Collection:
interactive=False, interactive=False,
logger=logging.getLogger(), logger=logging.getLogger(),
max_deep=None, max_deep=None,
mode='copy', mode='move',
use_date_filename=False, use_date_filename=False,
use_file_dates=False, use_file_dates=False,
): ):
@ -275,7 +274,6 @@ class Collection:
logger.error(f'Directory {self.root} does not exist') logger.error(f'Directory {self.root} does not exist')
sys.exit(1) sys.exit(1)
self.path_format = path_format
self.db = Sqlite(self.root) self.db = Sqlite(self.root)
# Options # Options
@ -522,17 +520,17 @@ class Collection:
return items return items
def walklevel(self, src_path, maxlevel=None): def walklevel(self, src_dir, maxlevel=None):
""" """
Walk into input directory recursively until desired maxlevel Walk into input directory recursively until desired maxlevel
source: https://stackoverflow.com/questions/229186/os-walk-without-digging-into-directories-below source: https://stackoverflow.com/questions/229186/os-walk-without-digging-into-directories-below
""" """
src_path = str(src_path) src_dir = str(src_dir)
if not os.path.isdir(src_path): if not os.path.isdir(src_dir):
return None return None
num_sep = src_path.count(os.path.sep) num_sep = src_dir.count(os.path.sep)
for root, dirs, files in os.walk(src_path): for root, dirs, files in os.walk(src_dir):
level = root.count(os.path.sep) - num_sep level = root.count(os.path.sep) - num_sep
yield root, dirs, files, level yield root, dirs, files, level
if maxlevel is not None and level >= maxlevel: if maxlevel is not None and level >= maxlevel:
@ -765,8 +763,7 @@ class Collection:
return self.check_db() return self.check_db()
def init(self, loc, ignore_tags=set()): def get_medias(self, loc, ignore_tags=set()):
record = True
for file_path in self._get_all_files(): for file_path in self._get_all_files():
media = Media( media = Media(
file_path, file_path,
@ -778,11 +775,22 @@ class Collection:
) )
metadata = media.get_metadata(self.root, loc, self.db, self.cache) 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)
yield media, file_path
def init(self, loc, ignore_tags=set()):
for media, file_path in self.get_medias(loc):
self._add_db_data(media.metadata) self._add_db_data(media.metadata)
self.summary.append((file_path, 'record')) self.summary.append((file_path, 'record'))
return self.summary return self.summary
def _init_check_db(self, loc=None, ignore_tags=set()):
if self.db.is_empty('metadata'):
self.init(loc, ignore_tags)
elif not self.check_db():
self.logger.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
def check_files(self): def check_files(self):
result = True result = True
for file_path in self._get_all_files(): for file_path in self._get_all_files():
@ -857,25 +865,8 @@ class Collection:
if parents != set(): if parents != set():
self.remove_empty_subdirs(parents) self.remove_empty_subdirs(parents)
def sort_files(self, paths, loc, remove_duplicates=False, ignore_tags=set()): def _get_path_list(self, path):
""" src_list = [
Sort files into appropriate folder
"""
# Check db
if [x for x in self.db.get_rows('metadata')] == []:
self.init(loc, ignore_tags)
elif not self.check_db():
self.logger.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
result = False
files_data = []
src_dirs_in_collection = set()
for path in paths:
self.dest_list = []
path = self._check_path(path)
conflict_file_list = []
self.src_list = [
x x
for x in self._get_files_in_path( for x in self._get_files_in_path(
path, glob=self.glob, path, glob=self.glob,
@ -883,18 +874,36 @@ class Collection:
) )
] ]
if self.interactive: if self.interactive:
self.src_list = self._modify_selection() src_list = self._modify_selection()
print('Processing...') print('Processing...')
# Get medias and paths return src_list
def sort_files(self, src_dirs, path_format, loc, remove_duplicates=False, ignore_tags=set()):
"""
Sort files into appropriate folder
"""
# Check db
self._init_check_db(loc, ignore_tags)
result = False
files_data = []
src_dirs_in_collection = set()
for src_dir in src_dirs:
self.dest_list = []
src_dir = self._check_path(src_dir)
conflict_file_list = []
self.src_list = self._get_path_list(src_dir)
# Get medias and src_dirs
for src_path in self.src_list: for src_path in self.src_list:
# List all src_dirs in collection # List all src dirs in collection
if self.root in src_path.parents: if self.root in src_path.parents:
src_dirs_in_collection.add(src_path.parent) src_dirs_in_collection.add(src_path.parent)
# Process files # Get file metadata
media = Media( media = Media(
src_path, src_path,
path, src_dir,
self.album_from_folder, self.album_from_folder,
ignore_tags, ignore_tags,
self.interactive, self.interactive,
@ -904,7 +913,7 @@ class Collection:
) )
metadata = media.get_metadata(self.root, 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
fpath = FPath(self.path_format, self.day_begins, self.logger) fpath = FPath(path_format, self.day_begins, self.logger)
relpath = Path(fpath.get_path(metadata)) relpath = Path(fpath.get_path(metadata))
files_data.append((copy(media), relpath)) files_data.append((copy(media), relpath))
@ -916,7 +925,6 @@ class Collection:
# sort files and solve conflicts # sort files and solve conflicts
for media, relpath in files_data: for media, relpath in files_data:
# Convert paths to string
src_path = media.file_path src_path = media.file_path
dest_path = self.root / relpath dest_path = self.root / relpath
@ -1102,3 +1110,69 @@ class Collection:
return self.summary, result return self.summary, result
def fill_data(self, path, key, loc=None, edit=False):
"""Fill metadata and exif data for given key"""
self._init_check_db()
if key in (
'latitude',
'longitude',
'latitude_ref',
'longitude_ref',
):
print(f"Set {key} alone is not allowed")
return None
if edit:
print(f"Edit {key} values:")
else:
print(f"Fill empty {key} values:")
self.src_list = self._get_path_list(path)
for file_path in self.src_list:
media = Media(
file_path,
self.root,
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)
print()
value = media.metadata[key]
if edit or not value:
print(f"FILE: '{file_path}'")
if edit:
print(f"{key}: '{value}'")
if edit or not value:
# Prompt value for given key for file_path
# utils.open_file()
prompt = [
inquirer.Text('value', message=key),
]
answer = inquirer.prompt(prompt, theme=self.theme)
# Validate value
if key in ('date_original', 'date_created', 'date_modified'):
# Check date format
value = str(media.get_date_format(answer['value']))
else:
if not answer[key].isalnum():
print("Invalid entry, use alphanumeric chars")
value = inquirer.prompt(prompt, theme=self.theme)
# print(f"{key}='{value}'")
media.metadata[key] = value
# Update database
self._add_db_data(media.metadata)
# Update exif data
media.set_key_values(key, value)
self.summary.append((file_path, 'record'))
return self.summary

View File

@ -49,7 +49,7 @@ class Sqlite:
'CameraMake': 'text', 'CameraMake': 'text',
'CameraModel': 'text', 'CameraModel': 'text',
'OriginalName': 'text', 'OriginalName': 'text',
'SrcPath': 'text', 'SrcDir': 'text',
'Subdirs': 'text', 'Subdirs': 'text',
'Filename': 'text', 'Filename': 'text',
} }
@ -109,6 +109,21 @@ class Sqlite:
return False return False
def get_rows(self, table):
"""Cycle through rows in table
:params: str
:return: iter
"""
self.cur.execute(f'select * from {table}')
for row in self.cur:
yield row
def is_empty(self, table):
if [x for x in self.get_rows(table)] == []:
return True
return False
def _run(self, query, n=0): def _run(self, query, n=0):
result = False result = False
result = self.cur.execute(query).fetchone() result = self.cur.execute(query).fetchone()
@ -234,8 +249,8 @@ class Sqlite:
self.cur.execute(f'SELECT * FROM {table}').fetchall() 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. """
Find a name for a location in the database.
:param float latitude: Latitude of the location. :param float latitude: Latitude of the location.
:param float longitude: Longitude of the location. :param float longitude: Longitude of the location.
:param int threshold_m: Location in the database must be this close to :param int threshold_m: Location in the database must be this close to
@ -282,11 +297,3 @@ class Sqlite:
sql = f'select count() from {table}' sql = f'select count() from {table}'
return self._run(sql) return self._run(sql)
def get_rows(self, table):
"""Cycle through rows in table
:params: str
:return: iter
"""
self.cur.execute(f'select * from {table}')
for row in self.cur:
yield row

View File

@ -33,7 +33,7 @@ class Media:
def __init__( def __init__(
self, self,
file_path, file_path,
src_path, src_dir,
album_from_folder=False, album_from_folder=False,
ignore_tags=set(), ignore_tags=set(),
interactive=False, interactive=False,
@ -45,7 +45,7 @@ class Media:
:params: Path, Path, bool, set, bool, Logger :params: Path, Path, bool, set, bool, Logger
""" """
self.file_path = file_path self.file_path = file_path
self.src_path = src_path self.src_dir = src_dir
self.album_from_folder = album_from_folder self.album_from_folder = album_from_folder
self.exif_metadata = None self.exif_metadata = None
@ -214,14 +214,14 @@ class Media:
default=default, default=default,
), ),
] ]
answers = inquirer.prompt(choices_list, theme=self.theme)
if not answers['date_list']:
prompt = [ prompt = [
inquirer.Text('date_custom', message="date"), inquirer.Text('date_custom', message="date"),
] ]
answers = inquirer.prompt(choices_list, theme=self.theme)
if not answers['date_list']:
answers = inquirer.prompt(prompt, theme=self.theme) answers = inquirer.prompt(prompt, theme=self.theme)
return utils.get_date_from_string(answers['date_custom']) return self.get_date_format(answers['date_custom'])
else: else:
return answers['date_list'] return answers['date_list']
@ -236,9 +236,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 = utils.get_date_from_string(self.metadata['original_name']) date_filename = self.get_date_format(self.metadata['original_name'])
else: else:
date_filename = utils.get_date_from_string(basename) date_filename = self.get_date_format(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']
@ -386,16 +386,16 @@ class Media:
else: else:
formated_data = value formated_data = value
self.metadata[key] = formated_data self.metadata[key] = formated_data
for key in 'src_path', 'subdirs', 'filename': for key in 'src_dir', 'subdirs', 'filename':
label = utils.snake2camel(key) label = utils.snake2camel(key)
formated_data = db.get_metadata_data(relpath, label) formated_data = db.get_metadata_data(relpath, label)
self.metadata[key] = formated_data self.metadata[key] = formated_data
location_id = db.get_metadata_data(relpath, 'LocationId') location_id = db.get_metadata_data(relpath, 'LocationId')
else: else:
self.metadata['src_path'] = str(self.src_path) self.metadata['src_dir'] = str(self.src_dir)
self.metadata['subdirs'] = str( self.metadata['subdirs'] = str(
self.file_path.relative_to(self.src_path).parent self.file_path.relative_to(self.src_dir).parent
) )
self.metadata['filename'] = self.file_path.name self.metadata['filename'] = self.file_path.name
# Get metadata from exif # Get metadata from exif
@ -500,6 +500,19 @@ class Media:
""" """
return ExifTool(self.file_path, logger=self.logger).setvalue(tag, value) return ExifTool(self.file_path, logger=self.logger).setvalue(tag, value)
def set_key_values(self, key, value):
"""Set tags values for given key"""
status = True
if self.exif_metadata is None:
return False
for tag in self.tags_keys[key]:
if tag in self.exif_metadata:
if not self.set_value(tag, value):
status = False
return status
def set_date_media(self, time): def set_date_media(self, time):
"""Set the date/time a photo was taken. """Set the date/time a photo was taken.

View File

@ -1,10 +1,7 @@
click==6.6 click==6.6
imagehash==4.2.1 imagehash==4.2.1
inquirer inquirer
requests==2.20.0
Send2Trash==1.3.0
configparser==3.5.0 configparser==3.5.0
tabulate==0.7.7 tabulate==0.7.7
Pillow==8.0 Pillow==8.0
pyheif_pillow_opener=0.1 pyheif_pillow_opener=0.1
six==1.9

View File

@ -1,5 +1,6 @@
# TODO to be removed later # TODO to be removed later
from datetime import datetime from datetime import datetime
import inquirer
import os import os
import pytest import pytest
import shutil import shutil
@ -131,10 +132,11 @@ class TestCollection:
assert not exiftool_is_running() assert not exiftool_is_running()
def test_sort_files(self, tmp_path): def test_sort_files(self, tmp_path):
collection = Collection(tmp_path, self.path_format, collection = Collection(tmp_path, album_from_folder=True,
album_from_folder=True, logger=self.logger) logger=self.logger, mode='copy')
loc = GeoLocation() loc = GeoLocation()
summary, result = collection.sort_files([self.src_path], loc) summary, result = collection.sort_files([self.src_path],
self.path_format, loc)
# Summary is created and there is no errors # Summary is created and there is no errors
assert summary, summary assert summary, summary
@ -157,13 +159,13 @@ class TestCollection:
assert summary, summary assert summary, summary
assert not result, result assert not result, result
collection = Collection(tmp_path, None, mode='move', logger=self.logger) collection = Collection(tmp_path, logger=self.logger)
summary = collection.update(loc) summary = collection.update(loc)
assert summary, summary assert summary, summary
collection = Collection(tmp_path, self.path_format, album_from_folder=True) collection = Collection(tmp_path, mode='copy', album_from_folder=True)
loc = GeoLocation() loc = GeoLocation()
summary, result = collection.sort_files([self.src_path], loc) summary, result = collection.sort_files([self.src_path], self.path_format, loc)
assert summary, summary assert summary, summary
assert result, result assert result, result
@ -171,16 +173,17 @@ class TestCollection:
# TODO check if path follow path_format # TODO check if path follow path_format
def test_sort_files_invalid_db(self, tmp_path): def test_sort_files_invalid_db(self, tmp_path):
collection = Collection(tmp_path, self.path_format) collection = Collection(tmp_path, mode='copy')
loc = GeoLocation() loc = GeoLocation()
randomize_db(tmp_path) randomize_db(tmp_path)
with pytest.raises(sqlite3.DatabaseError) as e: with pytest.raises(sqlite3.DatabaseError) as e:
summary, result = collection.sort_files([self.src_path], loc) summary, result = collection.sort_files([self.src_path],
self.path_format, loc)
def test_sort_file(self, tmp_path): def test_sort_file(self, tmp_path):
for mode in 'copy', 'move': for mode in 'copy', 'move':
collection = Collection(tmp_path, self.path_format, mode=mode) collection = Collection(tmp_path, mode=mode)
# copy mode # copy mode
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'
@ -200,8 +203,7 @@ class TestCollection:
# TODO check for conflicts # TODO check for conflicts
def test__get_files_in_path(self, tmp_path): def test__get_files_in_path(self, tmp_path):
collection = Collection(tmp_path, self.path_format, collection = Collection(tmp_path, exclude={'**/*.dng',}, max_deep=1,
exclude={'**/*.dng',}, max_deep=1,
use_date_filename=True, use_file_dates=True) use_date_filename=True, use_file_dates=True)
paths = [x for x in collection._get_files_in_path(self.src_path, paths = [x for x in collection._get_files_in_path(self.src_path,
glob='**/photo*')] glob='**/photo*')]
@ -212,7 +214,7 @@ class TestCollection:
def test_sort_similar_images(self, tmp_path): def test_sort_similar_images(self, tmp_path):
path = tmp_path / 'collection' path = tmp_path / 'collection'
shutil.copytree(self.src_path, path) shutil.copytree(self.src_path, path)
collection = Collection(path, None, mode='move', logger=self.logger) collection = Collection(path, logger=self.logger)
loc = GeoLocation() loc = GeoLocation()
summary = collection.init(loc) summary = collection.init(loc)
summary, result = collection.sort_similar_images(path, similarity=60) summary, result = collection.sort_similar_images(path, similarity=60)

View File

@ -1,3 +1,4 @@
from ordigi.utils import distance_between_two_points
from ordigi.geolocation import GeoLocation from ordigi.geolocation import GeoLocation
import pytest import pytest
@ -8,8 +9,11 @@ class TestGeoLocation:
def test_coordinates_by_name(self): def test_coordinates_by_name(self):
coordinates = self.loc.coordinates_by_name('Sunnyvale, CA') coordinates = self.loc.coordinates_by_name('Sunnyvale, CA')
assert coordinates['latitude'] == 37.3688301 latitude = coordinates['latitude']
assert coordinates['longitude'] == -122.036349 longitude = coordinates['longitude']
distance = distance_between_two_points(latitude, longitude, 37.3745086, -122.0581602)
assert distance <= 3000
def test_place_name(self): def test_place_name(self):
place_name = self.loc.place_name(lat=37.368, lon=-122.03) place_name = self.loc.place_name(lat=37.368, lon=-122.03)