Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
|
1a716f9b94 | |
|
a45c11b216 | |
|
64018561f2 | |
|
c28496c602 | |
|
a72d0b0f2c | |
|
b9c30a4d85 | |
|
3bd06f0321 | |
|
08aaa0c5c4 | |
|
11ad41da09 | |
|
817b84152f |
|
@ -1,36 +0,0 @@
|
||||||
"""
|
|
||||||
The audio module contains classes specifically for dealing with audio files.
|
|
||||||
The :class:`Audio` class inherits from the :class:`~dozo.media.Media`
|
|
||||||
class.
|
|
||||||
|
|
||||||
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from .media import Media
|
|
||||||
|
|
||||||
|
|
||||||
class Audio(Media):
|
|
||||||
|
|
||||||
"""An audio object.
|
|
||||||
|
|
||||||
:param str source: The fully qualified path to the audio file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__name__ = 'Audio'
|
|
||||||
|
|
||||||
#: Valid extensions for audio files.
|
|
||||||
extensions = ('m4a',)
|
|
||||||
|
|
||||||
def __init__(self, source=None, ignore_tags=set()):
|
|
||||||
super().__init__(source, ignore_tags=set())
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""Check the file extension against valid file extensions.
|
|
||||||
|
|
||||||
The list of valid file extensions come from self.extensions.
|
|
||||||
|
|
||||||
:returns: bool
|
|
||||||
"""
|
|
||||||
source = self.source
|
|
||||||
return os.path.splitext(source)[1][1:].lower() in self.extensions
|
|
|
@ -1,43 +0,0 @@
|
||||||
"""
|
|
||||||
The video module contains the :class:`Video` class, which represents video
|
|
||||||
objects (AVI, MOV, etc.).
|
|
||||||
|
|
||||||
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# load modules
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .media import Media
|
|
||||||
|
|
||||||
|
|
||||||
class Video(Media):
|
|
||||||
|
|
||||||
"""A video object.
|
|
||||||
|
|
||||||
:param str source: The fully qualified path to the video file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__name__ = 'Video'
|
|
||||||
|
|
||||||
#: Valid extensions for video files.
|
|
||||||
extensions = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts')
|
|
||||||
|
|
||||||
def __init__(self, source=None, ignore_tags=set()):
|
|
||||||
super().__init__(source, ignore_tags=set())
|
|
||||||
# self.set_gps_ref = False
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""Check the file extension against valid file extensions.
|
|
||||||
|
|
||||||
The list of valid file extensions come from self.extensions.
|
|
||||||
|
|
||||||
:returns: bool
|
|
||||||
"""
|
|
||||||
source = self.source
|
|
||||||
return os.path.splitext(source)[1][1:].lower() in self.extensions
|
|
|
@ -5,8 +5,8 @@
|
||||||
# be a number between 0-23')
|
# be a number between 0-23')
|
||||||
day_begins=4
|
day_begins=4
|
||||||
|
|
||||||
dirs_path=%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}
|
dirs_path={%Y}/{%m-%b}-{city}-{folder}
|
||||||
name={%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}
|
name={%Y%m%d-%H%M%S}-%u{original_name}.%l{ext}
|
||||||
|
|
||||||
[Exclusions]
|
[Exclusions]
|
||||||
name1=.directory
|
name1=.directory
|
|
@ -3,18 +3,17 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from send2trash import send2trash
|
|
||||||
|
|
||||||
from dozo import constants
|
from ordigi import config
|
||||||
from dozo import config
|
from ordigi import constants
|
||||||
from dozo.filesystem import FileSystem
|
from ordigi import log
|
||||||
from dozo.database import Db
|
from ordigi.database import Db
|
||||||
from dozo.media.media import Media, get_all_subclasses
|
from ordigi.filesystem import FileSystem
|
||||||
from dozo.summary import Summary
|
from ordigi.media import Media, get_all_subclasses
|
||||||
|
from ordigi.summary import Summary
|
||||||
|
|
||||||
FILESYSTEM = FileSystem()
|
FILESYSTEM = FileSystem()
|
||||||
|
|
||||||
|
@ -34,22 +33,6 @@ def _batch(debug):
|
||||||
plugins.run_batch()
|
plugins.run_batch()
|
||||||
|
|
||||||
|
|
||||||
def get_logger(verbose, debug):
|
|
||||||
if debug:
|
|
||||||
level = logging.DEBUG
|
|
||||||
elif verbose:
|
|
||||||
level = logging.INFO
|
|
||||||
else:
|
|
||||||
level = logging.WARNING
|
|
||||||
|
|
||||||
logging.basicConfig(format='%(levelname)s:%(message)s', level=level)
|
|
||||||
logging.debug('This message should appear on the console')
|
|
||||||
logging.info('So should this')
|
|
||||||
logging.getLogger('asyncio').setLevel(level)
|
|
||||||
logger = logging.getLogger('dozo')
|
|
||||||
logger.level = level
|
|
||||||
return logger
|
|
||||||
|
|
||||||
@click.command('sort')
|
@click.command('sort')
|
||||||
@click.option('--debug', default=False, is_flag=True,
|
@click.option('--debug', default=False, is_flag=True,
|
||||||
help='Override the value in constants.py with True.')
|
help='Override the value in constants.py with True.')
|
||||||
|
@ -57,6 +40,8 @@ def get_logger(verbose, debug):
|
||||||
help='Dry run only, no change made to the filesystem.')
|
help='Dry run only, no change made to the filesystem.')
|
||||||
@click.option('--destination', '-d', type=click.Path(file_okay=False),
|
@click.option('--destination', '-d', type=click.Path(file_okay=False),
|
||||||
default=None, help='Sort files into this directory.')
|
default=None, help='Sort files into this directory.')
|
||||||
|
@click.option('--clean', '-C', default=False, is_flag=True,
|
||||||
|
help='Clean empty folders')
|
||||||
@click.option('--copy', '-c', default=False, is_flag=True,
|
@click.option('--copy', '-c', default=False, is_flag=True,
|
||||||
help='True if you want files to be copied over from src_dir to\
|
help='True if you want files to be copied over from src_dir to\
|
||||||
dest_dir rather than moved')
|
dest_dir rather than moved')
|
||||||
|
@ -79,10 +64,10 @@ def get_logger(verbose, debug):
|
||||||
@click.option('--verbose', '-v', default=False, is_flag=True,
|
@click.option('--verbose', '-v', default=False, is_flag=True,
|
||||||
help='True if you want to see details of file processing')
|
help='True if you want to see details of file processing')
|
||||||
@click.argument('paths', required=True, nargs=-1, type=click.Path())
|
@click.argument('paths', required=True, nargs=-1, type=click.Path())
|
||||||
def _sort(debug, dry_run, destination, copy, exclude_regex, filter_by_ext, ignore_tags,
|
def _sort(debug, dry_run, destination, clean, copy, exclude_regex, filter_by_ext, ignore_tags,
|
||||||
max_deep, remove_duplicates, reset_cache, verbose, paths):
|
max_deep, remove_duplicates, reset_cache, verbose, paths):
|
||||||
"""Sort files or directories by reading their EXIF and organizing them
|
"""Sort files or directories by reading their EXIF and organizing them
|
||||||
according to config.ini preferences.
|
according to ordigi.conf preferences.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if copy:
|
if copy:
|
||||||
|
@ -90,7 +75,7 @@ def _sort(debug, dry_run, destination, copy, exclude_regex, filter_by_ext, ignor
|
||||||
else:
|
else:
|
||||||
mode = 'move'
|
mode = 'move'
|
||||||
|
|
||||||
logger = get_logger(verbose, debug)
|
logger = log.get_logger(verbose, debug)
|
||||||
|
|
||||||
if max_deep is not None:
|
if max_deep is not None:
|
||||||
max_deep = int(max_deep)
|
max_deep = int(max_deep)
|
||||||
|
@ -106,6 +91,8 @@ def _sort(debug, dry_run, destination, copy, exclude_regex, filter_by_ext, ignor
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
paths = set(paths)
|
paths = set(paths)
|
||||||
|
filter_by_ext = set(filter_by_ext)
|
||||||
|
|
||||||
destination = os.path.abspath(os.path.expanduser(destination))
|
destination = os.path.abspath(os.path.expanduser(destination))
|
||||||
|
|
||||||
if not os.path.exists(destination):
|
if not os.path.exists(destination):
|
||||||
|
@ -124,17 +111,21 @@ def _sort(debug, dry_run, destination, copy, exclude_regex, filter_by_ext, ignor
|
||||||
# Initialize Db
|
# Initialize Db
|
||||||
db = Db(destination)
|
db = Db(destination)
|
||||||
|
|
||||||
if 'Directory' in conf and 'day_begins' in conf['Directory']:
|
if 'Path' in conf and 'day_begins' in conf['Path']:
|
||||||
config_directory = conf['Directory']
|
config_directory = conf['Path']
|
||||||
day_begins = config_directory['day_begins']
|
day_begins = int(config_directory['day_begins'])
|
||||||
else:
|
else:
|
||||||
day_begins = 0
|
day_begins = 0
|
||||||
filesystem = FileSystem(cache, day_begins, dry_run, exclude_regex_list,
|
filesystem = FileSystem(cache, day_begins, dry_run, exclude_regex_list,
|
||||||
filter_by_ext, logger, max_deep, mode, path_format)
|
filter_by_ext, logger, max_deep, mode, path_format)
|
||||||
|
|
||||||
|
import ipdb; ipdb.set_trace()
|
||||||
summary, has_errors = filesystem.sort_files(paths, destination, db,
|
summary, has_errors = filesystem.sort_files(paths, destination, db,
|
||||||
remove_duplicates, ignore_tags)
|
remove_duplicates, ignore_tags)
|
||||||
|
|
||||||
|
if clean:
|
||||||
|
remove_empty_folders(destination, logger)
|
||||||
|
|
||||||
if verbose or debug:
|
if verbose or debug:
|
||||||
summary.write()
|
summary.write()
|
||||||
|
|
||||||
|
@ -142,13 +133,49 @@ def _sort(debug, dry_run, destination, copy, exclude_regex, filter_by_ext, ignor
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_empty_folders(path, logger, remove_root=True):
|
||||||
|
'Function to remove empty folders'
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
# remove empty subfolders
|
||||||
|
files = os.listdir(path)
|
||||||
|
if len(files):
|
||||||
|
for f in files:
|
||||||
|
fullpath = os.path.join(path, f)
|
||||||
|
if os.path.isdir(fullpath):
|
||||||
|
remove_empty_folders(fullpath, logger)
|
||||||
|
|
||||||
|
# if folder empty, delete it
|
||||||
|
files = os.listdir(path)
|
||||||
|
if len(files) == 0 and remove_root:
|
||||||
|
logger.info(f"Removing empty folder: {path}")
|
||||||
|
os.rmdir(path)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command('clean')
|
||||||
|
@click.option('--debug', default=False, is_flag=True,
|
||||||
|
help='Override the value in constants.py with True.')
|
||||||
|
@click.option('--verbose', '-v', default=False, is_flag=True,
|
||||||
|
help='True if you want to see details of file processing')
|
||||||
|
@click.argument('path', required=True, nargs=1, type=click.Path())
|
||||||
|
def _clean(debug, verbose, path):
|
||||||
|
"""Remove empty folders
|
||||||
|
Usage: clean [--verbose|--debug] directory [removeRoot]"""
|
||||||
|
|
||||||
|
logger = log.get_logger(verbose, debug)
|
||||||
|
|
||||||
|
remove_empty_folders(path, logger)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.command('generate-db')
|
@click.command('generate-db')
|
||||||
@click.option('--path', type=click.Path(file_okay=False),
|
@click.option('--path', type=click.Path(file_okay=False),
|
||||||
required=True, help='Path of your photo library.')
|
required=True, help='Path of your photo library.')
|
||||||
@click.option('--debug', default=False, is_flag=True,
|
@click.option('--debug', default=False, is_flag=True,
|
||||||
help='Override the value in constants.py with True.')
|
help='Override the value in constants.py with True.')
|
||||||
def _generate_db(path, debug):
|
def _generate_db(path, debug):
|
||||||
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files. The hash.json file is located at ~/.dozo/.
|
"""Regenerate the hash.json database which contains all of the sha256 signatures of media files.
|
||||||
"""
|
"""
|
||||||
constants.debug = debug
|
constants.debug = debug
|
||||||
result = Result()
|
result = Result()
|
||||||
|
@ -221,7 +248,7 @@ def _compare(debug, dry_run, find_duplicates, output_dir, remove_duplicates,
|
||||||
revert_compare, similar_to, similarity, verbose, path):
|
revert_compare, similar_to, similarity, verbose, path):
|
||||||
'''Compare files in directories'''
|
'''Compare files in directories'''
|
||||||
|
|
||||||
logger = get_logger(verbose, debug)
|
logger = log.get_logger(verbose, debug)
|
||||||
|
|
||||||
# Initialize Db
|
# Initialize Db
|
||||||
db = Db(path)
|
db = Db(path)
|
||||||
|
@ -247,6 +274,7 @@ def main():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
main.add_command(_clean)
|
||||||
main.add_command(_compare)
|
main.add_command(_compare)
|
||||||
main.add_command(_sort)
|
main.add_command(_sort)
|
||||||
main.add_command(_generate_db)
|
main.add_command(_generate_db)
|
|
@ -1,7 +1,7 @@
|
||||||
"""Load config file as a singleton."""
|
"""Load config file as a singleton."""
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from os import path
|
from os import path
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
|
|
||||||
|
|
||||||
def write(conf_file, config):
|
def write(conf_file, config):
|
|
@ -8,8 +8,15 @@ from sys import version_info
|
||||||
#: If True, debug messages will be printed.
|
#: If True, debug messages will be printed.
|
||||||
debug = False
|
debug = False
|
||||||
|
|
||||||
#: Directory in which to store Dozo settings.
|
#Ordigi settings directory.
|
||||||
application_directory = '{}/.dozo'.format(path.expanduser('~'))
|
if 'XDG_CONFIG_HOME' in environ:
|
||||||
|
confighome = environ['XDG_CONFIG_HOME']
|
||||||
|
elif 'APPDATA' in environ:
|
||||||
|
confighome = environ['APPDATA']
|
||||||
|
else:
|
||||||
|
confighome = path.join(environ['HOME'], '.config')
|
||||||
|
application_directory = path.join(confighome, 'ordigi')
|
||||||
|
|
||||||
default_path = '{%Y-%m-%b}/{album}|{city}|{"Unknown Location"}'
|
default_path = '{%Y-%m-%b}/{album}|{city}|{"Unknown Location"}'
|
||||||
default_name = '{%Y-%m-%d_%H-%M-%S}-{name}-{title}.%l{ext}'
|
default_name = '{%Y-%m-%d_%H-%M-%S}-{name}-{title}.%l{ext}'
|
||||||
default_geocoder = 'Nominatim'
|
default_geocoder = 'Nominatim'
|
||||||
|
@ -23,7 +30,7 @@ location_db = 'location.json'
|
||||||
# TODO will be removed eventualy later
|
# TODO will be removed eventualy later
|
||||||
# location_db = '{}/location.json'.format(application_directory)
|
# location_db = '{}/location.json'.format(application_directory)
|
||||||
|
|
||||||
# Dozo installation directory.
|
# Ordigi installation directory.
|
||||||
script_directory = path.dirname(path.dirname(path.abspath(__file__)))
|
script_directory = path.dirname(path.dirname(path.abspath(__file__)))
|
||||||
|
|
||||||
#: Accepted language in responses from MapQuest
|
#: Accepted language in responses from MapQuest
|
||||||
|
@ -32,4 +39,4 @@ accepted_language = 'en'
|
||||||
# check python version, required in filesystem.py to trigger appropriate method
|
# check python version, required in filesystem.py to trigger appropriate method
|
||||||
python_version = version_info.major
|
python_version = version_info.major
|
||||||
|
|
||||||
CONFIG_FILE = '%s/config.ini' % application_directory
|
CONFIG_FILE = f'{application_directory}/ordigi.conf'
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Methods for interacting with information Dozo caches about stored media.
|
Methods for interacting with database files
|
||||||
"""
|
"""
|
||||||
from builtins import map
|
from builtins import map
|
||||||
from builtins import object
|
from builtins import object
|
||||||
|
@ -12,23 +12,17 @@ from math import radians, cos, sqrt
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from time import strftime
|
from time import strftime
|
||||||
|
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
|
|
||||||
|
|
||||||
class Db(object):
|
class Db(object):
|
||||||
|
|
||||||
"""A class for interacting with the JSON files created by Dozo."""
|
"""A class for interacting with the JSON files database."""
|
||||||
|
|
||||||
def __init__(self, target_dir):
|
def __init__(self, target_dir):
|
||||||
# verify that the application directory (~/.dozo) exists,
|
|
||||||
# else create it
|
|
||||||
# if not os.path.exists(constants.application_directory):
|
|
||||||
# os.makedirs(constants.application_directory)
|
|
||||||
|
|
||||||
# Create dir for target database
|
# Create dir for target database
|
||||||
dirname = os.path.join(target_dir, '.dozo')
|
dirname = os.path.join(target_dir, '.ordigi')
|
||||||
# Legacy dir
|
|
||||||
# dirname = constants.application_directory
|
|
||||||
|
|
||||||
if not os.path.exists(dirname):
|
if not os.path.exists(dirname):
|
||||||
try:
|
try:
|
||||||
|
@ -89,7 +83,7 @@ class Db(object):
|
||||||
# structure might be needed. Some speed up ideas:
|
# structure might be needed. Some speed up ideas:
|
||||||
# - Sort it and inter-half method can be used
|
# - Sort it and inter-half method can be used
|
||||||
# - Use integer part of long or lat as key to get a lower search list
|
# - Use integer part of long or lat as key to get a lower search list
|
||||||
# - Cache a small number of lookups, photos are likely to be taken in
|
# - Cache a small number of lookups, images are likely to be taken in
|
||||||
# clusters around a spot during import.
|
# clusters around a spot during import.
|
||||||
def add_location(self, latitude, longitude, place, write=False):
|
def add_location(self, latitude, longitude, place, write=False):
|
||||||
"""Add a location to the database.
|
"""Add a location to the database.
|
|
@ -14,25 +14,32 @@ import shutil
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
from dozo import geolocation
|
from ordigi import geolocation
|
||||||
|
|
||||||
from dozo.media.media import get_media_class, get_all_subclasses
|
from ordigi import media
|
||||||
from dozo.media.photo import Photo
|
from ordigi.media import Media, get_all_subclasses
|
||||||
from dozo.summary import Summary
|
from ordigi.images import Images
|
||||||
|
from ordigi.summary import Summary
|
||||||
|
|
||||||
|
|
||||||
class FileSystem(object):
|
class FileSystem(object):
|
||||||
"""A class for interacting with the file system."""
|
"""A class for interacting with the file system."""
|
||||||
|
|
||||||
def __init__(self, cache=False, day_begins=0, dry_run=False, exclude_regex_list=set(),
|
def __init__(self, cache=False, day_begins=0, dry_run=False, exclude_regex_list=set(),
|
||||||
filter_by_ext=(), logger=logging.getLogger(), max_deep=None,
|
filter_by_ext=set(), logger=logging.getLogger(), max_deep=None,
|
||||||
mode='copy', path_format=None):
|
mode='copy', path_format=None):
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.day_begins = day_begins
|
self.day_begins = day_begins
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
self.exclude_regex_list = exclude_regex_list
|
self.exclude_regex_list = exclude_regex_list
|
||||||
self.filter_by_ext = filter_by_ext
|
|
||||||
|
if '%media' in filter_by_ext:
|
||||||
|
filter_by_ext.remove('%media')
|
||||||
|
self.filter_by_ext = filter_by_ext.union(media.extensions)
|
||||||
|
else:
|
||||||
|
self.filter_by_ext = filter_by_ext
|
||||||
|
|
||||||
self.items = self.get_items()
|
self.items = self.get_items()
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.max_deep = max_deep
|
self.max_deep = max_deep
|
||||||
|
@ -69,7 +76,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_items(self):
|
def get_items(self):
|
||||||
return {
|
return {
|
||||||
'album': '{album}',
|
'album': '{album}',
|
||||||
|
@ -91,7 +97,6 @@ class FileSystem(object):
|
||||||
'date': '{(%[a-zA-Z][^a-zA-Z]*){1,8}}' # search for date format string
|
'date': '{(%[a-zA-Z][^a-zA-Z]*){1,8}}' # search for date format string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def walklevel(self, src_path, maxlevel=None):
|
def walklevel(self, src_path, maxlevel=None):
|
||||||
"""
|
"""
|
||||||
Walk into input directory recursively until desired maxlevel
|
Walk into input directory recursively until desired maxlevel
|
||||||
|
@ -108,7 +113,6 @@ class FileSystem(object):
|
||||||
if maxlevel is not None and level >= maxlevel:
|
if maxlevel is not None and level >= maxlevel:
|
||||||
del dirs[:]
|
del dirs[:]
|
||||||
|
|
||||||
|
|
||||||
def get_all_files(self, path, extensions=False, exclude_regex_list=set()):
|
def get_all_files(self, path, extensions=False, exclude_regex_list=set()):
|
||||||
"""Recursively get all files which match a path and extension.
|
"""Recursively get all files which match a path and extension.
|
||||||
|
|
||||||
|
@ -129,7 +133,7 @@ class FileSystem(object):
|
||||||
# Create a list of compiled regular expressions to match against the file path
|
# Create a list of compiled regular expressions to match against the file path
|
||||||
compiled_regex_list = [re.compile(regex) for regex in exclude_regex_list]
|
compiled_regex_list = [re.compile(regex) for regex in exclude_regex_list]
|
||||||
for dirname, dirnames, filenames in os.walk(path):
|
for dirname, dirnames, filenames in os.walk(path):
|
||||||
if dirname == os.path.join(path, '.dozo'):
|
if dirname == os.path.join(path, '.ordigi'):
|
||||||
continue
|
continue
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
# If file extension is in `extensions`
|
# If file extension is in `extensions`
|
||||||
|
@ -143,7 +147,6 @@ class FileSystem(object):
|
||||||
):
|
):
|
||||||
yield filename_path
|
yield filename_path
|
||||||
|
|
||||||
|
|
||||||
def check_for_early_morning_photos(self, date):
|
def check_for_early_morning_photos(self, date):
|
||||||
"""check for early hour photos to be grouped with previous day"""
|
"""check for early hour photos to be grouped with previous day"""
|
||||||
|
|
||||||
|
@ -154,7 +157,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return date
|
return date
|
||||||
|
|
||||||
|
|
||||||
def get_location_part(self, mask, part, place_name):
|
def get_location_part(self, mask, part, place_name):
|
||||||
"""Takes a mask for a location and interpolates the actual place names.
|
"""Takes a mask for a location and interpolates the actual place names.
|
||||||
|
|
||||||
|
@ -187,7 +189,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return folder_name
|
return folder_name
|
||||||
|
|
||||||
|
|
||||||
def get_part(self, item, mask, metadata, db, subdirs):
|
def get_part(self, item, mask, metadata, db, subdirs):
|
||||||
"""Parse a specific folder's name given a mask and metadata.
|
"""Parse a specific folder's name given a mask and metadata.
|
||||||
|
|
||||||
|
@ -275,15 +276,20 @@ class FileSystem(object):
|
||||||
|
|
||||||
part = part.strip()
|
part = part.strip()
|
||||||
|
|
||||||
# Capitalization
|
if part == '':
|
||||||
u_regex = '%u' + regex
|
# delete separator if any
|
||||||
l_regex = '%l' + regex
|
regex = '[-_ .]?(%[ul])?' + regex
|
||||||
if re.search(u_regex, this_part):
|
|
||||||
this_part = re.sub(u_regex, part.upper(), this_part)
|
|
||||||
elif re.search(l_regex, this_part):
|
|
||||||
this_part = re.sub(l_regex, part.lower(), this_part)
|
|
||||||
else:
|
|
||||||
this_part = re.sub(regex, part, this_part)
|
this_part = re.sub(regex, part, this_part)
|
||||||
|
else:
|
||||||
|
# Capitalization
|
||||||
|
u_regex = '%u' + regex
|
||||||
|
l_regex = '%l' + regex
|
||||||
|
if re.search(u_regex, this_part):
|
||||||
|
this_part = re.sub(u_regex, part.upper(), this_part)
|
||||||
|
elif re.search(l_regex, this_part):
|
||||||
|
this_part = re.sub(l_regex, part.lower(), this_part)
|
||||||
|
else:
|
||||||
|
this_part = re.sub(regex, part, this_part)
|
||||||
|
|
||||||
|
|
||||||
if this_part:
|
if this_part:
|
||||||
|
@ -404,7 +410,6 @@ class FileSystem(object):
|
||||||
elif metadata['date_modified'] is not None:
|
elif metadata['date_modified'] is not None:
|
||||||
return metadata['date_modified']
|
return metadata['date_modified']
|
||||||
|
|
||||||
|
|
||||||
def checksum(self, file_path, blocksize=65536):
|
def checksum(self, file_path, blocksize=65536):
|
||||||
"""Create a hash value for the given file.
|
"""Create a hash value for the given file.
|
||||||
|
|
||||||
|
@ -425,7 +430,6 @@ class FileSystem(object):
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def checkcomp(self, dest_path, src_checksum):
|
def checkcomp(self, dest_path, src_checksum):
|
||||||
"""Check file.
|
"""Check file.
|
||||||
"""
|
"""
|
||||||
|
@ -442,7 +446,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return src_checksum
|
return src_checksum
|
||||||
|
|
||||||
|
|
||||||
def sort_file(self, src_path, dest_path, remove_duplicates=True):
|
def sort_file(self, src_path, dest_path, remove_duplicates=True):
|
||||||
'''Copy or move file to dest_path.'''
|
'''Copy or move file to dest_path.'''
|
||||||
|
|
||||||
|
@ -452,8 +455,8 @@ class FileSystem(object):
|
||||||
# check for collisions
|
# check for collisions
|
||||||
if(src_path == dest_path):
|
if(src_path == dest_path):
|
||||||
self.logger.info(f'File {dest_path} already sorted')
|
self.logger.info(f'File {dest_path} already sorted')
|
||||||
return True
|
return None
|
||||||
if os.path.isfile(dest_path):
|
elif os.path.isfile(dest_path):
|
||||||
self.logger.info(f'File {dest_path} already exist')
|
self.logger.info(f'File {dest_path} already exist')
|
||||||
if remove_duplicates:
|
if remove_duplicates:
|
||||||
if filecmp.cmp(src_path, dest_path):
|
if filecmp.cmp(src_path, dest_path):
|
||||||
|
@ -462,7 +465,7 @@ class FileSystem(object):
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
os.remove(src_path)
|
os.remove(src_path)
|
||||||
self.logger.info(f'remove: {src_path}')
|
self.logger.info(f'remove: {src_path}')
|
||||||
return True
|
return None
|
||||||
else: # name is same, but file is different
|
else: # name is same, but file is different
|
||||||
self.logger.info(f'File in source and destination are different.')
|
self.logger.info(f'File in source and destination are different.')
|
||||||
return False
|
return False
|
||||||
|
@ -480,9 +483,6 @@ class FileSystem(object):
|
||||||
self.logger.info(f'copy: {src_path} -> {dest_path}')
|
self.logger.info(f'copy: {src_path} -> {dest_path}')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_file(self, src_path, dest_path, src_checksum, db):
|
def check_file(self, src_path, dest_path, src_checksum, db):
|
||||||
|
|
||||||
# Check if file remain the same
|
# Check if file remain the same
|
||||||
|
@ -493,9 +493,6 @@ class FileSystem(object):
|
||||||
db.add_hash(checksum, dest_path)
|
db.add_hash(checksum, dest_path)
|
||||||
db.update_hash_db()
|
db.update_hash_db()
|
||||||
|
|
||||||
if dest_path:
|
|
||||||
self.logger.info(f'{src_path} -> {dest_path}')
|
|
||||||
|
|
||||||
self.summary.append((src_path, dest_path))
|
self.summary.append((src_path, dest_path))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -506,24 +503,13 @@ class FileSystem(object):
|
||||||
|
|
||||||
return self.summary, has_errors
|
return self.summary, has_errors
|
||||||
|
|
||||||
|
def get_files_in_path(self, path, extensions=set()):
|
||||||
def get_files_in_path(self, path, extensions=False):
|
|
||||||
"""Recursively get files which match a path and extension.
|
"""Recursively get files which match a path and extension.
|
||||||
|
|
||||||
:param str path string: Path to start recursive file listing
|
:param str path string: Path to start recursive file listing
|
||||||
:param tuple(str) extensions: File extensions to include (whitelist)
|
:param tuple(str) extensions: File extensions to include (whitelist)
|
||||||
:returns: file_path, subdirs
|
:returns: file_path, subdirs
|
||||||
"""
|
"""
|
||||||
if self.filter_by_ext != () and not extensions:
|
|
||||||
# Filtering files by extensions.
|
|
||||||
if '%media' in self.filter_by_ext:
|
|
||||||
extensions = set()
|
|
||||||
subclasses = get_all_subclasses()
|
|
||||||
for cls in subclasses:
|
|
||||||
extensions.update(cls.extensions)
|
|
||||||
else:
|
|
||||||
extensions = self.filter_by_ext
|
|
||||||
|
|
||||||
file_list = set()
|
file_list = set()
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
if not self.should_exclude(path, self.exclude_regex_list, True):
|
if not self.should_exclude(path, self.exclude_regex_list, True):
|
||||||
|
@ -535,7 +521,7 @@ class FileSystem(object):
|
||||||
subdirs = ''
|
subdirs = ''
|
||||||
for dirname, dirnames, filenames, level in self.walklevel(path,
|
for dirname, dirnames, filenames, level in self.walklevel(path,
|
||||||
self.max_deep):
|
self.max_deep):
|
||||||
if dirname == os.path.join(path, '.dozo'):
|
if dirname == os.path.join(path, '.ordigi'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
subdirs = os.path.join(subdirs, os.path.basename(dirname))
|
subdirs = os.path.join(subdirs, os.path.basename(dirname))
|
||||||
|
@ -546,7 +532,7 @@ class FileSystem(object):
|
||||||
# Then append to the list
|
# Then append to the list
|
||||||
filename_path = os.path.join(dirname, filename)
|
filename_path = os.path.join(dirname, filename)
|
||||||
if (
|
if (
|
||||||
extensions == False
|
extensions == set()
|
||||||
or os.path.splitext(filename)[1][1:].lower() in extensions
|
or os.path.splitext(filename)[1][1:].lower() in extensions
|
||||||
and not self.should_exclude(filename_path, compiled_regex_list, False)
|
and not self.should_exclude(filename_path, compiled_regex_list, False)
|
||||||
):
|
):
|
||||||
|
@ -554,6 +540,35 @@ class FileSystem(object):
|
||||||
|
|
||||||
return file_list
|
return file_list
|
||||||
|
|
||||||
|
def _conflict_solved(self, conflict_file_list, item, dest_path):
|
||||||
|
self.logger.warning(f'Same name already exists...renaming to: {dest_path}')
|
||||||
|
del(conflict_file_list[item])
|
||||||
|
|
||||||
|
def solve_conflicts(self, conflict_file_list, remove_duplicates):
|
||||||
|
file_list = conflict_file_list.copy()
|
||||||
|
for item, file_paths in enumerate(file_list):
|
||||||
|
src_path = file_paths['src_path']
|
||||||
|
dest_path = file_paths['dest_path']
|
||||||
|
# Try to sort the file
|
||||||
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
|
# remove to conflict file list if file as be successfully copied or ignored
|
||||||
|
if result is True or None:
|
||||||
|
self._conflict_solved(conflict_file_list, item, dest_path)
|
||||||
|
else:
|
||||||
|
n = 1
|
||||||
|
while result is False:
|
||||||
|
if n > 100:
|
||||||
|
self.logger.warning(f'{self.mode}: to many append for {dest_path}...')
|
||||||
|
break
|
||||||
|
# Add appendix to the name
|
||||||
|
pre, ext = os.path.splitext(dest_path)
|
||||||
|
dest_path = pre + '_' + str(n) + ext
|
||||||
|
conflict_file_list[item]['dest_path'] = dest_path
|
||||||
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
|
else:
|
||||||
|
self._conflict_solved(conflict_file_list, item, dest_path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def sort_files(self, paths, destination, db, remove_duplicates=False,
|
def sort_files(self, paths, destination, db, remove_duplicates=False,
|
||||||
ignore_tags=set()):
|
ignore_tags=set()):
|
||||||
|
@ -568,11 +583,12 @@ class FileSystem(object):
|
||||||
|
|
||||||
path = os.path.expanduser(path)
|
path = os.path.expanduser(path)
|
||||||
|
|
||||||
conflict_file_list = set()
|
conflict_file_list = []
|
||||||
for src_path, subdirs in self.get_files_in_path(path):
|
for src_path, subdirs in self.get_files_in_path(path,
|
||||||
|
extensions=self.filter_by_ext):
|
||||||
# Process files
|
# Process files
|
||||||
src_checksum = self.checksum(src_path)
|
src_checksum = self.checksum(src_path)
|
||||||
media = get_media_class(src_path, ignore_tags, self.logger)
|
media = Media(src_path, ignore_tags, self.logger)
|
||||||
if media:
|
if media:
|
||||||
metadata = media.get_metadata()
|
metadata = media.get_metadata()
|
||||||
# Get the destination path according to metadata
|
# Get the destination path according to metadata
|
||||||
|
@ -587,40 +603,23 @@ class FileSystem(object):
|
||||||
|
|
||||||
self.create_directory(dest_directory)
|
self.create_directory(dest_directory)
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
||||||
if result:
|
|
||||||
self.summary, has_errors = self.check_file(src_path,
|
if result is False:
|
||||||
dest_path, src_checksum, db)
|
|
||||||
else:
|
|
||||||
# There is conflict files
|
# There is conflict files
|
||||||
conflict_file_list.add((src_path, dest_path))
|
conflict_file_list.append({'src_path': src_path, 'dest_path': dest_path})
|
||||||
|
result = self.solve_conflicts(conflict_file_list, remove_duplicates)
|
||||||
|
|
||||||
for src_path, dest_path in conflict_file_list:
|
if result is True:
|
||||||
# Try to sort the file
|
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
|
||||||
if result:
|
|
||||||
conflict_file_list.remove((src_path, dest_path))
|
|
||||||
else:
|
|
||||||
n = 1
|
|
||||||
while not result:
|
|
||||||
# Add appendix to the name
|
|
||||||
pre, ext = os.path.splitext(dest_path)
|
|
||||||
dest_path = pre + '_' + str(n) + ext
|
|
||||||
result = self.sort_file(src_path, dest_path, remove_duplicates)
|
|
||||||
if n > 100:
|
|
||||||
self.logger.error(f'{self.mode}: to many append for {dest_path}...')
|
|
||||||
break
|
|
||||||
self.logger.info(f'Same name already exists...renaming to: {dest_path}')
|
|
||||||
|
|
||||||
if result:
|
|
||||||
self.summary, has_errors = self.check_file(src_path,
|
self.summary, has_errors = self.check_file(src_path,
|
||||||
dest_path, src_checksum, db)
|
dest_path, src_checksum, db)
|
||||||
|
elif result is None:
|
||||||
|
has_errors = False
|
||||||
else:
|
else:
|
||||||
self.summary.append((src_path, False))
|
self.summary.append((src_path, False))
|
||||||
has_errors = True
|
has_errors = True
|
||||||
|
|
||||||
return self.summary, has_errors
|
return self.summary, has_errors
|
||||||
|
|
||||||
|
|
||||||
def check_path(self, path):
|
def check_path(self, path):
|
||||||
path = os.path.abspath(os.path.expanduser(path))
|
path = os.path.abspath(os.path.expanduser(path))
|
||||||
|
|
||||||
|
@ -631,7 +630,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def set_hash(self, result, src_path, dest_path, src_checksum, db):
|
def set_hash(self, result, src_path, dest_path, src_checksum, db):
|
||||||
if result:
|
if result:
|
||||||
# Check if file remain the same
|
# Check if file remain the same
|
||||||
|
@ -658,7 +656,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return has_errors
|
return has_errors
|
||||||
|
|
||||||
|
|
||||||
def move_file(self, img_path, dest_path, checksum, db):
|
def move_file(self, img_path, dest_path, checksum, db):
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
try:
|
try:
|
||||||
|
@ -669,13 +666,12 @@ class FileSystem(object):
|
||||||
self.logger.info(f'move: {img_path} -> {dest_path}')
|
self.logger.info(f'move: {img_path} -> {dest_path}')
|
||||||
return self.set_hash(True, img_path, dest_path, checksum, db)
|
return self.set_hash(True, img_path, dest_path, checksum, db)
|
||||||
|
|
||||||
|
|
||||||
def sort_similar_images(self, path, db, similarity=80):
|
def sort_similar_images(self, path, db, similarity=80):
|
||||||
|
|
||||||
has_errors = False
|
has_errors = False
|
||||||
path = self.check_path(path)
|
path = self.check_path(path)
|
||||||
for dirname, dirnames, filenames, level in self.walklevel(path, None):
|
for dirname, dirnames, filenames, level in self.walklevel(path, None):
|
||||||
if dirname == os.path.join(path, '.dozo'):
|
if dirname == os.path.join(path, '.ordigi'):
|
||||||
continue
|
continue
|
||||||
if dirname.find('similar_to') == 0:
|
if dirname.find('similar_to') == 0:
|
||||||
continue
|
continue
|
||||||
|
@ -684,21 +680,21 @@ class FileSystem(object):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
file_paths.add(os.path.join(dirname, filename))
|
file_paths.add(os.path.join(dirname, filename))
|
||||||
|
|
||||||
photo = Photo(logger=self.logger)
|
i = Images(file_paths, logger=self.logger)
|
||||||
|
|
||||||
images = set([ i for i in photo.get_images(file_paths) ])
|
images = set([ i for i in i.get_images() ])
|
||||||
for image in images:
|
for image in images:
|
||||||
if not os.path.isfile(image):
|
if not os.path.isfile(image):
|
||||||
continue
|
continue
|
||||||
checksum1 = self.checksum(image)
|
checksum1 = self.checksum(image)
|
||||||
# Process files
|
# Process files
|
||||||
# media = get_media_class(src_path, False, self.logger)
|
# media = Media(src_path, False, self.logger)
|
||||||
# TODO compare metadata
|
# TODO compare metadata
|
||||||
# if media:
|
# if media:
|
||||||
# metadata = media.get_metadata()
|
# metadata = media.get_metadata()
|
||||||
similar = False
|
similar = False
|
||||||
moved_imgs = set()
|
moved_imgs = set()
|
||||||
for img_path in photo.find_similar(image, file_paths, similarity):
|
for img_path in i.find_similar(image, similarity):
|
||||||
similar = True
|
similar = True
|
||||||
checksum2 = self.checksum(img_path)
|
checksum2 = self.checksum(img_path)
|
||||||
# move image into directory
|
# move image into directory
|
||||||
|
@ -732,13 +728,12 @@ class FileSystem(object):
|
||||||
|
|
||||||
return self.summary, has_errors
|
return self.summary, has_errors
|
||||||
|
|
||||||
|
|
||||||
def revert_compare(self, path, db):
|
def revert_compare(self, path, db):
|
||||||
|
|
||||||
has_errors = False
|
has_errors = False
|
||||||
path = self.check_path(path)
|
path = self.check_path(path)
|
||||||
for dirname, dirnames, filenames, level in self.walklevel(path, None):
|
for dirname, dirnames, filenames, level in self.walklevel(path, None):
|
||||||
if dirname == os.path.join(path, '.dozo'):
|
if dirname == os.path.join(path, '.ordigi'):
|
||||||
continue
|
continue
|
||||||
if dirname.find('similar_to') == 0:
|
if dirname.find('similar_to') == 0:
|
||||||
continue
|
continue
|
||||||
|
@ -764,7 +759,6 @@ class FileSystem(object):
|
||||||
|
|
||||||
return self.summary, has_errors
|
return self.summary, has_errors
|
||||||
|
|
||||||
|
|
||||||
def set_utime_from_metadata(self, date_taken, file_path):
|
def set_utime_from_metadata(self, date_taken, file_path):
|
||||||
""" Set the modification time on the file based on the file name.
|
""" Set the modification time on the file based on the file name.
|
||||||
"""
|
"""
|
||||||
|
@ -772,7 +766,6 @@ class FileSystem(object):
|
||||||
# Initialize date taken to what's returned from the metadata function.
|
# Initialize date taken to what's returned from the metadata function.
|
||||||
os.utime(file_path, (int(datetime.now().timestamp()), int(date_taken.timestamp())))
|
os.utime(file_path, (int(datetime.now().timestamp()), int(date_taken.timestamp())))
|
||||||
|
|
||||||
|
|
||||||
def should_exclude(self, path, regex_list=set(), needs_compiled=False):
|
def should_exclude(self, path, regex_list=set(), needs_compiled=False):
|
||||||
if(len(regex_list) == 0):
|
if(len(regex_list) == 0):
|
||||||
return False
|
return False
|
|
@ -1,6 +1,4 @@
|
||||||
"""Look up geolocation information for media objects."""
|
"""Look up geolocation information for media objects."""
|
||||||
from past.utils import old_div
|
|
||||||
|
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
|
@ -8,8 +6,8 @@ import geopy
|
||||||
from geopy.geocoders import Nominatim
|
from geopy.geocoders import Nominatim
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
from dozo.config import load_config, get_geocoder
|
from ordigi.config import load_config, get_geocoder
|
||||||
|
|
||||||
__KEY__ = None
|
__KEY__ = None
|
||||||
__DEFAULT_LOCATION__ = 'Unknown Location'
|
__DEFAULT_LOCATION__ = 'Unknown Location'
|
||||||
|
@ -28,7 +26,9 @@ def coordinates_by_name(name, db):
|
||||||
# If the name is not cached then we go ahead with an API lookup
|
# If the name is not cached then we go ahead with an API lookup
|
||||||
geocoder = get_geocoder()
|
geocoder = get_geocoder()
|
||||||
if geocoder == 'Nominatim':
|
if geocoder == 'Nominatim':
|
||||||
locator = Nominatim(user_agent='myGeocoder')
|
# timeout = DEFAULT_SENTINEL
|
||||||
|
timeout = 10
|
||||||
|
locator = Nominatim(user_agent='myGeocoder', timeout=timeout)
|
||||||
geolocation_info = locator.geocode(name)
|
geolocation_info = locator.geocode(name)
|
||||||
if geolocation_info is not None:
|
if geolocation_info is not None:
|
||||||
return {
|
return {
|
||||||
|
@ -53,12 +53,10 @@ def decimal_to_dms(decimal):
|
||||||
|
|
||||||
def dms_to_decimal(degrees, minutes, seconds, direction=' '):
|
def dms_to_decimal(degrees, minutes, seconds, direction=' '):
|
||||||
sign = 1
|
sign = 1
|
||||||
if(direction[0] in 'WSws'):
|
if direction[0] in 'WSws':
|
||||||
sign = -1
|
sign = -1
|
||||||
return (
|
|
||||||
float(degrees) + old_div(float(minutes), 60) +
|
return (degrees + minutes / 60 + seconds / 3600) * sign
|
||||||
old_div(float(seconds), 3600)
|
|
||||||
) * sign
|
|
||||||
|
|
||||||
|
|
||||||
def dms_string(decimal, type='latitude'):
|
def dms_string(decimal, type='latitude'):
|
||||||
|
@ -139,14 +137,19 @@ def lookup_osm(lat, lon, logger=logging.getLogger()):
|
||||||
|
|
||||||
prefer_english_names = get_prefer_english_names()
|
prefer_english_names = get_prefer_english_names()
|
||||||
try:
|
try:
|
||||||
locator = Nominatim(user_agent='myGeocoder')
|
timeout = 10
|
||||||
|
locator = Nominatim(user_agent='myGeocoder', timeout=timeout)
|
||||||
coords = (lat, lon)
|
coords = (lat, lon)
|
||||||
if(prefer_english_names):
|
if(prefer_english_names):
|
||||||
lang='en'
|
lang='en'
|
||||||
else:
|
else:
|
||||||
lang='local'
|
lang='local'
|
||||||
return locator.reverse(coords, language=lang).raw
|
locator_reverse = locator.reverse(coords, language=lang)
|
||||||
except geopy.exc.GeocoderUnavailable as e:
|
if locator_reverse is not None:
|
||||||
|
return locator_reverse.raw
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except geopy.exc.GeocoderUnavailable or geopy.exc.GeocoderServiceError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return None
|
return None
|
||||||
# Fix *** TypeError: `address` must not be None
|
# Fix *** TypeError: `address` must not be None
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
The photo module contains the :class:`Photo` class, which is used to track
|
The image module contains the :class:`Images` class, which is used to track
|
||||||
image objects (JPG, DNG, etc.).
|
image objects (JPG, DNG, etc.).
|
||||||
|
|
||||||
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
||||||
|
@ -10,50 +10,36 @@ import imghdr
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image as img
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .media import Media
|
# HEIC extension support (experimental, not tested)
|
||||||
|
PYHEIF = False
|
||||||
|
try:
|
||||||
|
from pyheif_pillow_opener import register_heif_opener
|
||||||
|
PYHEIF = True
|
||||||
|
# Allow to open HEIF/HEIC image from pillow
|
||||||
|
register_heif_opener()
|
||||||
|
except ImportError as e:
|
||||||
|
logging.info(e)
|
||||||
|
|
||||||
|
|
||||||
class Photo(Media):
|
class Image():
|
||||||
|
|
||||||
"""A photo object.
|
def __init__(self, img_path, hash_size=8):
|
||||||
|
|
||||||
:param str source: The fully qualified path to the photo file
|
|
||||||
"""
|
|
||||||
|
|
||||||
__name__ = 'Photo'
|
|
||||||
|
|
||||||
#: Valid extensions for photo files.
|
|
||||||
extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
|
|
||||||
|
|
||||||
def __init__(self, source=None, hash_size=8, ignore_tags=set(),
|
|
||||||
logger=logging.getLogger()):
|
|
||||||
super().__init__(source, ignore_tags)
|
|
||||||
|
|
||||||
|
self.img_path = img_path
|
||||||
self.hash_size = hash_size
|
self.hash_size = hash_size
|
||||||
self.logger = logger
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# HEIC extension support (experimental, not tested)
|
def is_image(self):
|
||||||
self.pyheif = False
|
|
||||||
try:
|
|
||||||
from pyheif_pillow_opener import register_heif_opener
|
|
||||||
self.pyheif = True
|
|
||||||
# Allow to open HEIF/HEIC images from pillow
|
|
||||||
register_heif_opener()
|
|
||||||
except ImportError as e:
|
|
||||||
self.logger.info(e)
|
|
||||||
|
|
||||||
def is_image(self, img_path):
|
|
||||||
"""Check whether the file is an image.
|
"""Check whether the file is an image.
|
||||||
:returns: bool
|
:returns: bool
|
||||||
"""
|
"""
|
||||||
# gh-4 This checks if the source file is an image.
|
# gh-4 This checks if the file is an image.
|
||||||
# It doesn't validate against the list of supported types.
|
# It doesn't validate against the list of supported types.
|
||||||
# We check with imghdr and pillow.
|
# We check with imghdr and pillow.
|
||||||
if imghdr.what(img_path) is None:
|
if imghdr.what(self.img_path) is None:
|
||||||
# Pillow is used as a fallback
|
# Pillow is used as a fallback
|
||||||
# imghdr won't detect all variants of images (https://bugs.python.org/issue28591)
|
# imghdr won't detect all variants of images (https://bugs.python.org/issue28591)
|
||||||
# see https://github.com/jmathai/elodie/issues/281
|
# see https://github.com/jmathai/elodie/issues/281
|
||||||
|
@ -65,7 +51,7 @@ class Photo(Media):
|
||||||
# things like mode, size, and other properties required to decode the file,
|
# things like mode, size, and other properties required to decode the file,
|
||||||
# but the rest of the file is not processed until later.
|
# but the rest of the file is not processed until later.
|
||||||
try:
|
try:
|
||||||
im = Image.open(img_path)
|
im = img.open(self.img_path)
|
||||||
except (IOError, UnidentifiedImageError):
|
except (IOError, UnidentifiedImageError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -74,26 +60,48 @@ class Photo(Media):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_images(self, file_paths):
|
def get_hash(self):
|
||||||
|
with img.open(self.img_path) as img_path:
|
||||||
|
return imagehash.average_hash(img_path, self.hash_size).hash
|
||||||
|
|
||||||
|
|
||||||
|
class Images():
|
||||||
|
|
||||||
|
"""A image object.
|
||||||
|
|
||||||
|
:param str img_path: The fully qualified path to the image file
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Valid extensions for image files.
|
||||||
|
extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
|
||||||
|
|
||||||
|
def __init__(self, file_paths=None, hash_size=8, logger=logging.getLogger()):
|
||||||
|
|
||||||
|
self.file_paths = file_paths
|
||||||
|
self.hash_size = hash_size
|
||||||
|
self.duplicates = []
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def get_images(self):
|
||||||
|
''':returns: img_path generator
|
||||||
'''
|
'''
|
||||||
:returns: img_path generator
|
for img_path in self.file_paths:
|
||||||
'''
|
image = Image(img_path)
|
||||||
for img_path in file_paths:
|
if image.is_image():
|
||||||
if self.is_image(img_path):
|
|
||||||
yield img_path
|
yield img_path
|
||||||
|
|
||||||
def get_images_hashes(self, file_paths):
|
def get_images_hashes(self):
|
||||||
"""Get image hashes"""
|
"""Get image hashes"""
|
||||||
hashes = {}
|
hashes = {}
|
||||||
duplicates = []
|
|
||||||
# Searching for duplicates.
|
# Searching for duplicates.
|
||||||
for img_path in self.get_images(file_paths):
|
for img_path in self.get_images():
|
||||||
with Image.open(img_path) as img:
|
with img.open(img_path) as img:
|
||||||
yield imagehash.average_hash(img, self.hash_size)
|
yield imagehash.average_hash(img, self.hash_size)
|
||||||
|
|
||||||
def find_duplicates(self, file_paths):
|
def find_duplicates(self, img_path):
|
||||||
"""Find duplicates"""
|
"""Find duplicates"""
|
||||||
for temp_hash in get_images_hashes(file_paths):
|
duplicates = []
|
||||||
|
for temp_hash in get_images_hashes(self.file_paths):
|
||||||
if temp_hash in hashes:
|
if temp_hash in hashes:
|
||||||
self.logger.info("Duplicate {} \nfound for image {}\n".format(img_path, hashes[temp_hash]))
|
self.logger.info("Duplicate {} \nfound for image {}\n".format(img_path, hashes[temp_hash]))
|
||||||
duplicates.append(img_path)
|
duplicates.append(img_path)
|
||||||
|
@ -118,10 +126,6 @@ class Photo(Media):
|
||||||
else:
|
else:
|
||||||
self.logger.info("No duplicates found")
|
self.logger.info("No duplicates found")
|
||||||
|
|
||||||
def get_hash(self, img_path):
|
|
||||||
with Image.open(img_path) as img:
|
|
||||||
return imagehash.average_hash(img, self.hash_size).hash
|
|
||||||
|
|
||||||
def diff(self, hash1, hash2):
|
def diff(self, hash1, hash2):
|
||||||
return np.count_nonzero(hash1 != hash2)
|
return np.count_nonzero(hash1 != hash2)
|
||||||
|
|
||||||
|
@ -131,24 +135,25 @@ class Photo(Media):
|
||||||
|
|
||||||
return similarity_img
|
return similarity_img
|
||||||
|
|
||||||
def find_similar(self, image, file_paths, similarity=80):
|
def find_similar(self, image, similarity=80):
|
||||||
'''
|
'''
|
||||||
Find similar images
|
Find similar images
|
||||||
:returns: img_path generator
|
:returns: img_path generator
|
||||||
'''
|
'''
|
||||||
hash1 = ''
|
hash1 = ''
|
||||||
if self.is_image(image):
|
image = Image(image)
|
||||||
hash1 = self.get_hash(image)
|
if image.is_image():
|
||||||
|
hash1 = image.get_hash()
|
||||||
|
|
||||||
self.logger.info(f'Finding similar images to {image}')
|
self.logger.info(f'Finding similar images to {image}')
|
||||||
|
|
||||||
threshold = 1 - similarity/100
|
threshold = 1 - similarity/100
|
||||||
diff_limit = int(threshold*(self.hash_size**2))
|
diff_limit = int(threshold*(self.hash_size**2))
|
||||||
|
|
||||||
for img_path in self.get_images(file_paths):
|
for img_path in self.get_images():
|
||||||
if img_path == image:
|
if img_path == image:
|
||||||
continue
|
continue
|
||||||
hash2 = self.get_hash(img_path)
|
hash2 = image.get_hash()
|
||||||
img_diff = self.diff(hash1, hash2)
|
img_diff = self.diff(hash1, hash2)
|
||||||
if img_diff <= diff_limit:
|
if img_diff <= diff_limit:
|
||||||
similarity_img = self.similarity(img_diff)
|
similarity_img = self.similarity(img_diff)
|
|
@ -0,0 +1,16 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def get_logger(verbose, debug):
|
||||||
|
if debug:
|
||||||
|
level = logging.DEBUG
|
||||||
|
elif verbose:
|
||||||
|
level = logging.INFO
|
||||||
|
else:
|
||||||
|
level = logging.WARNING
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(levelname)s:%(message)s', level=level)
|
||||||
|
logging.getLogger('asyncio').setLevel(level)
|
||||||
|
logger = logging.getLogger('ordigi')
|
||||||
|
logger.level = level
|
||||||
|
return logger
|
||||||
|
|
|
@ -1,28 +1,24 @@
|
||||||
"""
|
"""
|
||||||
Base :class:`Media` class for media objects that are tracked by Dozo.
|
Media :class:`Media` class to get file metadata
|
||||||
The Media class provides some base functionality used by all the media types.
|
|
||||||
Sub-classes (:class:`~dozo.media.Audio`, :class:`~dozo.media.Photo`, and :class:`~dozo.media.Video`).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import six
|
import six
|
||||||
import logging
|
|
||||||
|
|
||||||
# load modules
|
# load modules
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
import re
|
import re
|
||||||
from dozo.exiftool import ExifTool, ExifToolCaching
|
from ordigi.exiftool import ExifTool, ExifToolCaching
|
||||||
|
|
||||||
class Media():
|
class Media():
|
||||||
|
|
||||||
"""The media class for all media objects.
|
"""The media class for all media objects.
|
||||||
|
|
||||||
:param str source: The fully qualified path to the video file.
|
:param str file_path: The fully qualified path to the media file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__name__ = 'Media'
|
|
||||||
|
|
||||||
d_coordinates = {
|
d_coordinates = {
|
||||||
'latitude': 'latitude_ref',
|
'latitude': 'latitude_ref',
|
||||||
'longitude': 'longitude_ref'
|
'longitude': 'longitude_ref'
|
||||||
|
@ -34,8 +30,8 @@ class Media():
|
||||||
|
|
||||||
extensions = PHOTO + AUDIO + VIDEO
|
extensions = PHOTO + AUDIO + VIDEO
|
||||||
|
|
||||||
def __init__(self, sources=None, ignore_tags=set(), logger=logging.getLogger()):
|
def __init__(self, file_path, ignore_tags=set(), logger=logging.getLogger()):
|
||||||
self.source = sources
|
self.file_path = file_path
|
||||||
self.ignore_tags = ignore_tags
|
self.ignore_tags = ignore_tags
|
||||||
self.tags_keys = self.get_tags()
|
self.tags_keys = self.get_tags()
|
||||||
self.exif_metadata = None
|
self.exif_metadata = None
|
||||||
|
@ -104,7 +100,7 @@ class Media():
|
||||||
|
|
||||||
:returns: str or None
|
:returns: str or None
|
||||||
"""
|
"""
|
||||||
mimetype = mimetypes.guess_type(self.source)
|
mimetype = mimetypes.guess_type(self.file_path)
|
||||||
if(mimetype is None):
|
if(mimetype is None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -152,7 +148,8 @@ class Media():
|
||||||
value = re.sub(regex , r'\g<1>-\g<2>-\g<3>', value)
|
value = re.sub(regex , r'\g<1>-\g<2>-\g<3>', value)
|
||||||
return parse(value)
|
return parse(value)
|
||||||
except BaseException or dateutil.parser._parser.ParserError as e:
|
except BaseException or dateutil.parser._parser.ParserError as e:
|
||||||
self.logger.error(e)
|
self.logger.error(e, value)
|
||||||
|
import ipdb; ipdb.set_trace()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_coordinates(self, key, value):
|
def get_coordinates(self, key, value):
|
||||||
|
@ -198,7 +195,7 @@ class Media():
|
||||||
:returns: dict
|
:returns: dict
|
||||||
"""
|
"""
|
||||||
# Get metadata from exiftool.
|
# Get metadata from exiftool.
|
||||||
self.exif_metadata = ExifToolCaching(self.source, logger=self.logger).asdict()
|
self.exif_metadata = ExifToolCaching(self.file_path, logger=self.logger).asdict()
|
||||||
|
|
||||||
# TODO to be removed
|
# TODO to be removed
|
||||||
self.metadata = {}
|
self.metadata = {}
|
||||||
|
@ -224,9 +221,9 @@ class Media():
|
||||||
|
|
||||||
self.metadata[key] = formated_data
|
self.metadata[key] = formated_data
|
||||||
|
|
||||||
self.metadata['base_name'] = os.path.basename(os.path.splitext(self.source)[0])
|
self.metadata['base_name'] = os.path.basename(os.path.splitext(self.file_path)[0])
|
||||||
self.metadata['ext'] = os.path.splitext(self.source)[1][1:]
|
self.metadata['ext'] = os.path.splitext(self.file_path)[1][1:]
|
||||||
self.metadata['directory_path'] = os.path.dirname(self.source)
|
self.metadata['directory_path'] = os.path.dirname(self.file_path)
|
||||||
|
|
||||||
return self.metadata
|
return self.metadata
|
||||||
|
|
||||||
|
@ -245,8 +242,7 @@ class Media():
|
||||||
def get_class_by_file(cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()):
|
def get_class_by_file(cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()):
|
||||||
"""Static method to get a media object by file.
|
"""Static method to get a media object by file.
|
||||||
"""
|
"""
|
||||||
basestring = (bytes, str)
|
if not os.path.isfile(_file):
|
||||||
if not isinstance(_file, basestring) or not os.path.isfile(_file):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
extension = os.path.splitext(_file)[1][1:].lower()
|
extension = os.path.splitext(_file)[1][1:].lower()
|
||||||
|
@ -254,13 +250,9 @@ class Media():
|
||||||
if len(extension) > 0:
|
if len(extension) > 0:
|
||||||
for i in classes:
|
for i in classes:
|
||||||
if(extension in i.extensions):
|
if(extension in i.extensions):
|
||||||
return i(_file, ignore_tags=ignore_tags)
|
return i(_file, ignore_tags=ignore_tags, logger=logger)
|
||||||
|
|
||||||
exclude_list = ['.DS_Store', '.directory']
|
return Media(_file, logger, ignore_tags=ignore_tags, logger=logger)
|
||||||
if os.path.basename(_file) == '.DS_Store':
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return Media(_file, ignore_tags=ignore_tags, logger=logger)
|
|
||||||
|
|
||||||
def set_date_taken(self, date_key, time):
|
def set_date_taken(self, date_key, time):
|
||||||
"""Set the date/time a photo was taken.
|
"""Set the date/time a photo was taken.
|
||||||
|
@ -309,7 +301,7 @@ class Media():
|
||||||
|
|
||||||
:returns: bool
|
:returns: bool
|
||||||
"""
|
"""
|
||||||
folder = os.path.basename(os.path.dirname(self.source))
|
folder = os.path.basename(os.path.dirname(self.file_path))
|
||||||
|
|
||||||
return set_value(self, 'album', folder)
|
return set_value(self, 'album', folder)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
[pytest]
|
||||||
|
addopts = --ignore=old_tests -s
|
||||||
|
|
||||||
|
# collect_ignore = ["old_test"]
|
||||||
|
|
||||||
|
[pycodestyle]
|
||||||
|
# ignore = old_test/* ALL
|
|
@ -5,4 +5,5 @@ 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
|
||||||
six==1.9
|
six==1.9
|
||||||
|
|
|
@ -6,10 +6,10 @@ from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from dozo import config
|
from ordigi import config
|
||||||
from dozo.exiftool import _ExifToolProc
|
from ordigi.exiftool import _ExifToolProc
|
||||||
|
|
||||||
DOZO_PATH = Path(__file__).parent.parent
|
ORDIGI_PATH = Path(__file__).parent.parent
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_singletons():
|
def reset_singletons():
|
||||||
|
@ -18,8 +18,8 @@ def reset_singletons():
|
||||||
|
|
||||||
|
|
||||||
def copy_sample_files():
|
def copy_sample_files():
|
||||||
src_path = tempfile.mkdtemp(prefix='dozo-src')
|
src_path = tempfile.mkdtemp(prefix='ordigi-src')
|
||||||
paths = Path(DOZO_PATH, 'samples/test_exif').glob('*')
|
paths = Path(ORDIGI_PATH, 'samples/test_exif').glob('*')
|
||||||
file_paths = [x for x in paths if x.is_file()]
|
file_paths = [x for x in paths if x.is_file()]
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
source_path = Path(src_path, file_path.name)
|
source_path = Path(src_path, file_path.name)
|
||||||
|
@ -30,7 +30,7 @@ def copy_sample_files():
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def conf_path():
|
def conf_path():
|
||||||
tmp_path = tempfile.mkdtemp(prefix='dozo-')
|
tmp_path = tempfile.mkdtemp(prefix='ordigi-')
|
||||||
conf = RawConfigParser()
|
conf = RawConfigParser()
|
||||||
conf['Path'] = {
|
conf['Path'] = {
|
||||||
'day_begins': '4',
|
'day_begins': '4',
|
||||||
|
@ -40,7 +40,7 @@ def conf_path():
|
||||||
conf['Geolocation'] = {
|
conf['Geolocation'] = {
|
||||||
'geocoder': 'Nominatium'
|
'geocoder': 'Nominatium'
|
||||||
}
|
}
|
||||||
conf_path = Path(tmp_path, "dozo.conf")
|
conf_path = Path(tmp_path, "ordigi.conf")
|
||||||
config.write(conf_path, conf)
|
config.write(conf_path, conf)
|
||||||
|
|
||||||
yield conf_path
|
yield conf_path
|
||||||
|
|
|
@ -4,7 +4,7 @@ import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from dozo import config
|
from ordigi import config
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
import random
|
import random
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
CONTENT = "content"
|
CONTENT = "content"
|
||||||
|
|
||||||
class TestDozo:
|
class TestOrdigi:
|
||||||
@pytest.mark.skip()
|
@pytest.mark.skip()
|
||||||
def test__sort(self):
|
def test__sort(self):
|
||||||
assert 0
|
assert 0
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import dozo.exiftool
|
import ordigi.exiftool
|
||||||
from dozo.exiftool import get_exiftool_path
|
from ordigi.exiftool import get_exiftool_path
|
||||||
|
|
||||||
TEST_FILE_ONE_KEYWORD = "samples/images/wedding.jpg"
|
TEST_FILE_ONE_KEYWORD = "samples/images/wedding.jpg"
|
||||||
TEST_FILE_BAD_IMAGE = "samples/images/badimage.jpeg"
|
TEST_FILE_BAD_IMAGE = "samples/images/badimage.jpeg"
|
||||||
|
@ -103,86 +103,86 @@ if exiftool is None:
|
||||||
|
|
||||||
def test_get_exiftool_path():
|
def test_get_exiftool_path():
|
||||||
|
|
||||||
exiftool = dozo.exiftool.get_exiftool_path()
|
exiftool = ordigi.exiftool.get_exiftool_path()
|
||||||
assert exiftool is not None
|
assert exiftool is not None
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version():
|
||||||
exif = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif.version is not None
|
assert exif.version is not None
|
||||||
assert isinstance(exif.version, str)
|
assert isinstance(exif.version, str)
|
||||||
|
|
||||||
|
|
||||||
def test_read():
|
def test_read():
|
||||||
exif = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif.data["File:MIMEType"] == "image/jpeg"
|
assert exif.data["File:MIMEType"] == "image/jpeg"
|
||||||
assert exif.data["EXIF:ISO"] == 160
|
assert exif.data["EXIF:ISO"] == 160
|
||||||
assert exif.data["IPTC:Keywords"] == "wedding"
|
assert exif.data["IPTC:Keywords"] == "wedding"
|
||||||
|
|
||||||
|
|
||||||
def test_singleton():
|
def test_singleton():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
exif2 = dozo.exiftool.ExifTool(TEST_FILE_MULTI_KEYWORD)
|
exif2 = ordigi.exiftool.ExifTool(TEST_FILE_MULTI_KEYWORD)
|
||||||
|
|
||||||
assert exif1._process.pid == exif2._process.pid
|
assert exif1._process.pid == exif2._process.pid
|
||||||
|
|
||||||
|
|
||||||
def test_pid():
|
def test_pid():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif1.pid == exif1._process.pid
|
assert exif1.pid == exif1._process.pid
|
||||||
|
|
||||||
|
|
||||||
def test_exiftoolproc_process():
|
def test_exiftoolproc_process():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif1._exiftoolproc.process is not None
|
assert exif1._exiftoolproc.process is not None
|
||||||
|
|
||||||
|
|
||||||
def test_exiftoolproc_exiftool():
|
def test_exiftoolproc_exiftool():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif1._exiftoolproc.exiftool == dozo.exiftool.get_exiftool_path()
|
assert exif1._exiftoolproc.exiftool == ordigi.exiftool.get_exiftool_path()
|
||||||
|
|
||||||
|
|
||||||
def test_as_dict():
|
def test_as_dict():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
exifdata = exif1.asdict()
|
exifdata = exif1.asdict()
|
||||||
assert exifdata["XMP:TagsList"] == "wedding"
|
assert exifdata["XMP:TagsList"] == "wedding"
|
||||||
|
|
||||||
|
|
||||||
def test_as_dict_normalized():
|
def test_as_dict_normalized():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
exifdata = exif1.asdict(normalized=True)
|
exifdata = exif1.asdict(normalized=True)
|
||||||
assert exifdata["xmp:tagslist"] == "wedding"
|
assert exifdata["xmp:tagslist"] == "wedding"
|
||||||
assert "XMP:TagsList" not in exifdata
|
assert "XMP:TagsList" not in exifdata
|
||||||
|
|
||||||
|
|
||||||
def test_as_dict_no_tag_groups():
|
def test_as_dict_no_tag_groups():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
exifdata = exif1.asdict(tag_groups=False)
|
exifdata = exif1.asdict(tag_groups=False)
|
||||||
assert exifdata["TagsList"] == "wedding"
|
assert exifdata["TagsList"] == "wedding"
|
||||||
|
|
||||||
|
|
||||||
def test_json():
|
def test_json():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
exifdata = json.loads(exif1.json())
|
exifdata = json.loads(exif1.json())
|
||||||
assert exifdata[0]["XMP:TagsList"] == "wedding"
|
assert exifdata[0]["XMP:TagsList"] == "wedding"
|
||||||
|
|
||||||
|
|
||||||
def test_str():
|
def test_str():
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert "file: " in str(exif1)
|
assert "file: " in str(exif1)
|
||||||
assert "exiftool: " in str(exif1)
|
assert "exiftool: " in str(exif1)
|
||||||
|
|
||||||
|
|
||||||
def test_exiftool_terminate():
|
def test_exiftool_terminate():
|
||||||
""" Test that exiftool process is terminated when exiftool.terminate() is called """
|
""" Test that exiftool process is terminated when exiftool.terminate() is called """
|
||||||
exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif1 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
|
|
||||||
assert dozo.exiftool.exiftool_is_running()
|
assert ordigi.exiftool.exiftool_is_running()
|
||||||
|
|
||||||
dozo.exiftool.terminate_exiftool()
|
ordigi.exiftool.terminate_exiftool()
|
||||||
|
|
||||||
assert not dozo.exiftool.exiftool_is_running()
|
assert not ordigi.exiftool.exiftool_is_running()
|
||||||
|
|
||||||
# verify we can create a new instance after termination
|
# verify we can create a new instance after termination
|
||||||
exif2 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif2 = ordigi.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
assert exif2.asdict()["IPTC:Keywords"] == "wedding"
|
assert exif2.asdict()["IPTC:Keywords"] == "wedding"
|
||||||
|
|
|
@ -8,11 +8,11 @@ from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from .conftest import copy_sample_files
|
from .conftest import copy_sample_files
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
from dozo.database import Db
|
from ordigi.database import Db
|
||||||
from dozo.filesystem import FileSystem
|
from ordigi.filesystem import FileSystem
|
||||||
from dozo.media.media import Media
|
from ordigi.media import Media
|
||||||
from dozo.exiftool import ExifToolCaching, exiftool_is_running, terminate_exiftool
|
from ordigi.exiftool import ExifToolCaching, exiftool_is_running, terminate_exiftool
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
@pytest.mark.skip()
|
||||||
|
@ -153,6 +153,7 @@ class TestFilesystem:
|
||||||
for mode in 'copy', 'move':
|
for mode in 'copy', 'move':
|
||||||
filesystem = FileSystem(path_format=self.path_format, mode=mode)
|
filesystem = FileSystem(path_format=self.path_format, mode=mode)
|
||||||
# copy mode
|
# copy mode
|
||||||
|
import ipdb; ipdb.set_trace()
|
||||||
src_path = Path(self.src_paths, 'photo.png')
|
src_path = Path(self.src_paths, 'photo.png')
|
||||||
dest_path = Path(tmp_path,'photo_copy.png')
|
dest_path = Path(tmp_path,'photo_copy.png')
|
||||||
src_checksum = filesystem.checksum(src_path)
|
src_checksum = filesystem.checksum(src_path)
|
||||||
|
|
|
@ -6,14 +6,12 @@ import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from .conftest import copy_sample_files
|
from .conftest import copy_sample_files
|
||||||
from dozo import constants
|
from ordigi import constants
|
||||||
from dozo.media.media import Media
|
from ordigi.media import Media
|
||||||
from dozo.media.audio import Audio
|
from ordigi.images import Images
|
||||||
from dozo.media.photo import Photo
|
from ordigi.exiftool import ExifTool, ExifToolCaching
|
||||||
from dozo.media.video import Video
|
|
||||||
from dozo.exiftool import ExifTool, ExifToolCaching
|
|
||||||
|
|
||||||
DOZO_PATH = Path(__file__).parent.parent
|
ORDIGI_PATH = Path(__file__).parent.parent
|
||||||
CACHING = True
|
CACHING = True
|
||||||
|
|
||||||
class TestMetadata:
|
class TestMetadata:
|
||||||
|
|
|
@ -0,0 +1,612 @@
|
||||||
|
# NOW
|
||||||
|
|
||||||
|
# Media:
|
||||||
|
- rewrite set_date...
|
||||||
|
|
||||||
|
# Test:
|
||||||
|
- finish filesystem
|
||||||
|
- date_taken
|
||||||
|
- geolocation
|
||||||
|
|
||||||
|
move elodie to dozo
|
||||||
|
|
||||||
|
check for early morning photos: add test
|
||||||
|
|
||||||
|
add --folder-path option %Y-%d-%m/%city/%album
|
||||||
|
datetime.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
add %filename
|
||||||
|
add edit_exif command?
|
||||||
|
|
||||||
|
Add update command
|
||||||
|
|
||||||
|
# enhancement
|
||||||
|
- acccept Path in get_exiftool
|
||||||
|
- Use get_exiftool instead of get metadata:
|
||||||
|
try to do it in get_date_taken...
|
||||||
|
media class:
|
||||||
|
- Add self.file_path
|
||||||
|
-
|
||||||
|
|
||||||
|
## Album form folder
|
||||||
|
- move to filesystem
|
||||||
|
# TODO implement album from folder here?
|
||||||
|
# folder = os.path.basename(os.path.dirname(source))
|
||||||
|
# album = self.metadata['album']
|
||||||
|
# if album_from_folder and (album is None or album == ''):
|
||||||
|
# album = folder
|
||||||
|
# Update
|
||||||
|
use pathlib instead of os.path
|
||||||
|
|
||||||
|
Allow update in sort command in same dir if path is the dest dir
|
||||||
|
|
||||||
|
ENhancement: swap hash db key value: for checking file integrity
|
||||||
|
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
|
||||||
|
|
||||||
|
Enhancement: Option to keep existing directory structure
|
||||||
|
|
||||||
|
|
||||||
|
Fix: change versvalidion number to 0.x99
|
||||||
|
Fix: README
|
||||||
|
|
||||||
|
Refactoring: elodie update: update metadata of destination
|
||||||
|
|
||||||
|
Fix: update: fix move files...
|
||||||
|
|
||||||
|
Refactoring: Move exiftool config
|
||||||
|
|
||||||
|
Checksum:
|
||||||
|
FIX: test if checksum remain the same for all files (global check)
|
||||||
|
FIX: if dest file already here and checksum d'ont match change name to
|
||||||
|
prevent overwriting to file with same dest path
|
||||||
|
|
||||||
|
Enhancement: media file, do not filter files, only to prevent error when copying
|
||||||
|
fix: Valid file: check for open file error
|
||||||
|
|
||||||
|
Enhancement: Add %base_name string key
|
||||||
|
|
||||||
|
Refactoring: class get_metadata
|
||||||
|
check if as exiF, check exif type...
|
||||||
|
|
||||||
|
Interface: show error and warning
|
||||||
|
interface: less verbose when no error
|
||||||
|
interface: Move default setting to config?
|
||||||
|
|
||||||
|
Behavior: Move only by defaut without changing metatdata and filename...
|
||||||
|
|
||||||
|
Refactoring: check one time media is valid?
|
||||||
|
Refactoring: Unify source and path
|
||||||
|
Enhancement: allow nested dir
|
||||||
|
Fix: check exclusion for file
|
||||||
|
Refactoring: Import perl as submodule?
|
||||||
|
|
||||||
|
Enhancement: # setup arguments to exiftool
|
||||||
|
https://github.com/andrewning/sortphotos/blob/master/src/sortphotos.py
|
||||||
|
|
||||||
|
# AFTER
|
||||||
|
Enhancement: add walklevel function
|
||||||
|
Enhancement: change early morning date sort
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
Fix: date, make correction in filename if needed
|
||||||
|
Check: date from filename
|
||||||
|
Options:
|
||||||
|
--update-cache|-u
|
||||||
|
--date-from-filename
|
||||||
|
--location --time
|
||||||
|
# --date from folder
|
||||||
|
# --date from file
|
||||||
|
# -f overwrite metadata
|
||||||
|
|
||||||
|
Add get tag function
|
||||||
|
Add --copy alternative
|
||||||
|
--auto|-a: a set of option: geolocalisation, best match date, rename, album
|
||||||
|
from folder...
|
||||||
|
defaut: only move
|
||||||
|
# --keep-folder option
|
||||||
|
# --rename
|
||||||
|
-- no cache mode!!
|
||||||
|
--confirm unsure operation
|
||||||
|
--interactive
|
||||||
|
|
||||||
|
|
||||||
|
# TEST
|
||||||
|
# lat='45.58339'
|
||||||
|
# lon='4.79823'
|
||||||
|
# coordinates ='53.480837, -2.244914'
|
||||||
|
# Alger
|
||||||
|
# coords=(36.752887, 3.042048)
|
||||||
|
|
||||||
|
https://www.gitmemory.com/issue/pallets/click/843/634305917
|
||||||
|
https://github.com/pallets/click/issues/843
|
||||||
|
|
||||||
|
# import unittest
|
||||||
|
# import pytest
|
||||||
|
|
||||||
|
# from thing.__main__ import cli
|
||||||
|
|
||||||
|
|
||||||
|
# class TestCli(unittest.TestCase):
|
||||||
|
|
||||||
|
# @pytest.fixture(autouse=True)
|
||||||
|
# def capsys(self, capsys):
|
||||||
|
# self.capsys = capsys
|
||||||
|
|
||||||
|
# def test_cli(self):
|
||||||
|
# with pytest.raises(SystemExit) as ex:
|
||||||
|
# cli(["create", "--name", "test"])
|
||||||
|
# self.assertEqual(ex.value.code, 0)
|
||||||
|
# out, err = self.capsys.readouterr()
|
||||||
|
# self.assertEqual(out, "Succesfully created test\n")
|
||||||
|
|
||||||
|
|
||||||
|
# dev
|
||||||
|
# mode ~/.elodie ~/.config/elodie
|
||||||
|
# location selection buggy
|
||||||
|
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# /home/cedric/src/elodie/elodie/media/photo.py(86)get_date_taken()
|
||||||
|
# 85 # TODO potential bu for old photo below 1970...
|
||||||
|
# ---> 86 if(seconds_since_epoch == 0):
|
||||||
|
# 87 return None
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
def walklevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
if num_sep + level <= num_sep_this:
|
||||||
|
del dirs[:]
|
||||||
|
49/2: y=walklevel('/home/cedric', level=1)
|
||||||
|
49/3: next(y)
|
||||||
|
49/4: next(y)
|
||||||
|
49/5: next(y)
|
||||||
|
49/6: next(y)
|
||||||
|
49/7: next(y)
|
||||||
|
49/8: y=walklevel('/home/cedric', level=0)
|
||||||
|
49/9: next(y)
|
||||||
|
49/10: next(y)
|
||||||
|
49/11: y=walklevel('/home/cedric/.test/Nexcloud/', level=0)
|
||||||
|
49/12:
|
||||||
|
import os
|
||||||
|
|
||||||
|
def walklevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
if num_sep + level <= num_sep_this:
|
||||||
|
print dirs, files
|
||||||
|
49/13:
|
||||||
|
import os
|
||||||
|
|
||||||
|
def walklevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
if num_sep + level <= num_sep_this:
|
||||||
|
print(dirs, files)
|
||||||
|
49/14: y=walklevel('/home/cedric/.test/Nexcloud/', level=0)
|
||||||
|
49/15: next(y)
|
||||||
|
49/16: next(y)
|
||||||
|
49/17: y=walklevel('/home/cedric/.test/Nexcloud/', level=0)
|
||||||
|
49/18:
|
||||||
|
import os
|
||||||
|
|
||||||
|
def walklevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
49/19: y=walklevel('/home/cedric/.test/Nexcloud/', level=0)
|
||||||
|
49/20: next(y)
|
||||||
|
49/21: next(y)
|
||||||
|
49/22: y=walklevel('/home/cedric/.test/Nexcloud/', level=2)
|
||||||
|
49/23: next(y)
|
||||||
|
49/24: next(y)
|
||||||
|
49/25: y=walklevel('/home/cedric/.test/las canarias 2012/', level=2)
|
||||||
|
49/26: next(y)
|
||||||
|
49/27: next(y)
|
||||||
|
49/28: next(y)
|
||||||
|
49/29: next(y)
|
||||||
|
49/30: y=walklevel('/home/cedric/.test/las canarias 2012/', level=0)
|
||||||
|
49/31: next(y)
|
||||||
|
49/32: next(y)
|
||||||
|
49/33: next(y)
|
||||||
|
49/34:
|
||||||
|
import os
|
||||||
|
|
||||||
|
def walklevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
if num_sep + level <= num_sep_this:
|
||||||
|
print('fuck')
|
||||||
|
49/35: y=walklevel('/home/cedric/.test/las canarias 2012/', level=0)
|
||||||
|
49/36: next(y)
|
||||||
|
49/37: next(y)
|
||||||
|
49/38: next(y)
|
||||||
|
64/1: a=os.walk('/home/cedric/.test/las canarias 2012')
|
||||||
|
64/2: import os
|
||||||
|
64/3: a=os.walk('/home/cedric/.test/las canarias 2012')
|
||||||
|
64/4: next(a)
|
||||||
|
64/5: next(a)
|
||||||
|
64/6: os.path.sep
|
||||||
|
64/7: os.path.relpath('/home/cedric/.test/las canarias 2012/private', 'private')
|
||||||
|
64/8: os.path.relpath('/home/cedric/.test/las canarias 2012', 'private')
|
||||||
|
64/9: os.path.relpath('/home/cedric/.test/las canarias 2012/private', '/home/cedric/.test/las canarias 2012')
|
||||||
|
64/10: b='test'
|
||||||
|
64/11: a='private'
|
||||||
|
64/12: a+b
|
||||||
|
64/13: os.path.join(a,b,b)
|
||||||
|
64/14: !True
|
||||||
|
64/15: not True
|
||||||
|
64/16: a=TRue
|
||||||
|
64/17: a=True
|
||||||
|
64/18: not a
|
||||||
|
77/1:
|
||||||
|
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 {}
|
||||||
|
77/2: cd ~/.test/
|
||||||
|
77/3: ls
|
||||||
|
77/4: cd 2021-02-Feb/
|
||||||
|
77/5: ls
|
||||||
|
77/6: cd Villeurbanne/
|
||||||
|
77/7: ls
|
||||||
|
77/8: ls -l
|
||||||
|
77/9: exif = get_exif('2021-02-24_09-33-29-20210305_081001_01.mp4')
|
||||||
|
77/10:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
def get_exif(filename):
|
||||||
|
image = Image.open(filename)
|
||||||
|
image.verify()
|
||||||
|
return image._getexif()
|
||||||
|
77/11: exif = get_exif('2021-02-24_09-33-29-20210305_081001_01.mp4')
|
||||||
|
77/12: ..
|
||||||
|
77/13: cd ..
|
||||||
|
77/14: ls
|
||||||
|
77/15: cd ..
|
||||||
|
77/16: ls
|
||||||
|
77/17: cd 2021-03-Mar/
|
||||||
|
77/18: cd Villeurbanne/
|
||||||
|
77/19: ls
|
||||||
|
77/20: exif = get_exif('2021-03-09_09-58-42-img_20210309_105842.jpg')
|
||||||
|
77/21: exif
|
||||||
|
77/22:
|
||||||
|
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
|
||||||
|
77/23: get_geotagging(exif)
|
||||||
|
77/24: from PIL.ExifTags import TAGS
|
||||||
|
77/25:
|
||||||
|
def get_labeled_exif(exif):
|
||||||
|
labeled = {}
|
||||||
|
for (key, val) in exif.items():
|
||||||
|
labeled[TAGS.get(key)] = val
|
||||||
|
|
||||||
|
return labeled
|
||||||
|
77/26: get_geotagging(exif)
|
||||||
|
77/27: from PIL.ExifTags import GPSTAGS
|
||||||
|
77/28: get_geotagging(exif)
|
||||||
|
77/29: geotags = get_geotagging(exif)
|
||||||
|
77/30: get_location(geotags)
|
||||||
|
77/31:
|
||||||
|
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)
|
||||||
|
77/32: get_geotagging(exif)
|
||||||
|
77/33: get_location(geotags)
|
||||||
|
77/34: from geopy.geocoders import Here
|
||||||
|
78/1: from geopy.geocoders import Here
|
||||||
|
78/3:
|
||||||
|
78/4: get_exif
|
||||||
|
78/5: ls
|
||||||
|
78/6: cd ~/.test
|
||||||
|
78/7: ls
|
||||||
|
78/8: cd 2021-03-Mar/
|
||||||
|
78/9: ls
|
||||||
|
78/10: cd Villeurbanne/
|
||||||
|
78/11: get_exif('2021-03-04_11-50-32-img_20210304_125032.jpg')
|
||||||
|
78/12: exif=get_exif('2021-03-04_11-50-32-img_20210304_125032.jpg')
|
||||||
|
78/13: get_geotagging(exif)
|
||||||
|
78/14:
|
||||||
|
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
|
||||||
|
78/15: geotags = get_geotagging(exif)
|
||||||
|
78/17: geotags = get_geotagging(exif)
|
||||||
|
78/18: get_coordinates(geotags)
|
||||||
|
78/19:
|
||||||
|
|
||||||
|
78/23: get_location(geotags)
|
||||||
|
78/24:
|
||||||
|
78/25: get_location(geotags)
|
||||||
|
78/26:
|
||||||
|
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)
|
||||||
|
78/27: get_location(geotags)
|
||||||
|
78/28:
|
||||||
|
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)
|
||||||
|
78/29: get_location(geotags)
|
||||||
|
78/30: exif
|
||||||
|
78/31: get_geotagging(exif)
|
||||||
|
78/32: geotags = get_geotagging(exif)
|
||||||
|
78/33: get_coordinates(geotags)
|
||||||
|
78/34: geotags = get_geotagging(exif)
|
||||||
|
78/35: get_location(geotags)
|
||||||
|
78/36: get_coordinates(geotags)
|
||||||
|
78/37: coords = get_coordinates(geotags)
|
||||||
|
78/38: coords
|
||||||
|
78/39: uri = 'https://revgeocode.search.hereapi.com/v1/revgeocode'
|
||||||
|
78/40:
|
||||||
|
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']
|
Loading…
Reference in New Issue