471 lines
17 KiB
Python
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,))
|