parent
4a7df2d57d
commit
0864f58a12
|
@ -3,6 +3,7 @@ import shutil
|
||||||
|
|
||||||
from elodie import constants
|
from elodie import constants
|
||||||
|
|
||||||
|
|
||||||
def _decode(string, encoding='utf8'):
|
def _decode(string, encoding='utf8'):
|
||||||
"""Return a utf8 encoded unicode string.
|
"""Return a utf8 encoded unicode string.
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ def _decode(string, encoding='utf8'):
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
def _copyfile(src, dst):
|
def _copyfile(src, dst):
|
||||||
# Python 3 hangs using open/write method
|
# Python 3 hangs using open/write method
|
||||||
if (constants.python_version == 3):
|
if (constants.python_version == 3):
|
||||||
|
@ -45,7 +47,11 @@ def _copyfile(src, dst):
|
||||||
for x in iter(lambda: os.read(fin, BUFFER_SIZE), ""):
|
for x in iter(lambda: os.read(fin, BUFFER_SIZE), ""):
|
||||||
os.write(fout, x)
|
os.write(fout, x)
|
||||||
finally:
|
finally:
|
||||||
try: os.close(fin)
|
try:
|
||||||
except: pass
|
os.close(fin)
|
||||||
try: os.close(fout)
|
except:
|
||||||
except: pass
|
pass
|
||||||
|
try:
|
||||||
|
os.close(fout)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
|
@ -291,6 +291,7 @@ class FileSystem(object):
|
||||||
print('%s is not a valid media file. Skipping...' % _file)
|
print('%s is not a valid media file. Skipping...' % _file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
media.set_original_name()
|
||||||
metadata = media.get_metadata()
|
metadata = media.get_metadata()
|
||||||
|
|
||||||
directory_name = self.get_folder_path(metadata)
|
directory_name = self.get_folder_path(metadata)
|
||||||
|
@ -334,9 +335,8 @@ class FileSystem(object):
|
||||||
# Do not use copy2(), will have an issue when copying to a
|
# Do not use copy2(), will have an issue when copying to a
|
||||||
# network/mounted drive using copy and manual
|
# network/mounted drive using copy and manual
|
||||||
# set_date_from_filename gets the job done
|
# set_date_from_filename gets the job done
|
||||||
# Do not use copy2(), will have an issue when copying to a network/mounted drive
|
|
||||||
# shutil.copy seems slow, changing to streaming according to
|
# shutil.copy seems slow, changing to streaming according to
|
||||||
# http://stackoverflow.com/questions/22078621/python-how-to-copy-files-fast
|
# http://stackoverflow.com/questions/22078621/python-how-to-copy-files-fast # noqa
|
||||||
compatability._copyfile(_file, dest_path)
|
compatability._copyfile(_file, dest_path)
|
||||||
self.set_utime(media)
|
self.set_utime(media)
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,7 @@ class Base(object):
|
||||||
'album': self.get_album(),
|
'album': self.get_album(),
|
||||||
'title': self.get_title(),
|
'title': self.get_title(),
|
||||||
'mime_type': self.get_mimetype(),
|
'mime_type': self.get_mimetype(),
|
||||||
|
'original_name': self.get_original_name(),
|
||||||
'base_name': os.path.splitext(os.path.basename(source))[0],
|
'base_name': os.path.splitext(os.path.basename(source))[0],
|
||||||
'extension': self.get_extension(),
|
'extension': self.get_extension(),
|
||||||
'directory_path': os.path.dirname(source)
|
'directory_path': os.path.dirname(source)
|
||||||
|
@ -100,7 +101,7 @@ class Base(object):
|
||||||
def get_mimetype(self):
|
def get_mimetype(self):
|
||||||
"""Get the mimetype of the file.
|
"""Get the mimetype of the file.
|
||||||
|
|
||||||
:returns: str or None for a non-video
|
:returns: str or None for unsupported files.
|
||||||
"""
|
"""
|
||||||
if(not self.is_valid()):
|
if(not self.is_valid()):
|
||||||
return None
|
return None
|
||||||
|
@ -112,6 +113,15 @@ class Base(object):
|
||||||
|
|
||||||
return mimetype[0]
|
return mimetype[0]
|
||||||
|
|
||||||
|
def get_original_name(self):
|
||||||
|
"""Get the original name of the file from before it was imported.
|
||||||
|
Does not include the extension.
|
||||||
|
Overridden by Media class for files with EXIF.
|
||||||
|
|
||||||
|
:returns: str or None for unsupported files.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
"""Base method for getting the title of a file
|
"""Base method for getting the title of a file
|
||||||
|
|
||||||
|
@ -185,6 +195,12 @@ class Base(object):
|
||||||
if(key in metadata):
|
if(key in metadata):
|
||||||
self.metadata[key] = kwargs[key]
|
self.metadata[key] = kwargs[key]
|
||||||
|
|
||||||
|
def set_original_name(self):
|
||||||
|
"""Stores the original file name into EXIF/metadata.
|
||||||
|
:returns: bool
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_class_by_file(cls, _file, classes):
|
def get_class_by_file(cls, _file, classes):
|
||||||
"""Static method to get a media object by file.
|
"""Static method to get a media object by file.
|
||||||
|
|
|
@ -10,6 +10,8 @@ are used to represent the actual files.
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
# load modules
|
# load modules
|
||||||
from elodie import constants
|
from elodie import constants
|
||||||
from elodie.dependencies import get_exiftool
|
from elodie.dependencies import get_exiftool
|
||||||
|
@ -46,6 +48,7 @@ class Media(Base):
|
||||||
self.longitude_keys = ['EXIF:GPSLongitude']
|
self.longitude_keys = ['EXIF:GPSLongitude']
|
||||||
self.latitude_ref_key = 'EXIF:GPSLatitudeRef'
|
self.latitude_ref_key = 'EXIF:GPSLatitudeRef'
|
||||||
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
|
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
|
||||||
|
self.original_name_key = 'XMP:OriginalFileName'
|
||||||
self.set_gps_ref = True
|
self.set_gps_ref = True
|
||||||
self.exiftool_addedargs = [
|
self.exiftool_addedargs = [
|
||||||
'-overwrite_original',
|
'-overwrite_original',
|
||||||
|
@ -94,7 +97,7 @@ class Media(Base):
|
||||||
# Cast coordinate to a float due to a bug in exiftool's
|
# Cast coordinate to a float due to a bug in exiftool's
|
||||||
# -json output format.
|
# -json output format.
|
||||||
# https://github.com/jmathai/elodie/issues/171
|
# https://github.com/jmathai/elodie/issues/171
|
||||||
# http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html #noqa
|
# http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html # noqa
|
||||||
this_coordinate = float(exif[key])
|
this_coordinate = float(exif[key])
|
||||||
|
|
||||||
# TODO: verify that we need to check ref key
|
# TODO: verify that we need to check ref key
|
||||||
|
@ -129,6 +132,24 @@ class Media(Base):
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
def get_original_name(self):
|
||||||
|
"""Get the original name stored in EXIF.
|
||||||
|
|
||||||
|
:returns: str
|
||||||
|
"""
|
||||||
|
if(not self.is_valid()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
exiftool_attributes = self.get_exiftool_attributes()
|
||||||
|
|
||||||
|
if exiftool_attributes is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if(self.original_name_key not in exiftool_attributes):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return exiftool_attributes[self.original_name_key]
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
"""Get the title for a photo of video
|
"""Get the title for a photo of video
|
||||||
|
|
||||||
|
@ -213,6 +234,25 @@ class Media(Base):
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def set_original_name(self):
|
||||||
|
"""Sets the original name EXIF tag if not already set.
|
||||||
|
|
||||||
|
:returns: True, False, None
|
||||||
|
"""
|
||||||
|
if(not self.is_valid()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If EXIF original name tag is set then we return.
|
||||||
|
if self.get_original_name() is not None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
source = self.source
|
||||||
|
name = os.path.basename(source)
|
||||||
|
tags = {self.original_name_key: name}
|
||||||
|
status = self.__set_tags(tags)
|
||||||
|
self.reset_cache()
|
||||||
|
return status
|
||||||
|
|
||||||
def set_title(self, title):
|
def set_title(self, title):
|
||||||
"""Set title for a photo.
|
"""Set title for a photo.
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,16 @@ class Text(Base):
|
||||||
self.parse_metadata_line()
|
self.parse_metadata_line()
|
||||||
return super(Text, self).get_metadata()
|
return super(Text, self).get_metadata()
|
||||||
|
|
||||||
|
def get_original_name(self):
|
||||||
|
self.parse_metadata_line()
|
||||||
|
|
||||||
|
# We return the value if found in metadata
|
||||||
|
if(isinstance(self.metadata_line, dict) and
|
||||||
|
'original_name' in self.metadata_line):
|
||||||
|
return self.metadata_line['original_name']
|
||||||
|
|
||||||
|
return super(Text, self).get_original_name()
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
self.parse_metadata_line()
|
self.parse_metadata_line()
|
||||||
|
|
||||||
|
@ -92,11 +102,6 @@ class Text(Base):
|
||||||
self.reset_cache()
|
self.reset_cache()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def set_location(self, latitude, longitude):
|
|
||||||
status = self.write_metadata(latitude=latitude, longitude=longitude)
|
|
||||||
self.reset_cache()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def set_date_taken(self, passed_in_time):
|
def set_date_taken(self, passed_in_time):
|
||||||
if(time is None):
|
if(time is None):
|
||||||
return False
|
return False
|
||||||
|
@ -106,6 +111,29 @@ class Text(Base):
|
||||||
self.reset_cache()
|
self.reset_cache()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def set_original_name(self):
|
||||||
|
"""Sets the original name if not already set.
|
||||||
|
|
||||||
|
:returns: True, False, None
|
||||||
|
"""
|
||||||
|
if(not self.is_valid()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If EXIF original name tag is set then we return.
|
||||||
|
if self.get_original_name() is not None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
source = self.source
|
||||||
|
name = os.path.basename(source)
|
||||||
|
status = self.write_metadata(original_name=name)
|
||||||
|
self.reset_cache()
|
||||||
|
return status
|
||||||
|
|
||||||
|
def set_location(self, latitude, longitude):
|
||||||
|
status = self.write_metadata(latitude=latitude, longitude=longitude)
|
||||||
|
self.reset_cache()
|
||||||
|
return status
|
||||||
|
|
||||||
def parse_metadata_line(self):
|
def parse_metadata_line(self):
|
||||||
if isinstance(self.metadata_line, dict):
|
if isinstance(self.metadata_line, dict):
|
||||||
return self.metadata_line
|
return self.metadata_line
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
{"date_taken":1460027726.0,"latitude":"51.521435","longitude":"0.162714","title":"sample title","original_name":"originalname.txt"}
|
||||||
|
|
||||||
|
This file has a valid header.
|
|
@ -36,11 +36,16 @@ def create_working_folder():
|
||||||
|
|
||||||
def download_file(name, destination):
|
def download_file(name, destination):
|
||||||
try:
|
try:
|
||||||
|
url_to_file = 'https://s3.amazonaws.com/jmathai/github/elodie/{}'.format(name)
|
||||||
|
# urlretrieve works differently for python 2 and 3
|
||||||
|
if constants.python_version < 3:
|
||||||
final_name = '{}/{}{}'.format(destination, random_string(10), os.path.splitext(name)[1])
|
final_name = '{}/{}{}'.format(destination, random_string(10), os.path.splitext(name)[1])
|
||||||
urllib.urlretrieve(
|
urllib.urlretrieve(
|
||||||
'https://s3.amazonaws.com/jmathai/github/elodie/{}'.format(name),
|
url_to_file,
|
||||||
final_name
|
final_name
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
final_name, headers = urllib.request.urlretrieve(url_to_file)
|
||||||
return final_name
|
return final_name
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -24,6 +24,10 @@ from elodie.media.video import Video
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_subclasses():
|
||||||
|
subclasses = get_all_subclasses(Base)
|
||||||
|
expected = {Media, Base, Text, Photo, Video, Audio}
|
||||||
|
assert subclasses == expected, subclasses
|
||||||
|
|
||||||
def test_get_class_by_file_without_extension():
|
def test_get_class_by_file_without_extension():
|
||||||
base_file = helper.get_file('withoutextension')
|
base_file = helper.get_file('withoutextension')
|
||||||
|
@ -32,6 +36,32 @@ def test_get_class_by_file_without_extension():
|
||||||
|
|
||||||
assert cls is None, cls
|
assert cls is None, cls
|
||||||
|
|
||||||
|
def test_get_original_name():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'with-original-name.jpg')
|
||||||
|
file = helper.get_file('with-original-name.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name == 'originalfilename.jpg', original_name
|
||||||
|
|
||||||
|
def test_get_original_name_invalid_file():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'invalid.jpg')
|
||||||
|
file = helper.get_file('invalid.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name is None, original_name
|
||||||
|
|
||||||
def test_set_album_from_folder_invalid_file():
|
def test_set_album_from_folder_invalid_file():
|
||||||
temporary_folder, folder = helper.create_working_folder()
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
@ -106,8 +136,3 @@ def test_set_metadata_basename():
|
||||||
new_metadata = photo.get_metadata()
|
new_metadata = photo.get_metadata()
|
||||||
|
|
||||||
assert new_metadata['base_name'] == new_basename, new_metadata['base_name']
|
assert new_metadata['base_name'] == new_basename, new_metadata['base_name']
|
||||||
|
|
||||||
def test_get_all_subclasses():
|
|
||||||
subclasses = get_all_subclasses(Base)
|
|
||||||
expected = {Media, Base, Text, Photo, Video, Audio}
|
|
||||||
assert subclasses == expected, subclasses
|
|
||||||
|
|
|
@ -61,6 +61,88 @@ def test_get_class_by_file_invalid_type():
|
||||||
[Photo, Video, Audio])
|
[Photo, Video, Audio])
|
||||||
assert media is None
|
assert media is None
|
||||||
|
|
||||||
|
def test_get_original_name():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'with-original-name.jpg')
|
||||||
|
file = helper.get_file('with-original-name.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name == 'originalfilename.jpg', original_name
|
||||||
|
|
||||||
|
def test_get_original_name_invalid_file():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'invalid.jpg')
|
||||||
|
file = helper.get_file('invalid.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name is None, original_name
|
||||||
|
|
||||||
|
def test_set_original_name_when_exists():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'with-original-name.jpg')
|
||||||
|
file = helper.get_file('with-original-name.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
result = media.set_original_name()
|
||||||
|
|
||||||
|
assert result is None, result
|
||||||
|
|
||||||
|
def test_set_original_name_when_does_not_exist():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
origin = '%s/%s' % (folder, 'plain.jpg')
|
||||||
|
file = helper.get_file('plain.jpg')
|
||||||
|
|
||||||
|
shutil.copyfile(file, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Photo])
|
||||||
|
metadata_before = media.get_metadata()
|
||||||
|
result = media.set_original_name()
|
||||||
|
metadata_after = media.get_metadata()
|
||||||
|
|
||||||
|
assert metadata_before['original_name'] is None, metadata_before
|
||||||
|
assert metadata_after['original_name'] == 'plain.jpg', metadata_after
|
||||||
|
assert result is True, result
|
||||||
|
|
||||||
|
def test_set_original_name():
|
||||||
|
files = ['plain.jpg', 'audio.m4a', 'photo.nef', 'video.mov']
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
ext = os.path.splitext(file)[1]
|
||||||
|
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
random_file_name = '%s%s' % (helper.random_string(10), ext)
|
||||||
|
origin = '%s/%s' % (folder, random_file_name)
|
||||||
|
file_path = helper.get_file(file)
|
||||||
|
if file_path is False:
|
||||||
|
file_path = helper.download_file(file, folder)
|
||||||
|
|
||||||
|
shutil.copyfile(file_path, origin)
|
||||||
|
|
||||||
|
media = Media.get_class_by_file(origin, [Audio, Media, Photo, Video])
|
||||||
|
metadata = media.get_metadata()
|
||||||
|
media.set_original_name()
|
||||||
|
metadata_updated = media.get_metadata()
|
||||||
|
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
|
assert metadata['original_name'] is None, metadata['original_name']
|
||||||
|
assert metadata_updated['original_name'] == random_file_name, metadata_updated['original_name']
|
||||||
|
|
||||||
def is_valid():
|
def is_valid():
|
||||||
media = Media()
|
media = Media()
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,18 @@ def test_text_extensions():
|
||||||
|
|
||||||
assert extensions == valid_extensions, valid_extensions
|
assert extensions == valid_extensions, valid_extensions
|
||||||
|
|
||||||
|
def test_get_original_name():
|
||||||
|
media = Text(helper.get_file('with-original-name.txt'))
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name == 'originalname.txt', original_name
|
||||||
|
|
||||||
|
def test_get_original_name_when_does_not_exist():
|
||||||
|
media = Text(helper.get_file('valid.txt'))
|
||||||
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
|
assert original_name is None, original_name
|
||||||
|
|
||||||
def test_get_title():
|
def test_get_title():
|
||||||
text = Text(helper.get_file('valid.txt'))
|
text = Text(helper.get_file('valid.txt'))
|
||||||
text.get_metadata()
|
text.get_metadata()
|
||||||
|
@ -268,3 +280,20 @@ def test_set_location_without_header():
|
||||||
shutil.rmtree(folder)
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
assert helper.isclose(metadata['latitude'], 11.1111111111), metadata['latitude']
|
assert helper.isclose(metadata['latitude'], 11.1111111111), metadata['latitude']
|
||||||
|
|
||||||
|
def test_set_original_name():
|
||||||
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
random_file_name = '%s.txt' % helper.random_string(10)
|
||||||
|
origin = '%s/%s' % (folder, random_file_name)
|
||||||
|
shutil.copyfile(helper.get_file('valid.txt'), origin)
|
||||||
|
|
||||||
|
text = Text(origin)
|
||||||
|
metadata = text.get_metadata()
|
||||||
|
text.set_original_name()
|
||||||
|
metadata_updated = text.get_metadata()
|
||||||
|
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
|
assert metadata['original_name'] is None, metadata['original_name']
|
||||||
|
assert metadata_updated['original_name'] == random_file_name, metadata_updated['original_name']
|
||||||
|
|
Loading…
Reference in New Issue