123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- 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
|