import datetime from django.conf import settings from django.contrib.auth.models import User from django.db import models from django.utils import formats, timezone from django.urls import reverse import fstools import json import logging import media import os import pygal import time logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__) DEBUG = False TYPE_FOLDER = 'folder' TYPE_IMAGE = 'image' TYPE_VIDEO = 'video' TYPE_AUDIO = 'audio' TYPE_OTHER = 'other' EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ] EXTENTIONS_AUDIO = ['.mp3', ] EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ] def get_item_type(full_path): if os.path.isdir(full_path): return TYPE_FOLDER else: if os.path.splitext(full_path)[1].lower() in EXTENTIONS_IMAGE: return TYPE_IMAGE elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_VIDEO: return TYPE_VIDEO elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_AUDIO: return TYPE_AUDIO return TYPE_OTHER def supported_types(): rv = [] if pygal.show_audio(): rv.append(TYPE_AUDIO) if pygal.show_image(): rv.append(TYPE_IMAGE) if pygal.show_other(): rv.append(TYPE_OTHER) if pygal.show_video(): rv.append(TYPE_VIDEO) return rv def get_item_by_rel_path(rel_path): try: rv = Item.objects.get(rel_path=rel_path) except Item.DoesNotExist: rv = None if rv is not None: # return the existing item return rv else: # create new item, if rel_path exists in filesystem (folders needs to hold files) full_path = pygal.get_full_path(rel_path) if os.path.exists(full_path): # file exists or folder has files in substructure if get_item_type(full_path) != TYPE_FOLDER or len(fstools.filelist(full_path)) > 0: i = Item(rel_path=rel_path) i.save() logger.info('New Item created: %s', repr(rel_path)) return i def is_valid_area(x1, y1, x2, y2): for p in [x1, y1, x2, y2]: if type(p) is not int: return False if (x1, y1) == (x2, y2): return False return True class ItemData(dict): DATA_FIELDS = [ 'size', 'datetime', 'exposure_program', 'exposure_time', 'flash', 'f_number', 'focal_length', 'lon', 'lat', 'height', 'iso', 'camera', 'orientation', 'width', 'duration', 'ratio', 'album', 'artist', 'bitrate', 'genre', 'title', 'track', 'year', 'sil', 'num_audio', 'num_folders', 'num_images', 'num_other', 'num_videos', ] DEFAULT_VALUES = { 'size': 0, 'datetime': datetime.datetime(1900, 1, 1), 'exposure_program': '-', 'exposure_time': 0, 'flash': '-', 'f_number': 0, 'focal_length': 0, 'iso': 0, 'camera': '-', 'orientation': 1, 'duration': 3, 'ratio': 1, 'album': '-', 'artist': '-', 'bitrate': 0, 'genre': '-', 'title': '-', 'track': 0, 'year': 0, 'num_audio': 0, 'num_folders': 0, 'num_images': 0, 'num_other': 0, 'num_videos': 0, } def __init__(self, item): for key in self.DATA_FIELDS: value = getattr(item, key + '_c') if value is not None: self[key] = value setattr(self, key, value) else: if key in self.DEFAULT_VALUES: setattr(self, key, self.DEFAULT_VALUES[key]) @property def formatted_datetime(self): try: return formats.date_format(self.get('datetime'), format="SHORT_DATE_FORMAT", use_l10n=True) + ' - ' + formats.time_format(self.get('datetime'), use_l10n=True) except AttributeError: return 'No Datetime available' @property def gps(self): if self.get('lon') and self.get('lat'): return {'lon': self.get('lon'), 'lat': self.get('lat')} else: return None class Item(models.Model): DATA_VERSION_NUMBER = 0 # rel_path = models.TextField(unique=True) type = models.CharField(max_length=25, choices=((TYPE_AUDIO, 'Audio'), (TYPE_FOLDER, 'Folder'), (TYPE_IMAGE, 'Image'), (TYPE_OTHER, 'Other'), (TYPE_VIDEO, 'Video'))) public_access = models.BooleanField(default=False) read_access = models.ManyToManyField(User, related_name="read_access", blank=True) modify_access = models.ManyToManyField(User, related_name="modify_access", blank=True) favourite_of = models.ManyToManyField(User, related_name="favourite_of", blank=True) # uid_c = models.CharField(max_length=50, null=True, blank=True) settings_c = models.IntegerField(null=True, blank=True) data_version_c = models.IntegerField(null=True, blank=True) # common size_c = models.IntegerField(null=True, blank=True) datetime_c = models.DateTimeField(null=True, blank=True) # image exposure_program_c = models.CharField(max_length=100, null=True, blank=True) exposure_time_c = models.FloatField(null=True, blank=True) flash_c = models.CharField(max_length=100, null=True, blank=True) f_number_c = models.FloatField(null=True, blank=True) focal_length_c = models.FloatField(null=True, blank=True) lon_c = models.FloatField(null=True, blank=True) lat_c = models.FloatField(null=True, blank=True) height_c = models.IntegerField(null=True, blank=True) iso_c = models.IntegerField(null=True, blank=True) camera_c = models.CharField(max_length=100, null=True, blank=True) orientation_c = models.IntegerField(null=True, blank=True) width_c = models.IntegerField(null=True, blank=True) # video duration_c = models.FloatField(null=True, blank=True) ratio_c = models.FloatField(null=True, blank=True) # audio album_c = models.CharField(max_length=100, null=True, blank=True) artist_c = models.CharField(max_length=100, null=True, blank=True) bitrate_c = models.IntegerField(null=True, blank=True) genre_c = models.CharField(max_length=100, null=True, blank=True) title_c = models.CharField(max_length=100, null=True, blank=True) track_c = models.IntegerField(null=True, blank=True) year_c = models.IntegerField(null=True, blank=True) # folder num_audio_c = models.IntegerField(null=True, blank=True) num_folders_c = models.IntegerField(null=True, blank=True) num_images_c = models.IntegerField(null=True, blank=True) num_other_c = models.IntegerField(null=True, blank=True) num_videos_c = models.IntegerField(null=True, blank=True) sil_c = models.TextField(null=True, blank=True) MODEL_TO_MEDIA_DATA = { media.KEY_SIZE: 'size_c', media.KEY_TIME: 'datetime_c', media.KEY_EXPOSURE_PROGRAM: 'exposure_program_c', media.KEY_EXPOSURE_TIME: 'exposure_time_c', media.KEY_FLASH: 'flash_c', media.KEY_APERTURE: 'f_number_c', media.KEY_FOCAL_LENGTH: 'focal_length_c', media.KEY_GPS: {'lon': 'lon_c', 'lat': 'lat_c'}, media.KEY_HEIGHT: 'height_c', media.KEY_ISO: 'iso_c', media.KEY_CAMERA: 'camera_c', media.KEY_ORIENTATION: 'orientation_c', media.KEY_WIDTH: 'width_c', media.KEY_DURATION: 'duration_c', media.KEY_RATIO: 'ratio_c', media.KEY_ALBUM: 'album_c', media.KEY_ARTIST: 'artist_c', media.KEY_BITRATE: 'bitrate_c', media.KEY_GENRE: 'genre_c', media.KEY_TITLE: 'title_c', media.KEY_TRACK: 'track_c', media.KEY_YEAR: 'year_c', } def __init__(self, *args, **kwargs): self.__current_uid__ = None self.__current_settings__ = None models.Model.__init__(self, *args, **kwargs) @property def name(self): return os.path.splitext(os.path.basename(self.rel_path))[0] @property def item_data(self): if self.__cache_update_needed__(): # self.__set_model_fields_from_file__() # self.save() return self.cached_item_data @property def cached_item_data(self): return ItemData(self) def get_admin_url(self): """the url to the Django admin interface for the model instance""" info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) def suspended(self, user): return pygal.suspend_public() and not user.is_authenticated def may_read(self, user): if self.suspended(user): logger.info("Permiision denied to '%s' due to suspended not authenticated user.", self.name) return False elif self.type == TYPE_FOLDER: return True else: parent = get_item_by_rel_path(os.path.dirname(self.rel_path)) if parent.public_access is True: return True if user is None: logger.info("Permiision denied to %s due to user is None.", self.name) return False if not user.is_authenticated: logger.info("Permiision denied to %s due to user is not authenticated.", self.name) return False if user.is_superuser: return True return user in parent.read_access.all() def may_modify(self, user): if self.suspended(user): return False elif self.type == TYPE_FOLDER: return user in self.modify_access.all() else: if user is None: return False if not user.is_authenticated: return False if user.is_superuser: return True parent = get_item_by_rel_path(os.path.dirname(self.rel_path)) return user in parent.modify_access.all() def sort_string(self): if pygal.sort_by_date(): try: tm = int(self.item_data.datetime.strftime('%s')) except AttributeError: raise AttributeError('Unable to create a sortstring for %s. Used datetime was: %s' % (str(self), repr(self.item_data.datetime))) return '%012d_%s' % (tm, os.path.basename(self.rel_path)) else: return self.rel_path def sorted_itemlist(self): if self.type == TYPE_FOLDER: rv = [] for rel_path in json.loads(self.item_data['sil']): try: rv.append(Item.objects.get(rel_path=rel_path)) except Item.DoesNotExist: raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), rel_path)) return rv else: return None def thumbnail_item(self): if self.type == TYPE_FOLDER: try: first_rel_path = json.loads(self.item_data['sil'])[0] return Item.objects.get(rel_path=first_rel_path).thumbnail_item() except IndexError: raise Item.DoesNotExist("%s: Tried to get the thumbnail_item for an empty list." % str(self)) except Item.DoesNotExist: raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), first_rel_path)) else: return self def all_itemslist(self): rv = [] for i in self.sorted_itemlist(): if i.type != TYPE_FOLDER: rv.append(i) else: rv.extend(i.all_itemslist()) return rv def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self.__cache_update_needed__(): self.__set_model_fields_from_file__() return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) def current_uid(self): if self.__current_uid__ is None: self.__current_uid__ = fstools.uid(pygal.get_full_path(self.rel_path), None) return self.__current_uid__ def current_settings(self): if self.__current_settings__ is None: self.__current_settings__ = pygal.show_audio() * 1 + pygal.show_image() * 2 + pygal.show_other() * 4 + pygal.show_video() * 8 + pygal.sort_by_date() * 16 return self.__current_settings__ def __cache_update_needed__(self): if self.type == TYPE_FOLDER: return DEBUG or self.settings_c != self.current_settings() or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER else: return DEBUG or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER def __set_model_fields_from_file__(self): logger.info('Updating cached data for Item: %s' % repr(self.rel_path)) full_path = pygal.get_full_path(self.rel_path) self.type = get_item_type(full_path) self.uid_c = self.current_uid() self.settings_c = self.current_settings() self.data_version_c = self.DATA_VERSION_NUMBER if self.type == TYPE_FOLDER: self.__update_folder_file_data__(full_path) elif self.type == TYPE_OTHER: self.__update_other_file_data__(full_path) else: self.__update_media_file_data__(full_path) for key, value in self.cached_item_data.items(): logger.debug(' - Adding %s=%s', key, repr(value)) def __update_folder_file_data__(self, full_path): sil = [] self.size_c = 0 self.num_audio_c = 0 self.num_folders_c = 1 self.num_images_c = 0 self.num_other_c = 0 self.num_videos_c = 0 for fn in os.listdir(full_path): sub_rel_path = pygal.get_rel_path(os.path.join(full_path, fn)) sub_item = get_item_by_rel_path(sub_rel_path) # size, num_* if sub_item is not None: # Item does really exist / has sub-items if (sub_item.type == TYPE_FOLDER and len(json.loads(sub_item.item_data['sil']))) or sub_item.type in supported_types(): self.size_c += sub_item.item_data.size if sub_item.type == TYPE_AUDIO: self.num_audio_c += 1 elif sub_item.type == TYPE_FOLDER: self.num_audio_c += sub_item.item_data['num_audio'] self.num_folders_c += sub_item.item_data['num_folders'] self.num_images_c += sub_item.item_data['num_images'] self.num_other_c += sub_item.item_data['num_other'] self.num_videos_c += sub_item.item_data['num_videos'] elif sub_item.type == TYPE_IMAGE: self.num_images_c += 1 elif sub_item.type == TYPE_OTHER: self.num_other_c += 1 elif sub_item.type == TYPE_VIDEO: self.num_videos_c += 1 # sorted item list sil.append(sub_item) sil.sort(key=lambda entry: entry.sort_string(), reverse=pygal.sort_by_date()) self.sil_c = json.dumps([i.rel_path for i in sil], indent=4) # datetime if len(sil) > 0: self.datetime_c = sil[0].datetime_c def __update_media_file_data__(self, full_path): data = media.get_media_data(full_path) or {} for key in self.MODEL_TO_MEDIA_DATA: value = data.get(key) if key == media.KEY_GPS: # Split GPS data in lon and lat for k in self.MODEL_TO_MEDIA_DATA[key]: value_k = value[k] if value is not None else None setattr(self, self.MODEL_TO_MEDIA_DATA[key][k], value_k) else: if value is not None: if key == media.KEY_TIME: # convert time to datetime if data.get(media.KEY_TIME_IS_SUBSTITUTION) and self.type == TYPE_IMAGE: # don't use time substitution for images value = None else: value = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc) setattr(self, self.MODEL_TO_MEDIA_DATA[key], value) def __update_other_file_data__(self, full_path): self.size_c = os.path.getsize(full_path) # self.datetime_c = datetime.datetime.fromtimestamp(os.path.getctime(full_path), tz=timezone.utc) def __str__(self): return 'Item: %s' % self.rel_path def TagExist(item, data): return len(Tag.objects.filter(item=item, text=data['text'])) > 0 class Tag(models.Model): item = models.ForeignKey(Item, on_delete=models.CASCADE) text = models.CharField(max_length=100) topleft_x = models.IntegerField(null=True, blank=True) topleft_y = models.IntegerField(null=True, blank=True) bottomright_x = models.IntegerField(null=True, blank=True) bottomright_y = models.IntegerField(null=True, blank=True) def __init__(self, *args, **kwargs): self.__tm_start__ = time.time() models.Model.__init__(self, *args, **kwargs) logger.log(5, 'Initialising Tag Model object in %.02fs: %s', time.time() - self.__tm_start__, str(self)) @property def has_valid_coordinates(self): return is_valid_area(self.topleft_x, self.topleft_y, self.bottomright_x, self.bottomright_y) def get_admin_url(self): """the url to the Django admin interface for the model instance""" info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) def export_key(self): return self.item.rel_path def export_data(self): rv = {} rv['text'] = self.text if self.has_valid_coordinates: rv['topleft_x'] = self.topleft_x rv['topleft_y'] = self.topleft_y rv['bottomright_x'] = self.bottomright_x rv['bottomright_y'] = self.bottomright_y return rv