Django Library PyGal
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

models.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import datetime
  2. from django.conf import settings
  3. from django.contrib.auth.models import User
  4. from django.db import models
  5. from django.utils import formats, timezone
  6. from django.urls import reverse
  7. import fstools
  8. import json
  9. import logging
  10. import media
  11. import os
  12. import pygal
  13. import time
  14. logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
  15. DEBUG = False
  16. TYPE_FOLDER = 'folder'
  17. TYPE_IMAGE = 'image'
  18. TYPE_VIDEO = 'video'
  19. TYPE_AUDIO = 'audio'
  20. TYPE_OTHER = 'other'
  21. EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ]
  22. EXTENTIONS_AUDIO = ['.mp3', ]
  23. EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ]
  24. def get_item_type(full_path):
  25. if os.path.isdir(full_path):
  26. return TYPE_FOLDER
  27. else:
  28. if os.path.splitext(full_path)[1].lower() in EXTENTIONS_IMAGE:
  29. return TYPE_IMAGE
  30. elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_VIDEO:
  31. return TYPE_VIDEO
  32. elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_AUDIO:
  33. return TYPE_AUDIO
  34. return TYPE_OTHER
  35. def supported_types():
  36. rv = []
  37. if pygal.show_audio():
  38. rv.append(TYPE_AUDIO)
  39. if pygal.show_image():
  40. rv.append(TYPE_IMAGE)
  41. if pygal.show_other():
  42. rv.append(TYPE_OTHER)
  43. if pygal.show_video():
  44. rv.append(TYPE_VIDEO)
  45. return rv
  46. def get_item_by_rel_path(rel_path):
  47. try:
  48. rv = Item.objects.get(rel_path=rel_path)
  49. except Item.DoesNotExist:
  50. rv = None
  51. if rv is not None:
  52. # return the existing item
  53. return rv
  54. else:
  55. # create new item, if rel_path exists in filesystem (folders needs to hold files)
  56. full_path = pygal.get_full_path(rel_path)
  57. if os.path.exists(full_path):
  58. # file exists or folder has files in substructure
  59. if get_item_type(full_path) != TYPE_FOLDER or len(fstools.filelist(full_path)) > 0:
  60. i = Item(rel_path=rel_path)
  61. i.save()
  62. logger.info('New Item created: %s', repr(rel_path))
  63. return i
  64. def is_valid_area(x1, y1, x2, y2):
  65. for p in [x1, y1, x2, y2]:
  66. if type(p) is not int:
  67. return False
  68. if (x1, y1) == (x2, y2):
  69. return False
  70. return True
  71. class ItemData(dict):
  72. DATA_FIELDS = [
  73. 'size',
  74. 'datetime',
  75. 'exposure_program',
  76. 'exposure_time',
  77. 'flash',
  78. 'f_number',
  79. 'focal_length',
  80. 'lon',
  81. 'lat',
  82. 'height',
  83. 'iso',
  84. 'camera',
  85. 'orientation',
  86. 'width',
  87. 'duration',
  88. 'ratio',
  89. 'album',
  90. 'artist',
  91. 'bitrate',
  92. 'genre',
  93. 'title',
  94. 'track',
  95. 'year',
  96. 'sil',
  97. 'num_audio',
  98. 'num_folders',
  99. 'num_images',
  100. 'num_other',
  101. 'num_videos',
  102. ]
  103. DEFAULT_VALUES = {
  104. 'size': 0,
  105. 'datetime': datetime.datetime(1900, 1, 1),
  106. 'exposure_program': '-',
  107. 'exposure_time': 0,
  108. 'flash': '-',
  109. 'f_number': 0,
  110. 'focal_length': 0,
  111. 'iso': 0,
  112. 'camera': '-',
  113. 'orientation': 1,
  114. 'duration': 3,
  115. 'ratio': 1,
  116. 'album': '-',
  117. 'artist': '-',
  118. 'bitrate': 0,
  119. 'genre': '-',
  120. 'title': '-',
  121. 'track': 0,
  122. 'year': 0,
  123. 'num_audio': 0,
  124. 'num_folders': 0,
  125. 'num_images': 0,
  126. 'num_other': 0,
  127. 'num_videos': 0,
  128. }
  129. def __init__(self, item):
  130. for key in self.DATA_FIELDS:
  131. value = getattr(item, key + '_c')
  132. if value is not None:
  133. self[key] = value
  134. setattr(self, key, value)
  135. else:
  136. if key in self.DEFAULT_VALUES:
  137. setattr(self, key, self.DEFAULT_VALUES[key])
  138. @property
  139. def formatted_datetime(self):
  140. try:
  141. return formats.date_format(self.get('datetime'), format="SHORT_DATE_FORMAT", use_l10n=True) + ' - ' + formats.time_format(self.get('datetime'), use_l10n=True)
  142. except AttributeError:
  143. return 'No Datetime available'
  144. @property
  145. def gps(self):
  146. if self.get('lon') and self.get('lat'):
  147. return {'lon': self.get('lon'), 'lat': self.get('lat')}
  148. else:
  149. return None
  150. class Item(models.Model):
  151. DATA_VERSION_NUMBER = 0
  152. #
  153. rel_path = models.TextField(unique=True)
  154. type = models.CharField(max_length=25, choices=((TYPE_AUDIO, 'Audio'), (TYPE_FOLDER, 'Folder'), (TYPE_IMAGE, 'Image'), (TYPE_OTHER, 'Other'), (TYPE_VIDEO, 'Video')))
  155. public_access = models.BooleanField(default=False)
  156. read_access = models.ManyToManyField(User, related_name="read_access", blank=True)
  157. modify_access = models.ManyToManyField(User, related_name="modify_access", blank=True)
  158. favourite_of = models.ManyToManyField(User, related_name="favourite_of", blank=True)
  159. #
  160. uid_c = models.CharField(max_length=50, null=True, blank=True)
  161. settings_c = models.IntegerField(null=True, blank=True)
  162. data_version_c = models.IntegerField(null=True, blank=True)
  163. # common
  164. size_c = models.IntegerField(null=True, blank=True)
  165. datetime_c = models.DateTimeField(null=True, blank=True)
  166. # image
  167. exposure_program_c = models.CharField(max_length=100, null=True, blank=True)
  168. exposure_time_c = models.FloatField(null=True, blank=True)
  169. flash_c = models.CharField(max_length=100, null=True, blank=True)
  170. f_number_c = models.FloatField(null=True, blank=True)
  171. focal_length_c = models.FloatField(null=True, blank=True)
  172. lon_c = models.FloatField(null=True, blank=True)
  173. lat_c = models.FloatField(null=True, blank=True)
  174. height_c = models.IntegerField(null=True, blank=True)
  175. iso_c = models.IntegerField(null=True, blank=True)
  176. camera_c = models.CharField(max_length=100, null=True, blank=True)
  177. orientation_c = models.IntegerField(null=True, blank=True)
  178. width_c = models.IntegerField(null=True, blank=True)
  179. # video
  180. duration_c = models.FloatField(null=True, blank=True)
  181. ratio_c = models.FloatField(null=True, blank=True)
  182. # audio
  183. album_c = models.CharField(max_length=100, null=True, blank=True)
  184. artist_c = models.CharField(max_length=100, null=True, blank=True)
  185. bitrate_c = models.IntegerField(null=True, blank=True)
  186. genre_c = models.CharField(max_length=100, null=True, blank=True)
  187. title_c = models.CharField(max_length=100, null=True, blank=True)
  188. track_c = models.IntegerField(null=True, blank=True)
  189. year_c = models.IntegerField(null=True, blank=True)
  190. # folder
  191. num_audio_c = models.IntegerField(null=True, blank=True)
  192. num_folders_c = models.IntegerField(null=True, blank=True)
  193. num_images_c = models.IntegerField(null=True, blank=True)
  194. num_other_c = models.IntegerField(null=True, blank=True)
  195. num_videos_c = models.IntegerField(null=True, blank=True)
  196. sil_c = models.TextField(null=True, blank=True)
  197. MODEL_TO_MEDIA_DATA = {
  198. media.KEY_SIZE: 'size_c',
  199. media.KEY_TIME: 'datetime_c',
  200. media.KEY_EXPOSURE_PROGRAM: 'exposure_program_c',
  201. media.KEY_EXPOSURE_TIME: 'exposure_time_c',
  202. media.KEY_FLASH: 'flash_c',
  203. media.KEY_APERTURE: 'f_number_c',
  204. media.KEY_FOCAL_LENGTH: 'focal_length_c',
  205. media.KEY_GPS: {'lon': 'lon_c', 'lat': 'lat_c'},
  206. media.KEY_HEIGHT: 'height_c',
  207. media.KEY_ISO: 'iso_c',
  208. media.KEY_CAMERA: 'camera_c',
  209. media.KEY_ORIENTATION: 'orientation_c',
  210. media.KEY_WIDTH: 'width_c',
  211. media.KEY_DURATION: 'duration_c',
  212. media.KEY_RATIO: 'ratio_c',
  213. media.KEY_ALBUM: 'album_c',
  214. media.KEY_ARTIST: 'artist_c',
  215. media.KEY_BITRATE: 'bitrate_c',
  216. media.KEY_GENRE: 'genre_c',
  217. media.KEY_TITLE: 'title_c',
  218. media.KEY_TRACK: 'track_c',
  219. media.KEY_YEAR: 'year_c',
  220. }
  221. def __init__(self, *args, **kwargs):
  222. self.__current_uid__ = None
  223. self.__current_settings__ = None
  224. models.Model.__init__(self, *args, **kwargs)
  225. @property
  226. def name(self):
  227. return os.path.splitext(os.path.basename(self.rel_path))[0]
  228. @property
  229. def item_data(self):
  230. if self.__cache_update_needed__():
  231. #
  232. self.__set_model_fields_from_file__()
  233. #
  234. self.save()
  235. return self.cached_item_data
  236. @property
  237. def cached_item_data(self):
  238. return ItemData(self)
  239. def get_admin_url(self):
  240. """the url to the Django admin interface for the model instance"""
  241. info = (self._meta.app_label, self._meta.model_name)
  242. return reverse('admin:%s_%s_change' % info, args=(self.pk,))
  243. def suspended(self, user):
  244. return pygal.suspend_public() and not user.is_authenticated
  245. def may_read(self, user):
  246. if self.suspended(user):
  247. logger.info("Permiision denied to '%s' due to suspended not authenticated user.", self.name)
  248. return False
  249. elif self.type == TYPE_FOLDER:
  250. return True
  251. else:
  252. parent = get_item_by_rel_path(os.path.dirname(self.rel_path))
  253. if parent.public_access is True:
  254. return True
  255. if user is None:
  256. logger.info("Permiision denied to %s due to user is None.", self.name)
  257. return False
  258. if not user.is_authenticated:
  259. logger.info("Permiision denied to %s due to user is not authenticated.", self.name)
  260. return False
  261. if user.is_superuser:
  262. return True
  263. return user in parent.read_access.all()
  264. def may_modify(self, user):
  265. if self.suspended(user):
  266. return False
  267. elif self.type == TYPE_FOLDER:
  268. return user in self.modify_access.all()
  269. else:
  270. if user is None:
  271. return False
  272. if not user.is_authenticated:
  273. return False
  274. if user.is_superuser:
  275. return True
  276. parent = get_item_by_rel_path(os.path.dirname(self.rel_path))
  277. return user in parent.modify_access.all()
  278. def sort_string(self):
  279. if pygal.sort_by_date():
  280. try:
  281. tm = int(self.item_data.datetime.strftime('%s'))
  282. except AttributeError:
  283. raise AttributeError('Unable to create a sortstring for %s. Used datetime was: %s' % (str(self), repr(self.item_data.datetime)))
  284. return '%012d_%s' % (tm, os.path.basename(self.rel_path))
  285. else:
  286. return self.rel_path
  287. def sorted_itemlist(self):
  288. if self.type == TYPE_FOLDER:
  289. rv = []
  290. for rel_path in json.loads(self.item_data['sil']):
  291. try:
  292. rv.append(Item.objects.get(rel_path=rel_path))
  293. except Item.DoesNotExist:
  294. raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), rel_path))
  295. return rv
  296. else:
  297. return None
  298. def thumbnail_item(self):
  299. if self.type == TYPE_FOLDER:
  300. try:
  301. first_rel_path = json.loads(self.item_data['sil'])[0]
  302. return Item.objects.get(rel_path=first_rel_path).thumbnail_item()
  303. except IndexError:
  304. raise Item.DoesNotExist("%s: Tried to get the thumbnail_item for an empty list." % str(self))
  305. except Item.DoesNotExist:
  306. raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), first_rel_path))
  307. else:
  308. return self
  309. def all_itemslist(self):
  310. rv = []
  311. for i in self.sorted_itemlist():
  312. if i.type != TYPE_FOLDER:
  313. rv.append(i)
  314. else:
  315. rv.extend(i.all_itemslist())
  316. return rv
  317. def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
  318. if self.__cache_update_needed__():
  319. self.__set_model_fields_from_file__()
  320. return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
  321. def current_uid(self):
  322. if self.__current_uid__ is None:
  323. self.__current_uid__ = fstools.uid(pygal.get_full_path(self.rel_path), None)
  324. return self.__current_uid__
  325. def current_settings(self):
  326. if self.__current_settings__ is None:
  327. self.__current_settings__ = pygal.show_audio() * 1 + pygal.show_image() * 2 + pygal.show_other() * 4 + pygal.show_video() * 8 + pygal.sort_by_date() * 16
  328. return self.__current_settings__
  329. def __cache_update_needed__(self):
  330. if self.type == TYPE_FOLDER:
  331. 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
  332. else:
  333. return DEBUG or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER
  334. def __set_model_fields_from_file__(self):
  335. logger.info('Updating cached data for Item: %s' % repr(self.rel_path))
  336. full_path = pygal.get_full_path(self.rel_path)
  337. self.type = get_item_type(full_path)
  338. self.uid_c = self.current_uid()
  339. self.settings_c = self.current_settings()
  340. self.data_version_c = self.DATA_VERSION_NUMBER
  341. if self.type == TYPE_FOLDER:
  342. self.__update_folder_file_data__(full_path)
  343. elif self.type == TYPE_OTHER:
  344. self.__update_other_file_data__(full_path)
  345. else:
  346. self.__update_media_file_data__(full_path)
  347. for key, value in self.cached_item_data.items():
  348. logger.debug(' - Adding %s=%s', key, repr(value))
  349. def __update_folder_file_data__(self, full_path):
  350. sil = []
  351. self.size_c = 0
  352. self.num_audio_c = 0
  353. self.num_folders_c = 1
  354. self.num_images_c = 0
  355. self.num_other_c = 0
  356. self.num_videos_c = 0
  357. for fn in os.listdir(full_path):
  358. sub_rel_path = pygal.get_rel_path(os.path.join(full_path, fn))
  359. sub_item = get_item_by_rel_path(sub_rel_path)
  360. # size, num_*
  361. if sub_item is not None: # Item does really exist / has sub-items
  362. if (sub_item.type == TYPE_FOLDER and len(json.loads(sub_item.item_data['sil']))) or sub_item.type in supported_types():
  363. self.size_c += sub_item.item_data.size
  364. if sub_item.type == TYPE_AUDIO:
  365. self.num_audio_c += 1
  366. elif sub_item.type == TYPE_FOLDER:
  367. self.num_audio_c += sub_item.item_data['num_audio']
  368. self.num_folders_c += sub_item.item_data['num_folders']
  369. self.num_images_c += sub_item.item_data['num_images']
  370. self.num_other_c += sub_item.item_data['num_other']
  371. self.num_videos_c += sub_item.item_data['num_videos']
  372. elif sub_item.type == TYPE_IMAGE:
  373. self.num_images_c += 1
  374. elif sub_item.type == TYPE_OTHER:
  375. self.num_other_c += 1
  376. elif sub_item.type == TYPE_VIDEO:
  377. self.num_videos_c += 1
  378. # sorted item list
  379. sil.append(sub_item)
  380. sil.sort(key=lambda entry: entry.sort_string(), reverse=pygal.sort_by_date())
  381. self.sil_c = json.dumps([i.rel_path for i in sil], indent=4)
  382. # datetime
  383. if len(sil) > 0:
  384. self.datetime_c = sil[0].datetime_c
  385. def __update_media_file_data__(self, full_path):
  386. data = media.get_media_data(full_path) or {}
  387. for key in self.MODEL_TO_MEDIA_DATA:
  388. value = data.get(key)
  389. if key == media.KEY_GPS: # Split GPS data in lon and lat
  390. for k in self.MODEL_TO_MEDIA_DATA[key]:
  391. value_k = value[k] if value is not None else None
  392. setattr(self, self.MODEL_TO_MEDIA_DATA[key][k], value_k)
  393. else:
  394. if value is not None:
  395. if key == media.KEY_TIME: # convert time to datetime
  396. if data.get(media.KEY_TIME_IS_SUBSTITUTION) and self.type == TYPE_IMAGE: # don't use time substitution for images
  397. value = None
  398. else:
  399. value = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
  400. setattr(self, self.MODEL_TO_MEDIA_DATA[key], value)
  401. def __update_other_file_data__(self, full_path):
  402. self.size_c = os.path.getsize(full_path)
  403. #
  404. self.datetime_c = datetime.datetime.fromtimestamp(os.path.getctime(full_path), tz=timezone.utc)
  405. def __str__(self):
  406. return 'Item: %s' % self.rel_path
  407. def TagExist(item, data):
  408. return len(Tag.objects.filter(item=item, text=data['text'])) > 0
  409. class Tag(models.Model):
  410. item = models.ForeignKey(Item, on_delete=models.CASCADE)
  411. text = models.CharField(max_length=100)
  412. topleft_x = models.IntegerField(null=True, blank=True)
  413. topleft_y = models.IntegerField(null=True, blank=True)
  414. bottomright_x = models.IntegerField(null=True, blank=True)
  415. bottomright_y = models.IntegerField(null=True, blank=True)
  416. def __init__(self, *args, **kwargs):
  417. self.__tm_start__ = time.time()
  418. models.Model.__init__(self, *args, **kwargs)
  419. logger.log(5, 'Initialising Tag Model object in %.02fs: %s', time.time() - self.__tm_start__, str(self))
  420. @property
  421. def has_valid_coordinates(self):
  422. return is_valid_area(self.topleft_x, self.topleft_y, self.bottomright_x, self.bottomright_y)
  423. def get_admin_url(self):
  424. """the url to the Django admin interface for the model instance"""
  425. info = (self._meta.app_label, self._meta.model_name)
  426. return reverse('admin:%s_%s_change' % info, args=(self.pk,))
  427. def export_key(self):
  428. return self.item.rel_path
  429. def export_data(self):
  430. rv = {}
  431. rv['text'] = self.text
  432. if self.has_valid_coordinates:
  433. rv['topleft_x'] = self.topleft_x
  434. rv['topleft_y'] = self.topleft_y
  435. rv['bottomright_x'] = self.bottomright_x
  436. rv['bottomright_y'] = self.bottomright_y
  437. return rv