Compare commits

..

15 Commits

13 changed files with 1004 additions and 662 deletions

143
.gitignore vendored
View File

@ -1,139 +1,20 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Compiled python modules.
*.pyc
# Other
/build/
/.coverage
/diagnostics.lua
docs/_build
docs/Ordigi_data_scheme.odg
# Setuptools distribution folder.
/dist/
# Python egg metadata, regenerated from source files by setuptools.
/*.egg-info
/env/
/htmlcov
/ressources
/Session.vim
/tags

709
LICENSE

File diff suppressed because it is too large Load Diff

119
docs/ordigi.mm Normal file
View File

@ -0,0 +1,119 @@
<map version="freeplane 1.7.0">
<!--To view this file, download free mind mapping software Freeplane from http://freeplane.sourceforge.net -->
<node TEXT="elodie" FOLDED="false" ID="ID_577640973" CREATED="1624709002278" MODIFIED="1624709019473" STYLE="oval"><hook NAME="MapStyle">
<conditional_styles>
<conditional_style ACTIVE="true" LOCALIZED_STYLE_REF="styles.connection" LAST="false">
<node_periodic_level_condition PERIOD="2" REMAINDER="1"/>
</conditional_style>
<conditional_style ACTIVE="true" LOCALIZED_STYLE_REF="styles.topic" LAST="false">
<node_level_condition VALUE="2" MATCH_CASE="false" MATCH_APPROXIMATELY="false" COMPARATION_RESULT="0" SUCCEED="true"/>
</conditional_style>
<conditional_style ACTIVE="true" LOCALIZED_STYLE_REF="styles.subtopic" LAST="false">
<node_level_condition VALUE="4" MATCH_CASE="false" MATCH_APPROXIMATELY="false" COMPARATION_RESULT="0" SUCCEED="true"/>
</conditional_style>
<conditional_style ACTIVE="true" LOCALIZED_STYLE_REF="styles.subsubtopic" LAST="false">
<node_level_condition VALUE="6" MATCH_CASE="false" MATCH_APPROXIMATELY="false" COMPARATION_RESULT="0" SUCCEED="true"/>
</conditional_style>
</conditional_styles>
<properties edgeColorConfiguration="#808080ff,#ff0000ff,#0000ffff,#00ff00ff,#ff00ffff,#00ffffff,#7c0000ff,#00007cff,#007c00ff,#7c007cff,#007c7cff,#7c7c00ff" fit_to_viewport="false" show_note_icons="true"/>
<map_styles>
<stylenode LOCALIZED_TEXT="styles.root_node" STYLE="oval" UNIFORM_SHAPE="true" VGAP_QUANTITY="24.0 pt">
<font SIZE="24"/>
<stylenode LOCALIZED_TEXT="styles.predefined" POSITION="right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="default" ICON_SIZE="12.0 pt" COLOR="#000000" STYLE="fork">
<font NAME="Arial" SIZE="10" BOLD="false" ITALIC="false"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.details"/>
<stylenode LOCALIZED_TEXT="defaultstyle.attributes">
<font SIZE="9"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.note" COLOR="#000000" BACKGROUND_COLOR="#ffffff" TEXT_ALIGN="LEFT"/>
<stylenode LOCALIZED_TEXT="defaultstyle.floating">
<edge STYLE="hide_edge"/>
<cloud COLOR="#f0f0f0" SHAPE="ROUND_RECT"/>
</stylenode>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.user-defined" POSITION="right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="styles.topic" COLOR="#18898b" STYLE="fork">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.subtopic" COLOR="#cc3300" STYLE="fork">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.subsubtopic" COLOR="#669900">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.connection" COLOR="#606060" STYLE="fork">
<font NAME="Arial" SIZE="8" BOLD="false"/>
</stylenode>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.AutomaticLayout" POSITION="right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="AutomaticLayout.level.root" COLOR="#000000" STYLE="oval">
<font SIZE="18"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,1" COLOR="#0033ff">
<font SIZE="16"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,2" COLOR="#00b439">
<font SIZE="14"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,3" COLOR="#990000">
<font SIZE="12"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,4" COLOR="#111111">
<font SIZE="10"/>
</stylenode>
</stylenode>
</stylenode>
</map_styles>
</hook>
<node TEXT="import" POSITION="right" ID="ID_1958811617" CREATED="1624709031603" MODIFIED="1624710428698"><richcontent TYPE="NOTE">
<html>
<head>
</head>
<body>
<p>
Import from external source
</p>
</body>
</html>
</richcontent>
<node TEXT="--update" ID="ID_1408411362" CREATED="1624710635676" MODIFIED="1624710643751"/>
</node>
<node TEXT="update" POSITION="right" ID="ID_200299843" CREATED="1624709041259" MODIFIED="1624710451112"><richcontent TYPE="NOTE">
<html>
<head>
</head>
<body>
<p>
Update metadata
</p>
</body>
</html>
</richcontent>
</node>
<node TEXT="sort" FOLDED="true" POSITION="right" ID="ID_474160274" CREATED="1624709213958" MODIFIED="1624710465196"><richcontent TYPE="NOTE">
<html>
<head>
</head>
<body>
<p>
Sort photo
</p>
</body>
</html>
</richcontent>
<node TEXT="sort files" ID="ID_1215066925" CREATED="1624709364728" MODIFIED="1624709367203"/>
</node>
</node>
</map>

241
notes.md Normal file
View File

@ -0,0 +1,241 @@
# Name ideas
dozo
fog
mtool
ordigi
# Geocoders
- Pelias
- Photon
- Nominatium
# TEST
def get_exif(filename):
image = Image.open(filename)
image.verify()
return image._getexif()
def get_geotagging(exif):
if not exif:
raise ValueError("No EXIF metadata found")
geotagging = {}
for (idx, tag) in TAGS.items():
if tag == 'GPSInfo':
if idx not in exif:
raise ValueError("No EXIF geotagging found")
for (key, val) in GPSTAGS.items():
if key in exif[idx]:
geotagging[val] = exif[idx][key]
return geotagging
get_geotagging(exif)
from PIL.ExifTags import TAGS
def get_labeled_exif(exif):
labeled = {}
for (key, val) in exif.items():
labeled[TAGS.get(key)] = val
return labeled
get_geotagging(exif)
from PIL.ExifTags import GPSTAGS
get_geotagging(exif)
geotags = get_geotagging(exif)
get_location(geotags)
def get_decimal_from_dms(dms, ref):
degrees = dms[0][0] / dms[0][1]
minutes = dms[1][0] / dms[1][1] / 60.0
seconds = dms[2][0] / dms[2][1] / 3600.0
if ref in ['S', 'W']:
degrees = -degrees
minutes = -minutes
seconds = -seconds
return round(degrees + minutes + seconds, 5)
def get_coordinates(geotags):
lat = get_decimal_from_dms(geotags['GPSLatitude'], geotags['GPSLatitudeRef'])
lon = get_decimal_from_dms(geotags['GPSLongitude'], geotags['GPSLongitudeRef'])
return (lat,lon)
def get_geotagging(exif):
if not exif:
raise ValueError("No EXIF metadata found")
geotagging = {}
for (idx, tag) in TAGS.items():
if tag == 'GPSInfo':
if idx not in exif:
raise ValueError("No EXIF geotagging found")
for (key, val) in GPSTAGS.items():
if key in exif[idx]:
geotagging[val] = exif[idx][key]
return geotagging
def get_decimal_from_dms(dms, ref):
degrees = dms[0]
minutes = dms[1] / 60.0
seconds = dms[2] / 3600.0
if ref in ['S', 'W']:
degrees = -degrees
minutes = -minutes
seconds = -seconds
return round(degrees + minutes + seconds, 5)
headers = {}
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/41: headers = {}
78/42:
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/43:
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/44: API_KEY=m5aGo8xGe4LLhxeKZYpHr2MPXGN2aDhe
78/45: API_KEY='m5aGo8xGe4LLhxeKZYpHr2MPXGN2aDhe'
78/46:
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/47: API_KEY='m5aGo8xGe4LLhxeKZYpHr2MPXGN2aDhe'
78/48:
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/49:
params = {
'apiKey': os.environ['m5aGo8xGe4LLhxeKZYpHr2MPXGN2aDhe'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
78/50: %load_ext autotime
78/51:
import pandas as pd
import geopandas as gpd
import geopy
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiterimport matplotlib.pyplot as plt
import plotly_express as pximport tqdm
from tqdm._tqdm_notebook import tqdm_notebook
78/52:
import pandas as pd
import geopandas as gpd
import geopy
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiterimport matplotlib.pyplot as plt
import plotly_express as px
import pandas as pd
import geopandas as gpd
from PIL import Image
filename='2021-02-24_09-33-29-20210305_081001_01.mp4'
def get_exif(filename):
image = Image.open(filename)
image.verify()
return image._getexif()
exif=get_exif(filename)
from PIL.ExifTags import TAGS
from PIL.ExifTags import GPSTAGS
def get_geotagging(exif):
if not exif:
raise ValueError("No EXIF metadata found")
geotagging = {}
for (idx, tag) in TAGS.items():
if tag == 'GPSInfo':
if idx not in exif:
raise ValueError("No EXIF geotagging found")
for (key, val) in GPSTAGS.items():
if key in exif[idx]:
geotagging[val] = exif[idx][key]
return geotagging
geotags = get_geotagging(exif)
import os
import requests
def get_location(geotags):
coords = get_coordinates(geotags)
uri = 'https://revgeocode.search.hereapi.com/v1/revgeocode'
headers = {}
params = {
'apiKey': os.environ['API_KEY'],
'at': "%s,%s" % coords,
'lang': 'en-US',
'limit': 1,
}
response = requests.get(uri, headers=headers, params=params)
try:
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(str(e))
return {}
def get_coordinates(geotags):
lat = get_decimal_from_dms(geotags['GPSLatitude'], geotags['GPSLatitudeRef'])
lon = get_decimal_from_dms(geotags['GPSLongitude'], geotags['GPSLongitudeRef'])
return (lat,lon)
coords = get_coordinates(geotags)
import geopy
from geopy.geocoders import Nominatim
locator = Nominatim(user_agent='myGeocoder')
# coordinates ='53.480837, -2.244914'
lat='45.58339'
lon='4.79823'
coords = lat + ',' + lon
locator.reverse(coords)
location =locator.reverse(coords)
location.address.split(',')
city=location.address.split(',')[1].strip()
country=location.address.split(',')[7].strip()
location.raw
rint
country=location.raw['address']['country']
city=location.raw['address']['village']

View File

@ -12,26 +12,11 @@ from ordigi.geolocation import GeoLocation
from ordigi import utils
_logger_options = [
click.option(
'--quiet',
'-q',
default=False,
is_flag=True,
help='Log level set to ERROR',
),
click.option(
'--verbose',
'-v',
default=False,
is_flag=True,
help='Log level set to INFO',
),
click.option(
'--debug',
'-d',
default=False,
is_flag=True,
help='Log level set to DEBUG',
default='WARNING',
help='Log level [WARNING,INFO,DEBUG,NOTSET]',
),
]
@ -183,7 +168,7 @@ def _check(**kwargs):
"""
root = Path(kwargs['path']).expanduser().absolute()
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
collection = Collection(root)
@ -206,7 +191,7 @@ def _check(**kwargs):
@add_options(_filter_options)
@click.option(
'--dedup-regex',
'-D',
'-d',
default=None,
multiple=True,
help='Regex to match duplicate strings parts',
@ -233,7 +218,7 @@ def _clean(**kwargs):
"""Clean media collection"""
folders = kwargs['folders']
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
subdirs = kwargs['subdirs']
@ -283,7 +268,7 @@ def _clean(**kwargs):
def _clone(**kwargs):
"""Clone media collection to another location"""
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
src_path = Path(kwargs['src']).expanduser().absolute()
@ -336,7 +321,7 @@ def _compare(**kwargs):
subdirs = kwargs['subdirs']
root = kwargs['collection']
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
paths, root = _get_paths(subdirs, root)
@ -373,25 +358,16 @@ def _compare(**kwargs):
multiple=True,
help="Select exif tags groups to edit",
)
@click.option(
'--overwrite',
'-O',
default=False,
is_flag=True,
help="Overwrite db and exif value by key value",
)
@click.argument('subdirs', required=False, nargs=-1, type=click.Path())
@click.argument('path', required=True, nargs=1, type=click.Path())
def _edit(**kwargs):
"""Edit EXIF metadata in files or directories"""
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
paths, root = _get_paths(kwargs['subdirs'], kwargs['path'])
overwrite = kwargs['overwrite']
collection = Collection(
root,
{
@ -408,11 +384,13 @@ def _edit(**kwargs):
'camera_make',
'camera_model',
'city',
'coordinates',
'country',
# 'date_created',
'date_media',
# 'date_modified',
'date_original',
'default',
'latitude',
'location',
'longitude',
@ -427,9 +405,6 @@ def _edit(**kwargs):
keys = set(editable_keys)
else:
keys = set(kwargs['key'])
if 'coordinates' in keys:
keys.remove('coordinates')
keys.update(['latitude', 'longitude'])
location = False
for key in keys:
@ -437,6 +412,10 @@ def _edit(**kwargs):
LOG.error(f"key '{key}' is not valid")
sys.exit(1)
if key == 'coordinates':
keys.remove('coordinates')
keys.update(['latitude', 'longitude'])
if key in (
'city',
'latitude',
@ -452,7 +431,7 @@ def _edit(**kwargs):
else:
loc = None
summary = collection.edit_metadata(paths, keys, loc, overwrite)
summary = collection.edit_metadata(paths, keys, loc, overwrite=True)
if log_level < 30:
summary.print()
@ -469,7 +448,7 @@ def _init(**kwargs):
Init media collection database.
"""
root = Path(kwargs['path']).expanduser().absolute()
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
collection = Collection(root)
@ -506,7 +485,7 @@ def _import(**kwargs):
"""Sort files or directories by reading their EXIF and organizing them
according to ordigi.conf preferences.
"""
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
src_paths, root = _get_paths(kwargs['src'], kwargs['dest'])
@ -562,7 +541,7 @@ def _sort(**kwargs):
"""Sort files or directories by reading their EXIF and organizing them
according to ordigi.conf preferences.
"""
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
paths, root = _get_paths(kwargs['subdirs'], kwargs['dest'])
@ -613,7 +592,7 @@ def _update(**kwargs):
Update media collection database.
"""
root = Path(kwargs['path']).expanduser().absolute()
log_level = log.get_level(kwargs['quiet'], kwargs['verbose'], kwargs['debug'])
log_level = log.get_level(kwargs['verbose'])
log.console(LOG, level=log_level)
collection = Collection(root)

View File

@ -494,7 +494,6 @@ class SortMedias:
self.summary = Summary(self.root)
# Attributes
self.input = request.Input()
self.theme = request.load_theme()
def _checkcomp(self, dest_path, src_checksum):
@ -581,10 +580,14 @@ class SortMedias:
self.log.warning(f'Target directory {dir_path} is a file')
# Rename the src_file
if self.interactive:
answer = self.input.text(
"New name for" f"'{dir_path.name}' file"
)
file_path = dir_path.parent / answer
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')
@ -757,7 +760,6 @@ class Collection(SortMedias):
self.paths,
root,
self.opt['Exif'],
{},
self.db,
self.opt['Terminal']['interactive'],
)
@ -865,20 +867,7 @@ class Collection(SortMedias):
return True
def check_file(self, file_path):
self.medias.checksums[file_path] = utils.checksum(file_path)
if self._check_file(file_path, self.medias.checksums[file_path]):
return True
# We d'ont want to silently ignore or correct this without
# resetting the cache as is could be due to file corruption
self.log.error(f'modified or corrupted file.')
self.log.info(
'Use ordigi update --checksum or --reset-cache, check database integrity or try to restore the file'
)
return False
def check_db(self, checksums=True):
def check_db(self):
"""
Check if db FilePath match to collection filesystem
:returns: bool
@ -887,11 +876,18 @@ class Collection(SortMedias):
db_rows = [row['FilePath'] for row in self.db.sqlite.get_rows('metadata')]
for file_path in file_paths:
result = self.file_in_db(file_path, db_rows)
checksum = utils.checksum(file_path)
if not result:
self.log.error('Db data is not accurate')
self.log.info(f'{file_path} not in db')
return False
elif checksums and not self.check_file(file_path):
elif not self._check_file(file_path, checksum):
# We d'ont want to silently ignore or correct this without
# resetting the cache as is could be due to file corruption
self.log.error(f'modified or corrupted file.')
self.log.info(
'Use ordigi update --checksum or --reset-cache, check database integrity or try to restore the file'
)
return False
nb_files = len(file_paths)
@ -910,10 +906,10 @@ class Collection(SortMedias):
self.log.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
def _init_check_db(self, checksums=True, loc=None):
def _init_check_db(self, loc=None):
if self.db.sqlite.is_empty('metadata'):
self.init(loc)
elif not self.check_db(checksums):
elif not self.check_db():
self.log.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
@ -942,7 +938,6 @@ class Collection(SortMedias):
db_rows = list(self.db.sqlite.get_rows('metadata'))
invalid_db_rows = set()
db_paths = set()
self.log.info(f"Update database:")
for db_row in db_rows:
abspath = self.root / db_row['FilePath']
if abspath not in file_paths:
@ -954,17 +949,15 @@ class Collection(SortMedias):
relpath = os.path.relpath(file_path, self.root)
metadata = {}
self.medias.checksums[file_path] = utils.checksum(file_path)
if (
not self._check_file(file_path, self.medias.checksums[file_path])
and update_checksum
):
checksum = utils.checksum(file_path)
if not self._check_file(file_path, checksum) and update_checksum:
# metatata will fill checksum from file
metadata = self.medias.get_metadata(file_path, self.root, loc=loc)
metadata = self.medias.get_metadata(
file_path, self.root, checksum, loc=loc
)
metadata['file_path'] = relpath
# set row attribute to the file
self.db.add_file_data(metadata)
self.log.info(f"Update '{file_path}' checksum to db")
self.summary.append('update', file_path)
# If file not in database
@ -985,13 +978,11 @@ class Collection(SortMedias):
break
# set row attribute to the file
self.db.add_file_data(metadata)
self.log.info(f"Add '{file_path}' to db")
self.summary.append('update', file_path)
# Finally delete invalid rows
for row in invalid_db_rows:
self.db.sqlite.delete_filepath(row['FilePath'])
self.log.info(f"Delete invalid row : '{row['FilePath']}' from db")
return self.summary
@ -1066,7 +1057,7 @@ class Collection(SortMedias):
Sort files into appropriate folder
"""
# Check db
self._init_check_db(loc=loc)
self._init_check_db(loc)
path_format = self.opt['Path']['path_format']
self.log.debug(f'path_format: {path_format}')
@ -1198,43 +1189,35 @@ class Collection(SortMedias):
def edit_metadata(self, paths, keys, loc=None, overwrite=False):
"""Edit metadata and exif data for given key"""
self._init_check_db()
if self.db.sqlite.is_empty('metadata'):
self.init(loc)
for file_path, media in self.medias.get_medias_datas(paths, loc=loc):
result = False
media.metadata['file_path'] = os.path.relpath(file_path, self.root)
if not self.check_file(file_path):
self.log.error('Db data is not accurate run `ordigi update`')
sys.exit(1)
exif = WriteExif(
file_path,
media.metadata,
ignore_tags=self.opt['Exif']['ignore_tags'],
)
for key in keys:
print()
value = media.metadata[key]
if overwrite or not value:
print(f"FILE: '{file_path}'")
if overwrite and value:
if overwrite:
print(f"{key}: '{value}'")
if overwrite or not value:
# Prompt value for given key for file_path
answer = self.input.text(key)
# Check value
prompt = [
inquirer.Text('value', message=key),
]
answer = inquirer.prompt(prompt, theme=self.theme)
# answer = {'value': '03-12-2021 08:12:35'}
# Validate value
if key in ('date_original', 'date_created', 'date_modified'):
# Check date format
value = media.get_date_format(answer)
value = media.get_date_format(answer['value'])
else:
value = answer
while not value.isalnum():
if not value: break
value = answer['value']
if not value.isalnum():
print("Invalid entry, use alphanumeric chars")
value = inquirer.prompt(prompt, theme=self.theme)
result = False
if value:
media.metadata[key] = value
if key == 'location':
@ -1245,25 +1228,40 @@ class Collection(SortMedias):
media.set_location_from_coordinates(loc)
# Update exif data
if key == 'location':
result = exif.set_key_values(
'latitude', media.metadata['latitude']
if key in (
'date_original',
'album',
'title',
'latitude',
'location',
'longitude',
'latitude_ref',
'longitude_ref',
):
exif = WriteExif(
file_path,
media.metadata,
ignore_tags=self.opt['Exif']['ignore_tags'],
)
result = exif.set_key_values(
'longitude', media.metadata['longitude']
)
elif key in exif.get_tags().keys():
result = exif.set_key_values(key, value)
if key == 'location':
result = exif.set_key_values(
'latitude', media.metadata['latitude']
)
result = exif.set_key_values(
'longitude', media.metadata['longitude']
)
else:
result = exif.set_key_values(key, value)
# Update checksum
media.metadata['checksum'] = utils.checksum(file_path)
# Update checksum
media.metadata['checksum'] = utils.checksum(file_path)
# Update database
self.db.add_file_data(media.metadata)
# Update database
self.db.add_file_data(media.metadata)
if result:
self.summary.append('update', True, file_path)
else:
self.summary.append('update', False, file_path)
if result:
self.summary.append('update', True, file_path)
else:
self.summary.append('update', False, file_path)
return self.summary

View File

@ -46,16 +46,9 @@ def file_logger(logger, file, level=30):
logger.addHandler(handler)
def get_level(quiet=False, verbose=False, debug=False, num=None):
"""Return int logging level from command line args"""
if num and num.isnumeric():
def get_level(verbose):
"""Return int logging level from string"""
if verbose.isnumeric():
return int(verbose)
if debug:
return int(logging.getLevelName('DEBUG'))
if verbose:
return int(logging.getLevelName('INFO'))
if quiet:
return int(logging.getLevelName('ERROR'))
return int(logging.getLevelName('WARNING'))
return int(logging.getLevelName(verbose))

View File

@ -345,8 +345,11 @@ class Media(ReadExif):
sys.exit()
if not answers['date_list']:
answer = self.prompt.text("date")
return self.get_date_format(answer)
prompt = [
inquirer.Text('date_custom', message="date"),
]
answers = inquirer.prompt(prompt, theme=self.theme)
return self.get_date_format(answers['date_custom'])
return answers['date_list']
@ -464,12 +467,17 @@ class Media(ReadExif):
default=f'{album}',
),
]
prompt = [
inquirer.Text('custom', message="album"),
]
answers = inquirer.prompt(choices_list, theme=self.theme)
if not answers:
sys.exit()
if not answers['album']:
return self.input.text("album")
answers = inquirer.prompt(prompt, theme=self.theme)
return answers['custom']
return answers['album']
@ -638,7 +646,6 @@ class Medias:
paths,
root,
exif_options,
checksums=None,
db=None,
interactive=False,
):
@ -651,11 +658,6 @@ class Medias:
self.root = root
# Options
if checksums:
self.checksums = checksums
else:
self.checksums = {}
self.exif_opt = exif_options
self.ignore_tags = self.exif_opt['ignore_tags']
@ -682,14 +684,7 @@ class Medias:
return media
def get_media_data(self, file_path, src_dir, loc=None):
"""Get media class instance with metadata"""
if self.checksums and file_path in self.checksums.keys():
checksum = self.checksums[file_path]
else:
checksum = None
def get_media_data(self, file_path, src_dir, checksum=None, loc=None):
media = self.get_media(file_path, src_dir, checksum)
media.get_metadata(
self.root, loc, self.db.sqlite, self.exif_opt['cache']
@ -697,9 +692,9 @@ class Medias:
return media
def get_metadata(self, src_path, src_dir, loc=None):
def get_metadata(self, src_path, src_dir, checksum=None, loc=None):
"""Get metadata"""
return self.get_media_data(src_path, src_dir, loc).metadata
return self.get_media_data(src_path, src_dir, checksum, loc).metadata
def get_paths(self, src_dirs, imp=False):
"""Get paths"""

View File

@ -1,6 +1,5 @@
import inquirer
from blessed import Terminal
from colorama import init,Fore,Style,Back
term = Terminal()
@ -35,15 +34,6 @@ def load_theme():
return inquirer.themes.load_theme_from_dict(custom_theme)
class Input():
def __init__(self):
init()
def text(self, message):
return input(f'{Fore.BLUE}[{Fore.YELLOW}?{Fore.BLUE}]{Fore.WHITE} {message}: ')
# def edit_prompt(self, key: str, value: str) -> str:
# print(f"Date conflict for file: {self.file_path}")

View File

@ -5,7 +5,6 @@ import pytest
import inquirer
from ordigi import cli
from ordigi.request import Input
CONTENT = "content"
@ -27,7 +26,7 @@ class TestOrdigi:
def setup_class(cls, sample_files_paths):
cls.runner = CliRunner()
cls.src_path, cls.file_paths = sample_files_paths
cls.logger_options = ('--debug',)
cls.logger_options = (('--verbose', 'DEBUG'),)
cls.filter_options = (
('--ignore-tags', 'CreateDate'),
('--ext', 'jpg'),
@ -82,23 +81,21 @@ class TestOrdigi:
def test_edit(self, monkeypatch):
bool_options = (
*self.logger_options,
)
bool_options = ()
arg_options = (
*self.logger_options,
*self.filter_options,
)
def mockreturn(self, message):
return '03-12-2021 08:12:35'
def mockreturn(prompt, theme):
return {'value': '03-12-2021 08:12:35'}
monkeypatch.setattr(Input, 'text', mockreturn)
monkeypatch.setattr(inquirer, 'prompt', mockreturn)
args = (
'--key',
'date_original',
'--overwrite',
str(self.src_path.joinpath('test_exif/photo.png')),
str(self.src_path),
)
@ -110,7 +107,6 @@ class TestOrdigi:
def test_sort(self):
bool_options = (
*self.logger_options,
# '--interactive',
'--dry-run',
'--album-from-folder',
@ -121,6 +117,7 @@ class TestOrdigi:
)
arg_options = (
*self.logger_options,
*self.filter_options,
('--path-format', '{%Y}/{folder}/{name}.{ext}'),
@ -135,29 +132,36 @@ class TestOrdigi:
def test_clone(self, tmp_path):
arg_options = (
*self.logger_options,
)
paths = (str(self.src_path), str(tmp_path))
self.assert_cli(cli._init, [str(self.src_path)])
self.assert_cli(cli._clone, ['--dry-run', *self.logger_options, *paths])
self.assert_cli(cli._clone, ['--dry-run', '--verbose', 'DEBUG', *paths])
self.assert_cli(cli._clone, paths)
def assert_init(self):
self.assert_cli(cli._init, [*self.logger_options, str(self.src_path)])
for opt, arg in self.logger_options:
self.assert_cli(cli._init, [opt, arg, str(self.src_path)])
def assert_update(self):
file_path = Path(ORDIGI_PATH, 'samples/test_exif/photo.cr2')
dest_path = self.src_path / 'photo_moved.cr2'
shutil.copyfile(file_path, dest_path)
self.assert_cli(cli._update, [*self.logger_options, str(self.src_path)])
for opt, arg in self.logger_options:
self.assert_cli(cli._update, [opt, arg, str(self.src_path)])
self.assert_cli(cli._update, ['--checksum', str(self.src_path)])
def assert_check(self):
self.assert_cli(cli._check, [*self.logger_options, str(self.src_path)])
for opt, arg in self.logger_options:
self.assert_cli(cli._check, [opt, arg, str(self.src_path)])
def assert_clean(self):
bool_options = (
*self.logger_options,
# '--interactive',
'--dry-run',
'--delete-excluded',
@ -167,6 +171,7 @@ class TestOrdigi:
)
arg_options = (
*self.logger_options,
*self.filter_options,
('--dedup-regex', r'\d{4}-\d{2}'),
)
@ -187,7 +192,6 @@ class TestOrdigi:
def test_import(self, tmp_path):
bool_options = (
*self.logger_options,
# '--interactive',
'--dry-run',
'--album-from-folder',
@ -198,6 +202,7 @@ class TestOrdigi:
)
arg_options = (
*self.logger_options,
('--exclude', '.DS_Store'),
*self.filter_options,
('--path-format', '{%Y}/{folder}/{stem}.{ext}'),
@ -213,7 +218,6 @@ class TestOrdigi:
def test_compare(self):
bool_options = (
*self.logger_options,
# '--interactive',
'--dry-run',
'--find-duplicates',
@ -221,6 +225,7 @@ class TestOrdigi:
)
arg_options = (
*self.logger_options,
*self.filter_options,
# ('--similar-to', ''),
('--similarity', '65'),

View File

@ -8,14 +8,13 @@ import inquirer
from ordigi import LOG
from ordigi import constants
from ordigi import utils
from ordigi.summary import Summary
from ordigi.collection import Collection, FPath, Paths
from ordigi.exiftool import ExifTool, ExifToolCaching, exiftool_is_running, terminate_exiftool
from ordigi.geolocation import GeoLocation
from ordigi.media import Media, ReadExif
from ordigi.request import Input
from ordigi import utils
from .conftest import randomize_files, randomize_db
from ordigi.summary import Summary
LOG.setLevel(10)
@ -170,7 +169,7 @@ class TestCollection:
path_format = 'test_exif/<city>/<%Y>-<name>.%l<ext>'
summary = collection.sort_files([tmp_path], loc)
self.assert_sort(summary, 23)
self.assert_sort(summary, 26)
shutil.copytree(tmp_path / 'test_exif', tmp_path / 'test_exif_copy')
collection.summary = Summary(tmp_path)
@ -258,10 +257,10 @@ class TestCollection:
shutil.copytree(self.src_path, path)
collection = Collection(path, {'cache': False})
def mockreturn(self, message):
return '03-12-2021 08:12:35'
def mockreturn(prompt, theme):
return {'value': '03-12-2021 08:12:35'}
monkeypatch.setattr(Input, 'text', mockreturn)
monkeypatch.setattr(inquirer, 'prompt', mockreturn)
collection.edit_metadata({path}, {'date_original'}, overwrite=True)
# check if db value is set
@ -279,10 +278,10 @@ class TestCollection:
collection = Collection(path, {'cache': False})
loc = GeoLocation()
def mockreturn(self, message):
return 'lyon'
def mockreturn(prompt, theme):
return {'value': 'lyon'}
monkeypatch.setattr(Input, 'text', mockreturn)
monkeypatch.setattr(inquirer, 'prompt', mockreturn)
collection.edit_metadata({path}, {'location'}, loc, True)
# check if db value is set

111
todo.md Executable file
View File

@ -0,0 +1,111 @@
# NOW
- db integrity have not to be checked in media but in collection??
- build structure to store file path and info with metadata
metadatas[file_path] = {'checksum': value}. Init must select same files than
get_metadatata
- check edit_metadata again test with valid doc
- show exif metadata
- print all values and select some to edit
- dry run = no changes
- compare custom output folder similar to?
- ordigi-gui
- add name and dirpath options???
# TODO
Options:
--location --time
# -f overwrite metadata
--auto|-a: a set of option: geolocalisation, best match date, rename, album
from folder...
# --keep-folder option
# --rename
--confirm unsure operation
# Bugs
- summary
- set date original???, interactive mode...
- Faire en sorte que le programme ne plante pas...
- option to not update exif metadata...
## Exiftools
https://gitlab.com/TNThieding/exif
exiftool -akljklbum=tjkljkestjlj /tmp/pytest-of-cedric/pytest-12/test_sort_files0/2008-10-Oct/test_exif/2008-10-24_09-12-56-photo.nef
exiftool -album=tjkljkestjlj /tmp/pytest-of-cedric/pytest-12/test_sort_files0/2008-10-Oct/test_exif/2008-10-24_09-12-56-photo.nef
1 image files updated
Get result code....
## Doc use sphinx??
## Commands
- ordigi view/show
- ordigi search
- use tree to show paths?
# Pylint
https://pythonspeed.com/articles/pylint/
use config file
# Media:
# Test:
# enhancement
- summary: replace success by copied/moved/deleted
## Alias
alias ogi=ordigi
## Image analysis
https://pypi.org/project/google-cloud-vision/
https://googleapis.dev/python/vision/latest/index.html
https://www.datacamp.com/community/tutorials/beginner-guide-google-vision-api
## Album form folder
# Update
https://github.com/JohannesBuchner/imagehash
https://github.com/cw-somil/Duplicate-Remover
https://leons.im/posts/a-python-implementation-of-simhash-algorithm/
Visualy check similar image
https://www.pluralsight.com/guides/importing-image-data-into-numpy-arrays
https://stackoverflow.com/questions/56056054/add-check-boxes-to-scrollable-image-in-python
https://wellsr.com/python/python-image-manipulation-with-pillow-library/
kitty gird image?
https://fr.wikibooks.org/wiki/PyQt/PyQt_versus_wxPython
https://docs.python.org/3/faq/gui.html
https://docs.opencv.org/3.4/d3/df2/tutorial_py_basic_ops.html
https://stackoverflow.com/questions/52727332/python-tkinter-create-checkbox-list-from-listbox
Image gird method:
matplot
https://gist.github.com/lebedov/7018889ba47668c64bcf96aee82caec0
Tkinter
https://python-forum.io/thread-22700.html
https://stackoverflow.com/questions/43326282/how-can-i-use-images-in-a-tkinter-grid
wxwidget
https://wxpython.org/Phoenix/docs/html/wx.lib.agw.thumbnailctrl.html
Ability to change metadata to selection
Fix: change versvalidion number to 0.x99
https://github.com/andrewning/sortphotos/blob/master/src/sortphotos.py
# AFTER

30
workflow.md Normal file
View File

@ -0,0 +1,30 @@
# Create virtual environment
nmkvirtualenv ordigi
# Work on it (activate and cd)
workon ordigi
# Install required dependencies
pip install -r requirements.txt
# Liked it to path
pip install -e .
# View file tree of path
tree /dest/path
# Test code
pylint ordigi/* -E
pylint ordigi/**
pytest --cov=ordigi --cov-report html tests/*.py
pip install --prefix=~/.local -e .
# config
## Path format
dirs_path=<%Y>/<%m-%b>-<city>-<folder>
name=<%Y%m%d-%H%M%S>-%u<original_name>|%u<basename>.%l<ext>
## run
ordigi import 220719.bkp -f -c -R collection