Refactoring media class

This commit is contained in:
Cédric Leporcq 2021-06-20 19:51:21 +02:00
parent 6b2d2b31c3
commit cb8a4cd24e
13 changed files with 469 additions and 556 deletions

View File

@ -155,8 +155,8 @@ def _import(destination, source, file, album_from_folder, trash,
sys.exit(1) sys.exit(1)
@click.command('generate-db') @click.command('generate-db')
@click.option('--source', type=click.Path(file_okay=False), @click.option('--path', type=click.Path(file_okay=False),
required=True, help='Source 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):
@ -164,17 +164,17 @@ def _generate_db(path, debug):
""" """
constants.debug = debug constants.debug = debug
result = Result() result = Result()
source = os.path.abspath(os.path.expanduser(source)) path = os.path.abspath(os.path.expanduser(path))
if not os.path.isdir(source): if not os.path.isdir(path):
log.error('Source is not a valid directory %s' % source) log.error('path is not a valid directory %s' % path)
sys.exit(1) sys.exit(1)
db = Db(path) db = Db(path)
db.backup_hash_db() db.backup_hash_db()
db.reset_hash_db() db.reset_hash_db()
for current_file in FILESYSTEM.get_all_files(source): for current_file in FILESYSTEM.get_all_files(path):
result.append((current_file, True)) result.append((current_file, True))
db.add_hash(db.checksum(current_file), current_file) db.add_hash(db.checksum(current_file), current_file)
log.progress() log.progress()
@ -184,9 +184,11 @@ def _generate_db(path, debug):
result.write() result.write()
@click.command('verify') @click.command('verify')
@click.option('--path', type=click.Path(file_okay=False),
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 _verify(debug): def _verify(path, debug):
constants.debug = debug constants.debug = debug
result = Result() result = Result()
db = Db(path) db = Db(path)
@ -216,7 +218,7 @@ def update_location(media, file_path, location_name, db):
if location_coords and 'latitude' in location_coords and \ if location_coords and 'latitude' in location_coords and \
'longitude' in location_coords: 'longitude' in location_coords:
location_status = media.set_location(location_coords[ location_status = media.set_location(location_coords[
'latitude'], location_coords['longitude']) 'latitude'], location_coords['longitude'], file_path)
if not location_status: if not location_status:
log.error('Failed to update location') log.error('Failed to update location')
log.all(('{"source":"%s",' % file_path, log.all(('{"source":"%s",' % file_path,
@ -307,7 +309,7 @@ def _update(album, location, time, title, paths, debug):
update_time(media, current_file, time) update_time(media, current_file, time)
updated = True updated = True
if album: if album:
media.set_album(album) media.set_album(album, current_file)
updated = True updated = True
import ipdb; ipdb.set_trace() import ipdb; ipdb.set_trace()

View File

@ -17,7 +17,7 @@ from elodie.config import load_config
from elodie import constants from elodie import constants
from elodie.localstorage import Db from elodie.localstorage import Db
from elodie.media import base from elodie.media import media
from elodie.plugins.plugins import Plugins from elodie.plugins.plugins import Plugins
class FileSystem(object): class FileSystem(object):
@ -94,7 +94,7 @@ class FileSystem(object):
# If extensions is None then we get all supported extensions # If extensions is None then we get all supported extensions
if not extensions: if not extensions:
extensions = set() extensions = set()
subclasses = base.get_all_subclasses() subclasses = media.get_all_subclasses()
for cls in subclasses: for cls in subclasses:
extensions.update(cls.extensions) extensions.update(cls.extensions)
@ -679,7 +679,9 @@ class FileSystem(object):
if album_from_folder: if album_from_folder:
media.set_album_from_folder(dest_path) media.set_album_from_folder(dest_path)
db.add_hash(checksum, dest_path) # get checksum of dest file
dest_checksum = db.checksum(dest_path)
db.add_hash(dest_checksum, dest_path)
db.update_hash_db() db.update_hash_db()
# Run `after()` for every loaded plugin and if any of them raise an exception # Run `after()` for every loaded plugin and if any of them raise an exception

View File

@ -1,15 +1,15 @@
""" """
The audio module contains classes specifically for dealing with audio files. The audio module contains classes specifically for dealing with audio files.
The :class:`Audio` class inherits from the :class:`~elodie.media.video.Video` The :class:`Audio` class inherits from the :class:`~elodie.media.Media`
class. class.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com> .. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
""" """
from .video import Video from .media import Media
class Audio(Video): class Audio(Media):
"""An audio object. """An audio object.
@ -22,4 +22,7 @@ class Audio(Video):
extensions = ('m4a',) extensions = ('m4a',)
def __init__(self, source=None): def __init__(self, source=None):
super(Audio, self).__init__(source) super().__init__(source)
def is_valid(self):
return super().is_valid()

View File

@ -1,264 +0,0 @@
"""
The base module provides a base :class:`Base` class for all objects that
are tracked by Elodie. The Base class provides some base functionality used
by all the media types, but isn't itself used to represent anything. Its
sub-classes (:class:`~elodie.media.audio.Audio`,
:class:`~elodie.media.photo.Photo`, :class:`~elodie.media.video.Video`)
are used to represent the actual files.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
import mimetypes
import os
try: # Py3k compatibility
basestring
except NameError:
basestring = (bytes, str)
class Base(object):
"""The base class for all media objects.
:param str source: The fully qualified path to the video file.
"""
__name__ = 'Base'
PHOTO = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
AUDIO = ('m4a',)
VIDEO = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts')
extensions = PHOTO + AUDIO + VIDEO
def __init__(self, source=None):
self.source = source
self.reset_cache()
def format_metadata(self, **kwargs):
"""Method to consistently return a populated metadata dictionary.
:returns: dict
"""
def get_album(self):
"""Base method for getting an album
:returns: None
"""
return None
def get_file_path(self):
"""Get the full path to the video.
:returns: string
"""
return self.source
def get_coordinate(self, type):
return None
def get_extension(self):
"""Get the file extension as a lowercased string.
:returns: string or None for a non-video
"""
if(not self.is_valid()):
return None
source = self.source
return os.path.splitext(source)[1][1:].lower()
def get_camera_make(self):
return None
def get_camera_model(self):
return None
def get_metadata(self, update_cache=False):
"""Get a dictionary of metadata for any file.
All keys will be present and have a value of None if not obtained.
:returns: dict or None for non-text files
"""
if(not self.is_valid()):
return None
if(isinstance(self.metadata, dict) and update_cache is False):
return self.metadata
source = self.source
folder = os.path.basename(os.path.dirname(source))
album = self.get_album()
album_from_folder = True
if album_from_folder and (album is None or album == ''):
album = folder
self.metadata = {
'date_original': self.get_date_attribute(self.date_original),
'date_created': self.get_date_attribute(self.date_created),
'date_modified': self.get_date_attribute(self.date_modified),
'camera_make': self.get_camera_make(),
'camera_model': self.get_camera_model(),
'latitude': self.get_coordinate('latitude'),
'longitude': self.get_coordinate('longitude'),
'album': album,
'title': self.get_title(),
'mime_type': self.get_mimetype(),
'original_name': self.get_original_name(),
'base_name': folder,
'extension': self.get_extension(),
'directory_path': os.path.dirname(source)
}
return self.metadata
def get_mimetype(self):
"""Get the mimetype of the file.
:returns: str or None for unsupported files.
"""
if(not self.is_valid()):
return None
source = self.source
mimetype = mimetypes.guess_type(source)
if(mimetype is None):
return None
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):
"""Base method for getting the title of a file
:returns: None
"""
return None
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
def reset_cache(self):
"""Resets any internal cache
"""
self.metadata = None
def set_album(self, name):
"""Base method for setting the album of a file
:returns: None
"""
return None
def set_album_from_folder(self, path):
"""Set the album attribute based on the leaf folder name
:returns: bool
"""
metadata = self.get_metadata()
# If this file has an album already set we do not overwrite EXIF
if(not isinstance(metadata, dict) or metadata['album'] is not None):
return False
folder = os.path.basename(metadata['directory_path'])
# If folder is empty we skip
if(len(folder) == 0):
return False
self.set_album(folder)
if status == False:
return False
return True
def set_metadata_basename(self, new_basename):
"""Update the basename attribute in the metadata dict for this instance.
This is used for when we update the EXIF title of a media file. Since
that determines the name of a file if we update the title of a file
more than once it appends to the file name.
i.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
:param str new_basename: New basename of file (with the old title
removed).
"""
self.get_metadata()
self.metadata['base_name'] = new_basename
def set_metadata(self, **kwargs):
"""Method to manually update attributes in metadata.
:params dict kwargs: Named parameters to update.
"""
metadata = self.get_metadata()
for key in kwargs:
if(key in metadata):
self.metadata[key] = kwargs[key]
def set_original_name(self):
"""Stores the original file name into EXIF/metadata.
:returns: bool
"""
return False
@classmethod
def get_class_by_file(cls, _file, classes):
"""Static method to get a media object by file.
"""
if not isinstance(_file, basestring) or not os.path.isfile(_file):
return None
extension = os.path.splitext(_file)[1][1:].lower()
if len(extension) > 0:
for i in classes:
if(extension in i.extensions):
return i(_file)
return None
@classmethod
def get_valid_extensions(cls):
"""Static method to access static extensions variable.
:returns: tuple(str)
"""
return cls.extensions
def get_all_subclasses(cls=None):
"""Module method to get all subclasses of Base.
"""
subclasses = set()
this_class = Base
if cls is not None:
this_class = cls
subclasses.add(this_class)
this_class_subclasses = this_class.__subclasses__()
for child_class in this_class_subclasses:
subclasses.update(get_all_subclasses(child_class))
return subclasses

View File

@ -1,14 +1,14 @@
""" """
The media module provides a base :class:`Media` class for media objects that The media module provides a base :class:`Media` class for media objects that
are tracked by Elodie. The Media class provides some base functionality used are tracked by Elodie. The Media class provides some base functionality used
by all the media types, but isn't itself used to represent anything. Its by all the media types. Its sub-classes (:class:`~elodie.media.Audio`,
sub-classes (:class:`~elodie.media.audio.Audio`, :class:`~elodie.media.Photo`, and :class:`~elodie.media.Video`)
:class:`~elodie.media.photo.Photo`, and :class:`~elodie.media.video.Video`)
are used to represent the actual files. are used to represent the actual files.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com> .. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
""" """
import mimetypes
import os import os
import six import six
@ -17,11 +17,17 @@ from elodie import log
from dateutil.parser import parse from dateutil.parser import parse
import re import re
from elodie.external.pyexiftool import ExifTool from elodie.external.pyexiftool import ExifTool
from elodie.media.base import Base
class Media(Base): # TODO remove
# try: # Py3k compatibility
# basestring
# except NameError:
# basestring = (bytes, str)
"""The base class for all media objects.
class Media():
"""The media class for all media objects.
:param str source: The fully qualified path to the video file. :param str source: The fully qualified path to the video file.
""" """
@ -33,23 +39,221 @@ class Media(Base):
'longitude': 'longitude_ref' 'longitude': 'longitude_ref'
} }
def __init__(self, source=None): PHOTO = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
super(Media, self).__init__(source) AUDIO = ('m4a',)
self.date_original = ['EXIF:DateTimeOriginal'] VIDEO = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts')
self.date_created = ['EXIF:CreateDate', 'QuickTime:CreateDate']
extensions = PHOTO + AUDIO + VIDEO
def __init__(self, sources=None):
self.source = sources
self.reset_cache()
self.date_original = [
'EXIF:DateTimeOriginal',
'H264:DateTimeOriginal',
'QuickTime:ContentCreateDate'
]
self.date_created = [
'EXIF:CreateDate',
'QuickTime:CreationDate',
'QuickTime:CreateDate',
'QuickTime:CreationDate-und-US',
'QuickTime:MediaCreateDate'
]
self.date_modified = ['File:FileModifyDate', 'QuickTime:ModifyDate'] self.date_modified = ['File:FileModifyDate', 'QuickTime:ModifyDate']
self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make'] self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make']
self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model'] self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model']
self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album'] self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album']
self.title_key = 'XMP:Title' self.title_keys = ['XMP:Title', 'XMP:DisplayName']
self.latitude_keys = ['EXIF:GPSLatitude'] self.latitude_keys = [
self.longitude_keys = ['EXIF:GPSLongitude'] 'EXIF:GPSLatitude',
'XMP:GPSLatitude',
# 'QuickTime:GPSLatitude',
'Composite:GPSLatitude'
]
self.longitude_keys = [
'EXIF:GPSLongitude',
'XMP:GPSLongitude',
# 'QuickTime:GPSLongitude',
'Composite: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.original_name_key = 'XMP:OriginalFileName'
self.set_gps_ref = True self.set_gps_ref = True
self.metadata = None
self.exif_metadata = None self.exif_metadata = None
def format_metadata(self, **kwargs):
"""Method to consistently return a populated metadata dictionary.
:returns: dict
"""
def get_file_path(self):
"""Get the full path to the video.
:returns: string
"""
return self.source
def get_extension(self):
"""Get the file extension as a lowercased string.
:returns: string or None for a non-video
"""
if(not self.is_valid()):
return None
source = self.source
return os.path.splitext(source)[1][1:].lower()
def get_metadata(self, update_cache=False, album_from_folder=False):
"""Get a dictionary of metadata for any file.
All keys will be present and have a value of None if not obtained.
:returns: dict or None for non-text files
"""
if(not self.is_valid()):
return None
if(isinstance(self.metadata, dict) and update_cache is False):
return self.metadata
source = self.source
folder = os.path.basename(os.path.dirname(source))
album = self.get_album()
if album_from_folder and (album is None or album == ''):
album = folder
self.metadata = {
'date_original': self.get_date_attribute(self.date_original),
'date_created': self.get_date_attribute(self.date_created),
'date_modified': self.get_date_attribute(self.date_modified),
'camera_make': self.get_camera_make(),
'camera_model': self.get_camera_model(),
'latitude': self.get_coordinate('latitude'),
'longitude': self.get_coordinate('longitude'),
'album': album,
'title': self.get_title(),
'mime_type': self.get_mimetype(),
'original_name': self.get_original_name(),
'base_name': os.path.basename(os.path.splitext(source)[0]),
'extension': self.get_extension(),
'directory_path': os.path.dirname(source)
}
return self.metadata
def get_mimetype(self):
"""Get the mimetype of the file.
:returns: str or None for unsupported files.
"""
if(not self.is_valid()):
return None
source = self.source
mimetype = mimetypes.guess_type(source)
if(mimetype is None):
return None
return mimetype[0]
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
def set_album_from_folder(self, path):
"""Set the album attribute based on the leaf folder name
:returns: bool
"""
metadata = self.get_metadata()
# If this file has an album already set we do not overwrite EXIF
if(not isinstance(metadata, dict) or metadata['album'] is not None):
return False
folder = os.path.basename(metadata['directory_path'])
# If folder is empty we skip
if(len(folder) == 0):
return False
status = self.set_album(folder, path)
if status == False:
return False
return True
def set_metadata_basename(self, new_basename):
"""Update the basename attribute in the metadata dict for this instance.
This is used for when we update the EXIF title of a media file. Since
that determines the name of a file if we update the title of a file
more than once it appends to the file name.
i.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
:param str new_basename: New basename of file (with the old title
removed).
"""
self.get_metadata()
self.metadata['base_name'] = new_basename
def set_metadata(self, **kwargs):
"""Method to manually update attributes in metadata.
:params dict kwargs: Named parameters to update.
"""
metadata = self.get_metadata()
for key in kwargs:
if(key in metadata):
self.metadata[key] = kwargs[key]
@classmethod
def get_class_by_file(cls, _file, classes):
"""Static method to get a media object by file.
"""
basestring = (bytes, str)
if not isinstance(_file, basestring) or not os.path.isfile(_file):
return None
extension = os.path.splitext(_file)[1][1:].lower()
if len(extension) > 0:
for i in classes:
if(extension in i.extensions):
return i(_file)
return None
@classmethod
def get_valid_extensions(cls):
"""Static method to access static extensions variable.
:returns: tuple(str)
"""
return cls.extensions
def get_album(self): def get_album(self):
"""Get album from EXIF """Get album from EXIF
@ -68,6 +272,7 @@ class Media(Base):
return None return None
def get_coordinate(self, type='latitude'): def get_coordinate(self, type='latitude'):
"""Get latitude or longitude of media from EXIF """Get latitude or longitude of media from EXIF
@ -114,6 +319,7 @@ class Media(Base):
return None return None
def get_exiftool_attributes(self): def get_exiftool_attributes(self):
"""Get attributes for the media object from exiftool. """Get attributes for the media object from exiftool.
@ -185,6 +391,7 @@ class Media(Base):
return None return None
def get_camera_model(self): def get_camera_model(self):
"""Get the camera make stored in EXIF. """Get the camera make stored in EXIF.
@ -204,6 +411,7 @@ class Media(Base):
return None return None
def get_original_name(self): def get_original_name(self):
"""Get the original name stored in EXIF. """Get the original name stored in EXIF.
@ -222,6 +430,7 @@ class Media(Base):
return exiftool_attributes[self.original_name_key] 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
@ -235,17 +444,19 @@ class Media(Base):
if exiftool_attributes is None: if exiftool_attributes is None:
return None return None
if(self.title_key not in exiftool_attributes): for title_key in self.title_keys:
if title_key in exiftool_attributes:
return exiftool_attributes[title_key]
return None return None
return exiftool_attributes[self.title_key]
def reset_cache(self): def reset_cache(self):
"""Resets any internal cache """Resets any internal cache
""" """
self.exiftool_attributes = None self.exiftool_attributes = None
self.exif_metadata = None self.exif_metadata = None
super(Media, self).reset_cache()
def set_album(self, name, path): def set_album(self, name, path):
"""Set album EXIF tag if not already set. """Set album EXIF tag if not already set.
@ -255,12 +466,16 @@ class Media(Base):
if self.get_album() is not None: if self.get_album() is not None:
return None return None
tags = {self.album_keys[0]: name} tags = {}
status = ExifTool().set_tags(tags, path) for key in self.album_keys:
tags[key] = name
status = self.__set_tags(tags, path)
self.reset_cache() self.reset_cache()
return status != ''
def set_date_original(self, time): return status
def set_date_original(self, time, path):
"""Set the date/time a photo was taken. """Set the date/time a photo was taken.
:param datetime time: datetime object of when the photo was taken :param datetime time: datetime object of when the photo was taken
@ -274,31 +489,39 @@ class Media(Base):
for key in self.date_original: for key in self.date_original:
tags[key] = formatted_time tags[key] = formatted_time
status = self.__set_tags(tags) status = self.__set_tags(tags, path)
if status == False: if status == False:
# exif attribute date_original d'ont exist # exif attribute date_original d'ont exist
for key in self.date_created: for key in self.date_created:
tags[key] = formatted_time tags[key] = formatted_time
status = self.__set_tags(tags) status = self.__set_tags(tags, path)
self.reset_cache() self.reset_cache()
return status return status
def set_location(self, latitude, longitude):
def set_location(self, latitude, longitude, path):
if(not self.is_valid()): if(not self.is_valid()):
return None return None
# The lat/lon _keys array has an order of precedence. # The lat/lon _keys array has an order of precedence.
# The first key is writable and we will give the writable # The first key is writable and we will give the writable
# key precence when reading. # key precence when reading.
tags = { # TODO check
self.latitude_keys[0]: latitude, # tags = {
self.longitude_keys[0]: longitude, # self.latitude_keys[0]: latitude,
} # self.longitude_keys[0]: longitude,
# }
tags = {}
for key in self.latitude_keys:
tags[key] = latitude
for key in self.longitude_keys:
tags[key] = longitude
# If self.set_gps_ref == True then it means we are writing an EXIF # If self.set_gps_ref == True then it means we are writing an EXIF
# GPS tag which requires us to set the reference key. # GPS tag which requires us to set the reference key.
# That's because the lat/lon are absolute values. # That's because the lat/lon are absolute values.
# TODO set_gps_ref = False for Video ?
if self.set_gps_ref: if self.set_gps_ref:
if latitude < 0: if latitude < 0:
tags[self.latitude_ref_key] = 'S' tags[self.latitude_ref_key] = 'S'
@ -306,12 +529,13 @@ class Media(Base):
if longitude < 0: if longitude < 0:
tags[self.longitude_ref_key] = 'W' tags[self.longitude_ref_key] = 'W'
status = self.__set_tags(tags) status = self.__set_tags(tags, path)
self.reset_cache() self.reset_cache()
return status return status
def set_original_name(self, path):
def set_original_name(self, path, name=None):
"""Sets the original name EXIF tag if not already set. """Sets the original name EXIF tag if not already set.
:returns: True, False, None :returns: True, False, None
@ -320,14 +544,17 @@ class Media(Base):
if self.get_original_name() is not None: if self.get_original_name() is not None:
return None return None
name = os.path.basename(path) if name == None:
name = os.path.basename(self.source)
tags = {self.original_name_key: name} tags = {self.original_name_key: name}
status = ExifTool().set_tags(tags, path) status = self.__set_tags(tags, path)
self.reset_cache() self.reset_cache()
return status != ''
def set_title(self, title): return status
def set_title(self, title, path):
"""Set title for a photo. """Set title for a photo.
:param str title: Title of the photo. :param str title: Title of the photo.
@ -339,23 +566,43 @@ class Media(Base):
if(title is None): if(title is None):
return None return None
tags = {self.title_key: title} tags = {}
status = self.__set_tags(tags) for key in self.title_keys:
tags[key] = title
status = self.__set_tags(tags, path)
self.reset_cache() self.reset_cache()
return status return status
def __set_tags(self, tags):
def __set_tags(self, tags, path):
if(not self.is_valid()): if(not self.is_valid()):
return None return None
source = self.source
status = '' status = ''
status = ExifTool().set_tags(tags,source) status = ExifTool().set_tags(tags, path)
if status.decode().find('unchanged') != -1 or status == '': if status.decode().find('unchanged') != -1 or status == '':
return False return False
if status.decode().find('error') != -1: if status.decode().find('error') != -1:
return False return False
return True return True
def get_all_subclasses(cls=None):
"""Module method to get all subclasses of Media.
"""
subclasses = set()
this_class = Media
if cls is not None:
this_class = cls
subclasses.add(this_class)
this_class_subclasses = this_class.__subclasses__()
for child_class in this_class_subclasses:
subclasses.update(get_all_subclasses(child_class))
return subclasses

View File

@ -25,7 +25,7 @@ class Photo(Media):
extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2') extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
def __init__(self, source=None): def __init__(self, source=None):
super(Photo, self).__init__(source) super().__init__(source)
# We only want to parse EXIF once so we store it here # We only want to parse EXIF once so we store it here
self.exif = None self.exif = None

View File

@ -28,31 +28,9 @@ class Video(Media):
extensions = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts') extensions = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts')
def __init__(self, source=None): def __init__(self, source=None):
super(Video, self).__init__(source) super().__init__(source)
self.date_original = [ # self.set_gps_ref = False
'EXIF:DateTimeOriginal',
'H264:DateTimeOriginal',
'QuickTime:ContentCreateDate' def is_valid(self):
] return super().is_valid()
self.date_created = [
'EXIF:CreateDate',
'QuickTime:CreationDate',
'QuickTime:CreateDate',
'QuickTime:CreationDate-und-US',
'QuickTime:MediaCreateDate'
]
self.date_modified = ['File:FileModifyDate']
self.title_key = 'XMP:DisplayName'
self.latitude_keys = [
'XMP:GPSLatitude',
# 'QuickTime:GPSLatitude',
'Composite:GPSLatitude'
]
self.longitude_keys = [
'XMP:GPSLongitude',
# 'QuickTime:GPSLongitude',
'Composite:GPSLongitude'
]
self.latitude_ref_key = 'EXIF:GPSLatitudeRef'
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
self.set_gps_ref = False

View File

@ -9,8 +9,7 @@ from elodie import log
from elodie.compatability import _decode from elodie.compatability import _decode
from elodie.filesystem import FileSystem from elodie.filesystem import FileSystem
from elodie.localstorage import Db from elodie.localstorage import Db
from elodie.media.base import Base, get_all_subclasses from elodie.media.media import Media, get_all_subclasses
from elodie.media.media import Media
from elodie.media.audio import Audio from elodie.media.audio import Audio
from elodie.media.photo import Photo from elodie.media.photo import Photo
from elodie.media.video import Video from elodie.media.video import Video

View File

@ -1,138 +0,0 @@
# Project imports
import os
import sys
import hashlib
import random
import re
import shutil
import string
import tempfile
import time
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))))
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import helper
from elodie.media.base import Base, get_all_subclasses
from elodie.media.media import Media
from elodie.media.audio import Audio
from elodie.media.photo import Photo
from elodie.media.video import Video
os.environ['TZ'] = 'GMT'
setup_module = helper.setup_module
teardown_module = helper.teardown_module
def test_get_all_subclasses():
subclasses = get_all_subclasses(Base)
expected = {Media, Base, Photo, Video, Audio}
assert subclasses == expected, subclasses
def test_get_class_by_file_without_extension():
base_file = helper.get_file('withoutextension')
cls = Base.get_class_by_file(base_file, [Audio, Photo, Video])
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():
temporary_folder, folder = helper.create_working_folder()
base_file = helper.get_file('invalid.jpg')
origin = '%s/invalid.jpg' % folder
shutil.copyfile(base_file, origin)
media = Media(origin)
status = media.set_album_from_folder()
assert status == False, status
def test_set_album_from_folder():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
photo = Photo(origin)
metadata = photo.get_metadata()
assert metadata['album'] is None, metadata['album']
new_album_name = os.path.split(folder)[1]
status = photo.set_album_from_folder()
assert status == True, status
photo_new = Photo(origin)
metadata_new = photo_new.get_metadata()
shutil.rmtree(folder)
assert metadata_new['album'] == new_album_name, metadata_new['album']
def test_set_metadata():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
photo = Photo(origin)
metadata = photo.get_metadata()
assert metadata['title'] == None, metadata['title']
new_title = 'Some Title'
photo.set_metadata(title = new_title)
new_metadata = photo.get_metadata()
assert new_metadata['title'] == new_title, new_metadata['title']
def test_set_metadata_basename():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
photo = Photo(origin)
metadata = photo.get_metadata()
assert metadata['base_name'] == 'photo', metadata['base_name']
new_basename = 'Some Base Name'
photo.set_metadata_basename(new_basename)
new_metadata = photo.get_metadata()
assert new_metadata['base_name'] == new_basename, new_metadata['base_name']

View File

@ -8,7 +8,6 @@ import tempfile
import time import time
from datetime import datetime from datetime import datetime
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))))
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import helper import helper
@ -100,16 +99,16 @@ def test_set_date_original():
origin = '%s/audio.m4a' % folder origin = '%s/audio.m4a' % folder
shutil.copyfile(helper.get_file('audio.m4a'), origin) shutil.copyfile(helper.get_file('audio.m4a'), origin)
media = Media(origin) audio = Audio(origin)
date = datetime(2013, 9, 30, 7, 6, 5) date = datetime(2013, 9, 30, 7, 6, 5)
status = media.set_date_original(date) status = audio.set_date_original(date)
assert status == True, status assert status == True, status
audio_new = Audio(origin) audio_new = Audio(origin)
metadata = audio_new.get_metadata() metadata = audio_new.get_metadata(update_cache=True)
date_original = metadata['date_created'] date_original = metadata['date_original']
shutil.rmtree(folder) shutil.rmtree(folder)
@ -134,7 +133,7 @@ def test_set_location():
assert status == True, status assert status == True, status
audio_new = Audio(origin) audio_new = Audio(origin)
metadata = audio_new.get_metadata() metadata = audio_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -160,7 +159,7 @@ def test_set_location_minus():
assert status == True, status assert status == True, status
audio_new = Audio(origin) audio_new = Audio(origin)
metadata = audio_new.get_metadata() metadata = audio_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -202,7 +201,7 @@ def test_set_title_non_ascii():
assert status == True, status assert status == True, status
audio_new = Audio(origin) audio_new = Audio(origin)
metadata = audio_new.get_metadata() metadata = audio_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)

View File

@ -9,13 +9,13 @@ import shutil
import string import string
import tempfile import tempfile
import time import time
import unittest
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))))
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import helper import helper
from elodie.media.media import Media, get_all_subclasses
from elodie.media.audio import Audio from elodie.media.audio import Audio
from elodie.media.media import Media
from elodie.media.photo import Photo from elodie.media.photo import Photo
from elodie.media.video import Video from elodie.media.video import Video
@ -24,6 +24,119 @@ os.environ['TZ'] = 'GMT'
setup_module = helper.setup_module setup_module = helper.setup_module
teardown_module = helper.teardown_module teardown_module = helper.teardown_module
def test_get_all_subclasses():
subclasses = get_all_subclasses(Media)
expected = {Media, Photo, Video, Audio}
assert subclasses == expected, subclasses
def test_get_class_by_file_without_extension():
base_file = helper.get_file('withoutextension')
cls = Media.get_class_by_file(base_file, [Audio, Photo, Video])
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():
temporary_folder, folder = helper.create_working_folder()
base_file = helper.get_file('invalid.jpg')
origin = '%s/invalid.jpg' % folder
shutil.copyfile(base_file, origin)
media = Media(origin)
status = media.set_album_from_folder(origin)
assert status == False, status
def test_set_album_from_folder():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
media = Media(origin)
metadata = media.get_metadata()
assert metadata['album'] is None, metadata['album']
new_album_name = os.path.split(folder)[1]
status = media.set_album_from_folder(origin)
assert status == True, status
media_new = Media(origin)
metadata_new = media_new.get_metadata(update_cache=True)
shutil.rmtree(folder)
assert metadata_new['album'] == new_album_name, metadata_new['album']
def test_set_metadata():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
media = Media(origin)
metadata = media.get_metadata()
assert metadata['title'] == None, metadata['title']
new_title = 'Some Title'
media.set_metadata(title = new_title)
new_metadata = media.get_metadata()
assert new_metadata['title'] == new_title, new_metadata['title']
def test_set_metadata_basename():
temporary_folder, folder = helper.create_working_folder()
origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)
media = Media(origin)
metadata = media.get_metadata()
assert metadata['base_name'] == 'photo', metadata['base_name']
new_basename = 'Some Base Name'
media.set_metadata_basename(new_basename)
new_metadata = media.get_metadata()
assert new_metadata['base_name'] == new_basename, new_metadata['base_name']
def test_get_file_path(): def test_get_file_path():
media = Media(helper.get_file('plain.jpg')) media = Media(helper.get_file('plain.jpg'))
path = media.get_file_path() path = media.get_file_path()
@ -63,32 +176,6 @@ 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(): def test_set_original_name_when_exists():
temporary_folder, folder = helper.create_working_folder() temporary_folder, folder = helper.create_working_folder()
@ -98,7 +185,7 @@ def test_set_original_name_when_exists():
shutil.copyfile(file, origin) shutil.copyfile(file, origin)
media = Media.get_class_by_file(origin, [Photo]) media = Media.get_class_by_file(origin, [Photo])
result = media.set_original_name() result = media.set_original_name(origin)
assert result is None, result assert result is None, result
@ -112,8 +199,8 @@ def test_set_original_name_when_does_not_exist():
media = Media.get_class_by_file(origin, [Photo]) media = Media.get_class_by_file(origin, [Photo])
metadata_before = media.get_metadata() metadata_before = media.get_metadata()
result = media.set_original_name() result = media.set_original_name(origin)
metadata_after = media.get_metadata() metadata_after = media.get_metadata(update_cache=True)
assert metadata_before['original_name'] is None, metadata_before assert metadata_before['original_name'] is None, metadata_before
assert metadata_after['original_name'] == 'plain.jpg', metadata_after assert metadata_after['original_name'] == 'plain.jpg', metadata_after
@ -132,7 +219,7 @@ def test_set_original_name_with_arg():
media = Media.get_class_by_file(origin, [Photo]) media = Media.get_class_by_file(origin, [Photo])
metadata_before = media.get_metadata() metadata_before = media.get_metadata()
result = media.set_original_name(new_name) result = media.set_original_name(new_name)
metadata_after = media.get_metadata() metadata_after = media.get_metadata(update_cache=True)
assert metadata_before['original_name'] is None, metadata_before assert metadata_before['original_name'] is None, metadata_before
assert metadata_after['original_name'] == new_name, metadata_after assert metadata_after['original_name'] == new_name, metadata_after
@ -154,10 +241,10 @@ def test_set_original_name():
shutil.copyfile(file_path, origin) shutil.copyfile(file_path, origin)
media = Media.get_class_by_file(origin, [Audio, Media, Photo, Video]) media = Media.get_class_by_file(origin, [Audio, Photo, Video])
metadata = media.get_metadata() metadata = media.get_metadata()
media.set_original_name() media.set_original_name(origin)
metadata_updated = media.get_metadata() metadata_updated = media.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)

View File

@ -10,7 +10,6 @@ import time
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))))
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import helper import helper
@ -122,8 +121,8 @@ def test_get_coordinates_with_null_coordinate():
assert longitude is None, longitude assert longitude is None, longitude
def test_get_date_original(): def test_get_date_original():
media = Media(helper.get_file('plain.jpg')) photo = Photo(helper.get_file('plain.jpg'))
date_original = media.get_date_attribute(['EXIF:DateTimeOriginal']) date_original = photo.get_date_attribute(['EXIF:DateTimeOriginal'])
#assert date_original == (2015, 12, 5, 0, 59, 26, 5, 339, 0), date_original #assert date_original == (2015, 12, 5, 0, 59, 26, 5, 339, 0), date_original
assert date_original == datetime(2015, 12, 5, 0, 59, 26), date_original assert date_original == datetime(2015, 12, 5, 0, 59, 26), date_original
@ -174,12 +173,12 @@ def test_set_album():
assert metadata['album'] is None, metadata['album'] assert metadata['album'] is None, metadata['album']
status = photo.set_album('Test Album') status = photo.set_album('Test Album', origin)
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata_new = photo_new.get_metadata() metadata_new = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -193,14 +192,14 @@ def test_set_date_original_with_missing_datetimeoriginal():
origin = '%s/photo.jpg' % folder origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('no-exif.jpg'), origin) shutil.copyfile(helper.get_file('no-exif.jpg'), origin)
media = Media(origin) photo = Photo(origin)
time = datetime(2013, 9, 30, 7, 6, 5) time = datetime(2013, 9, 30, 7, 6, 5)
status = media.set_date_original(time) status = photo.set_date_original(time)
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
date_original = metadata['date_original'] date_original = metadata['date_original']
@ -216,13 +215,13 @@ def test_set_date_original():
origin = '%s/photo.jpg' % folder origin = '%s/photo.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin) shutil.copyfile(helper.get_file('plain.jpg'), origin)
media = Media(origin) photo = Photo(origin)
status = media.set_date_original(datetime(2013, 9, 30, 7, 6, 5)) status = photo.set_date_original(datetime(2013, 9, 30, 7, 6, 5))
assert status == True, status assert status == True, status
media_new = Media(origin) photo_new = Photo(origin)
metadata = media_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
date_original = metadata['date_original'] date_original = metadata['date_original']
@ -250,7 +249,7 @@ def test_set_location():
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -276,7 +275,7 @@ def test_set_location_minus():
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -297,7 +296,7 @@ def test_set_title():
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -318,7 +317,7 @@ def test_set_title_non_ascii():
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)
@ -395,7 +394,7 @@ def _test_photo_type_set(type, date):
assert status == True, status assert status == True, status
photo_new = Photo(origin) photo_new = Photo(origin)
metadata = photo_new.get_metadata() metadata = photo_new.get_metadata(update_cache=True)
shutil.rmtree(folder) shutil.rmtree(folder)

View File

@ -9,7 +9,6 @@ import time
from datetime import datetime from datetime import datetime
from dateutil.parser import parse from dateutil.parser import parse
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))))
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import helper import helper
@ -102,7 +101,7 @@ def test_set_album():
assert metadata['album'] is None, metadata['album'] assert metadata['album'] is None, metadata['album']
status = video.set_album('Test Album') status = video.set_album('Test Album', origin)
assert status == True, status assert status == True, status