pygal/models.py

471 lines
17 KiB
Python

import datetime
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
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', ]
# Get a logger instance
logger = logging.getLogger("CACHING")
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):
if pygal.suspend_public() and not user.is_authenticated:
return True
def may_read(self, user):
if self.suspended(user):
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:
return False
if not user.is_authenticated:
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
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,))