Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
Cédric Leporcq | 795ef36da5 | |
Cédric Leporcq | f5faa70bc8 | |
Cédric Leporcq | 81462677a7 | |
Cédric Leporcq | 6d040b4ba6 | |
Cédric Leporcq | f21331889c | |
Cédric Leporcq | 501ab496ff | |
Cédric Leporcq | ed9fd7cc21 | |
Cédric Leporcq | 16e62cd451 | |
Cédric Leporcq | 3baa184a17 | |
Cédric Leporcq | dde2f4f66f | |
Cédric Leporcq | 22e87223a3 | |
Cédric Leporcq | 34e58d6a0f | |
Cédric Leporcq | e0509cdb57 | |
Cédric Leporcq | b571ba6468 | |
Cédric Leporcq | f8ae11c57f |
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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']
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
Loading…
Reference in New Issue