pygal/models.py

492 lines
18 KiB
Python
Raw Normal View History

2020-01-26 23:38:32 +01:00
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
2020-01-26 23:38:32 +01:00
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
2021-01-21 19:34:00 +01:00
try:
from config import APP_NAME as ROOT_LOGGER_NAME
except ImportError:
ROOT_LOGGER_NAME = 'root'
logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
2020-01-26 23:38:32 +01:00
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',
2020-01-26 23:38:32 +01:00
'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': '-',
2020-01-26 23:38:32 +01:00
'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)
2020-01-26 23:38:32 +01:00
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',
}
2020-01-26 23:38:32 +01:00
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
2020-01-26 23:38:32 +01:00
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
2020-01-26 23:38:32 +01:00
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:
2020-01-26 23:38:32 +01:00
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)
2020-01-26 23:38:32 +01:00
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())
2020-01-26 23:38:32 +01:00
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)
2020-01-26 23:38:32 +01:00
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
2020-09-01 12:37:25 +02:00
def TagExist(item, data):
return len(Tag.objects.filter(item=item, text=data['text'])) > 0
2020-01-26 23:38:32 +01:00
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,))
2020-09-01 12:37:25 +02:00
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