|
@@ -6,8 +6,8 @@ from django.urls import reverse
|
6
|
6
|
import fstools
|
7
|
7
|
import json
|
8
|
8
|
import logging
|
|
9
|
+import media
|
9
|
10
|
import os
|
10
|
|
-from PIL import Image
|
11
|
11
|
import pygal
|
12
|
12
|
import subprocess
|
13
|
13
|
import time
|
|
@@ -76,33 +76,6 @@ def get_item_by_rel_path(rel_path):
|
76
|
76
|
return i
|
77
|
77
|
|
78
|
78
|
|
79
|
|
-def ffprobe_lines(full_path):
|
80
|
|
- def _ffprobe_command(full_path):
|
81
|
|
- return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
|
82
|
|
-
|
83
|
|
- def _avprobe_command(full_path):
|
84
|
|
- return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
|
85
|
|
-
|
86
|
|
- def decode(string):
|
87
|
|
- for i in ['utf-8', 'cp1252']:
|
88
|
|
- try:
|
89
|
|
- return string.decode(i)
|
90
|
|
- except UnicodeEncodeError:
|
91
|
|
- pass
|
92
|
|
- except UnicodeDecodeError:
|
93
|
|
- pass
|
94
|
|
- return string
|
95
|
|
-
|
96
|
|
- try:
|
97
|
|
- try:
|
98
|
|
- text_to_be_parsed = subprocess.check_output(_avprobe_command(full_path))
|
99
|
|
- except OSError:
|
100
|
|
- text_to_be_parsed = subprocess.check_output(_ffprobe_command(full_path))
|
101
|
|
- except subprocess.CalledProcessError:
|
102
|
|
- text_to_be_parsed = ''
|
103
|
|
- return decode(text_to_be_parsed).splitlines()
|
104
|
|
-
|
105
|
|
-
|
106
|
79
|
def is_valid_area(x1, y1, x2, y2):
|
107
|
80
|
for p in [x1, y1, x2, y2]:
|
108
|
81
|
if type(p) is not int:
|
|
@@ -125,8 +98,7 @@ class ItemData(dict):
|
125
|
98
|
'lat',
|
126
|
99
|
'height',
|
127
|
100
|
'iso',
|
128
|
|
- 'camera_vendor',
|
129
|
|
- 'camera_model',
|
|
101
|
+ 'camera',
|
130
|
102
|
'orientation',
|
131
|
103
|
'width',
|
132
|
104
|
'duration',
|
|
@@ -154,8 +126,7 @@ class ItemData(dict):
|
154
|
126
|
'f_number': 0,
|
155
|
127
|
'focal_length': 0,
|
156
|
128
|
'iso': 0,
|
157
|
|
- 'camera_vendor': '-',
|
158
|
|
- 'camera_model': '-',
|
|
129
|
+ 'camera': '-',
|
159
|
130
|
'orientation': 1,
|
160
|
131
|
'duration': 3,
|
161
|
132
|
'ratio': 1,
|
|
@@ -224,8 +195,7 @@ class Item(models.Model):
|
224
|
195
|
lat_c = models.FloatField(null=True, blank=True)
|
225
|
196
|
height_c = models.IntegerField(null=True, blank=True)
|
226
|
197
|
iso_c = models.IntegerField(null=True, blank=True)
|
227
|
|
- camera_vendor_c = models.CharField(max_length=100, null=True, blank=True)
|
228
|
|
- camera_model_c = models.CharField(max_length=100, null=True, blank=True)
|
|
198
|
+ camera_c = models.CharField(max_length=100, null=True, blank=True)
|
229
|
199
|
orientation_c = models.IntegerField(null=True, blank=True)
|
230
|
200
|
width_c = models.IntegerField(null=True, blank=True)
|
231
|
201
|
# video
|
|
@@ -247,6 +217,31 @@ class Item(models.Model):
|
247
|
217
|
num_videos_c = models.IntegerField(null=True, blank=True)
|
248
|
218
|
sil_c = models.TextField(null=True, blank=True)
|
249
|
219
|
|
|
220
|
+ MODEL_TO_MEDIA_DATA = {
|
|
221
|
+ media.metadata.KEY_SIZE: 'size_c',
|
|
222
|
+ media.metadata.KEY_TIME: 'datetime_c',
|
|
223
|
+ media.metadata.KEY_EXPOSURE_PROGRAM: 'exposure_program_c',
|
|
224
|
+ media.metadata.KEY_EXPOSURE_TIME: 'exposure_time_c',
|
|
225
|
+ media.metadata.KEY_FLASH: 'flash_c',
|
|
226
|
+ media.metadata.KEY_APERTURE: 'f_number_c',
|
|
227
|
+ media.metadata.KEY_FOCAL_LENGTH: 'focal_length_c',
|
|
228
|
+ media.metadata.KEY_GPS: {'lon': 'lon_c', 'lat': 'lat_c'},
|
|
229
|
+ media.metadata.KEY_HEIGHT: 'height_c',
|
|
230
|
+ media.metadata.KEY_ISO: 'iso_c',
|
|
231
|
+ media.metadata.KEY_CAMERA: 'camera_c',
|
|
232
|
+ media.metadata.KEY_ORIENTATION: 'orientation_c',
|
|
233
|
+ media.metadata.KEY_WIDTH: 'width_c',
|
|
234
|
+ media.metadata.KEY_DURATION: 'duration_c',
|
|
235
|
+ media.metadata.KEY_RATIO: 'ratio_c',
|
|
236
|
+ media.metadata.KEY_ALBUM: 'album_c',
|
|
237
|
+ media.metadata.KEY_ARTIST: 'artist_c',
|
|
238
|
+ media.metadata.KEY_BITRATE: 'bitrate_c',
|
|
239
|
+ media.metadata.KEY_GENRE: 'genre_c',
|
|
240
|
+ media.metadata.KEY_TITLE: 'title_c',
|
|
241
|
+ media.metadata.KEY_TRACK: 'track_c',
|
|
242
|
+ media.metadata.KEY_YEAR: 'year_c',
|
|
243
|
+ }
|
|
244
|
+
|
250
|
245
|
def __init__(self, *args, **kwargs):
|
251
|
246
|
self.__current_uid__ = None
|
252
|
247
|
self.__current_settings__ = None
|
|
@@ -378,53 +373,15 @@ class Item(models.Model):
|
378
|
373
|
self.uid_c = self.current_uid()
|
379
|
374
|
self.settings_c = self.current_settings()
|
380
|
375
|
self.data_version_c = self.DATA_VERSION_NUMBER
|
381
|
|
- if self.type == TYPE_AUDIO:
|
382
|
|
- self.__update_audio_file_data__(full_path)
|
383
|
|
- elif self.type == TYPE_FOLDER:
|
|
376
|
+ if self.type == TYPE_FOLDER:
|
384
|
377
|
self.__update_folder_file_data__(full_path)
|
385
|
|
- elif self.type == TYPE_IMAGE:
|
386
|
|
- self.__update_image_file_data__(full_path)
|
387
|
378
|
elif self.type == TYPE_OTHER:
|
388
|
379
|
self.__update_other_file_data__(full_path)
|
389
|
|
- elif self.type == TYPE_VIDEO:
|
390
|
|
- self.__update_video_file_data__(full_path)
|
|
380
|
+ else:
|
|
381
|
+ self.__update_media_file_data__(full_path)
|
391
|
382
|
for key, value in self.cached_item_data.items():
|
392
|
383
|
logger.debug(' - Adding %s=%s', key, repr(value))
|
393
|
384
|
|
394
|
|
- def __update_audio_file_data__(self, full_path):
|
395
|
|
- self.size_c = os.path.getsize(full_path)
|
396
|
|
- #
|
397
|
|
- tag_type_target_dict = {}
|
398
|
|
- tag_type_target_dict['album'] = (str, 'album')
|
399
|
|
- tag_type_target_dict['TAG:album'] = (str, 'album')
|
400
|
|
- tag_type_target_dict['TAG:artist'] = (str, 'artist')
|
401
|
|
- tag_type_target_dict['artist'] = (str, 'artist')
|
402
|
|
- tag_type_target_dict['bit_rate'] = (self.__int_conv__, 'bitrate')
|
403
|
|
- tag_type_target_dict['duration'] = (float, 'duration')
|
404
|
|
- tag_type_target_dict['TAG:genre'] = (str, 'genre')
|
405
|
|
- tag_type_target_dict['genre'] = (str, 'genre')
|
406
|
|
- tag_type_target_dict['TAG:title'] = (str, 'title')
|
407
|
|
- tag_type_target_dict['title'] = (str, 'title')
|
408
|
|
- tag_type_target_dict['TAG:track'] = (self.__int_conv__, 'track')
|
409
|
|
- tag_type_target_dict['track'] = (self.__int_conv__, 'track')
|
410
|
|
- tag_type_target_dict['TAG:date'] = (self.__int_conv__, 'year')
|
411
|
|
- tag_type_target_dict['date'] = (self.__int_conv__, 'year')
|
412
|
|
- for line in ffprobe_lines(full_path):
|
413
|
|
- try:
|
414
|
|
- key, val = [snippet.strip() for snippet in line.split('=')]
|
415
|
|
- except ValueError:
|
416
|
|
- continue
|
417
|
|
- else:
|
418
|
|
- if key in tag_type_target_dict:
|
419
|
|
- tp, name = tag_type_target_dict[key]
|
420
|
|
- try:
|
421
|
|
- setattr(self, name + '_c', tp(val))
|
422
|
|
- except ValueError:
|
423
|
|
- logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name)
|
424
|
|
- #
|
425
|
|
- if self.year_c is not None and self.track_c is not None:
|
426
|
|
- self.datetime_c = datetime.datetime(max(1971, self.year_c), 1, 1, 12, 0, (60 - self.track_c) % 60, tzinfo=timezone.utc)
|
427
|
|
-
|
428
|
385
|
def __update_folder_file_data__(self, full_path):
|
429
|
386
|
sil = []
|
430
|
387
|
self.size_c = 0
|
|
@@ -462,151 +419,26 @@ class Item(models.Model):
|
462
|
419
|
if len(sil) > 0:
|
463
|
420
|
self.datetime_c = sil[0].datetime_c
|
464
|
421
|
|
465
|
|
- def __update_image_file_data__(self, full_path):
|
466
|
|
- self.size_c = os.path.getsize(full_path)
|
467
|
|
- #
|
468
|
|
- tag_type_target_dict = {}
|
469
|
|
- tag_type_target_dict[0x9003] = (self.__datetime_conv__, 'datetime')
|
470
|
|
- tag_type_target_dict[0x8822] = (self.__exposure_program_conv__, 'exposure_program')
|
471
|
|
- tag_type_target_dict[0x829A] = (self.__num_denum_conv__, 'exposure_time')
|
472
|
|
- tag_type_target_dict[0x9209] = (self.__flash_conv__, 'flash')
|
473
|
|
- tag_type_target_dict[0x829D] = (self.__num_denum_conv__, 'f_number')
|
474
|
|
- tag_type_target_dict[0x920A] = (self.__num_denum_conv__, 'focal_length')
|
475
|
|
- tag_type_target_dict[0x8825] = (self.__gps_conv__, ('lon', 'lat'))
|
476
|
|
- tag_type_target_dict[0xA003] = (self.__int_conv__, 'height')
|
477
|
|
- tag_type_target_dict[0x8827] = (self.__int_conv__, 'iso')
|
478
|
|
- tag_type_target_dict[0x010F] = (str, 'camera_vendor')
|
479
|
|
- tag_type_target_dict[0x0110] = (str, 'camera_model')
|
480
|
|
- tag_type_target_dict[0x0112] = (self.__int_conv__, 'orientation')
|
481
|
|
- tag_type_target_dict[0xA002] = (self.__int_conv__, 'width')
|
482
|
|
- im = Image.open(full_path)
|
483
|
|
- try:
|
484
|
|
- exif = dict(im._getexif().items())
|
485
|
|
- except AttributeError:
|
486
|
|
- logger.debug('%s does not have any exif information', full_path)
|
487
|
|
- else:
|
488
|
|
- for key in tag_type_target_dict:
|
489
|
|
- if key in exif:
|
490
|
|
- tp, name = tag_type_target_dict[key]
|
491
|
|
- if type(name) is tuple:
|
492
|
|
- data = tp(exif[key]) or (None, None)
|
493
|
|
- for name, val in zip(name, data):
|
494
|
|
- setattr(self, name + '_c', val)
|
495
|
|
- else:
|
496
|
|
- setattr(self, name + '_c', tp(exif[key]))
|
|
422
|
+ def __update_media_file_data__(self, full_path):
|
|
423
|
+ data = media.get_media_data(full_path) or {}
|
|
424
|
+ for key in self.MODEL_TO_MEDIA_DATA:
|
|
425
|
+ value = data.get(key)
|
|
426
|
+ if key == media.metadata.KEY_GPS: # Split GPS data in lon and lat
|
|
427
|
+ if value is not None:
|
|
428
|
+ for k in self.MODEL_TO_MEDIA_DATA[key]:
|
|
429
|
+ setattr(self, self.MODEL_TO_MEDIA_DATA[key][k], value[k])
|
|
430
|
+ else:
|
|
431
|
+ if key == media.metadata.KEY_TIME: # convert time to datetime
|
|
432
|
+ if data.get(media.metadata.KEY_TIME_IS_SUBSTITUTION) and self.type == TYPE_IMAGE: # don't use time substitution for images
|
|
433
|
+ break
|
|
434
|
+ value = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
|
|
435
|
+ setattr(self, self.MODEL_TO_MEDIA_DATA[key], value)
|
497
|
436
|
|
498
|
437
|
def __update_other_file_data__(self, full_path):
|
499
|
438
|
self.size_c = os.path.getsize(full_path)
|
500
|
439
|
#
|
501
|
440
|
self.datetime_c = datetime.datetime.fromtimestamp(os.path.getctime(full_path), tz=timezone.utc)
|
502
|
441
|
|
503
|
|
- def __update_video_file_data__(self, full_path):
|
504
|
|
- self.size_c = os.path.getsize(full_path)
|
505
|
|
- #
|
506
|
|
- tag_type_target_dict = {}
|
507
|
|
- tag_type_target_dict['creation_time'] = (self.__vid_datetime_conv__, 'datetime')
|
508
|
|
- tag_type_target_dict['TAG:creation_time'] = (self.__vid_datetime_conv__, 'datetime')
|
509
|
|
- tag_type_target_dict['duration'] = (float, 'duration')
|
510
|
|
- tag_type_target_dict['height'] = (self.__int_conv__, 'height')
|
511
|
|
- tag_type_target_dict['width'] = (self.__int_conv__, 'width')
|
512
|
|
- tag_type_target_dict['display_aspect_ratio'] = (self.__ratio_conv__, 'ratio')
|
513
|
|
- for line in ffprobe_lines(full_path):
|
514
|
|
- try:
|
515
|
|
- key, val = [snippet.strip() for snippet in line.split('=')]
|
516
|
|
- except ValueError:
|
517
|
|
- continue
|
518
|
|
- else:
|
519
|
|
- if key in tag_type_target_dict:
|
520
|
|
- tp, name = tag_type_target_dict[key]
|
521
|
|
- try:
|
522
|
|
- setattr(self, name + '_c', tp(val))
|
523
|
|
- except ValueError:
|
524
|
|
- logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name)
|
525
|
|
-
|
526
|
|
- def __datetime_conv__(self, dt):
|
527
|
|
- format_string = "%Y:%m:%d %H:%M:%S%z"
|
528
|
|
- return datetime.datetime.strptime(dt + '+0000', format_string)
|
529
|
|
-
|
530
|
|
- def __exposure_program_conv__(self, n):
|
531
|
|
- return {
|
532
|
|
- 0: 'Unidentified',
|
533
|
|
- 1: 'Manual',
|
534
|
|
- 2: 'Program Normal',
|
535
|
|
- 3: 'Aperture Priority',
|
536
|
|
- 4: 'Shutter Priority',
|
537
|
|
- 5: 'Program Creative',
|
538
|
|
- 6: 'Program Action',
|
539
|
|
- 7: 'Portrait Mode',
|
540
|
|
- 8: 'Landscape Mode'
|
541
|
|
- }.get(n, '-')
|
542
|
|
-
|
543
|
|
- def __flash_conv__(self, n):
|
544
|
|
- return {
|
545
|
|
- 0: 'No',
|
546
|
|
- 1: 'Fired',
|
547
|
|
- 5: 'Fired (?)', # no return sensed
|
548
|
|
- 7: 'Fired (!)', # return sensed
|
549
|
|
- 9: 'Fill Fired',
|
550
|
|
- 13: 'Fill Fired (?)',
|
551
|
|
- 15: 'Fill Fired (!)',
|
552
|
|
- 16: 'Off',
|
553
|
|
- 24: 'Auto Off',
|
554
|
|
- 25: 'Auto Fired',
|
555
|
|
- 29: 'Auto Fired (?)',
|
556
|
|
- 31: 'Auto Fired (!)',
|
557
|
|
- 32: 'Not Available'
|
558
|
|
- }.get(n, '-')
|
559
|
|
-
|
560
|
|
- def __int_conv__(self, value):
|
561
|
|
- try:
|
562
|
|
- return int(value)
|
563
|
|
- except ValueError:
|
564
|
|
- for c in ['.', '/', '-']:
|
565
|
|
- p = value.find(c)
|
566
|
|
- if p >= 0:
|
567
|
|
- value = value[:p]
|
568
|
|
- if value == '':
|
569
|
|
- return 0
|
570
|
|
- return int(value)
|
571
|
|
-
|
572
|
|
- def __num_denum_conv__(self, data):
|
573
|
|
- num, denum = data
|
574
|
|
- return num / denum
|
575
|
|
-
|
576
|
|
- def __gps_conv__(self, data):
|
577
|
|
- def lat_lon_cal(lon_or_lat):
|
578
|
|
- lon_lat = 0.
|
579
|
|
- fac = 1.
|
580
|
|
- for num, denum in lon_or_lat:
|
581
|
|
- lon_lat += float(num) / float(denum) * fac
|
582
|
|
- fac *= 1. / 60.
|
583
|
|
- return lon_lat
|
584
|
|
- try:
|
585
|
|
- lon = lat_lon_cal(data[0x0004])
|
586
|
|
- lat = lat_lon_cal(data[0x0002])
|
587
|
|
- if lon != 0 or lat != 0: # do not use lon and lat equal 0, caused by motorola gps weakness
|
588
|
|
- return lon, lat
|
589
|
|
- except KeyError:
|
590
|
|
- logger.warning('GPS data extraction failed for %s: %s', self.name, repr(data))
|
591
|
|
- return None
|
592
|
|
-
|
593
|
|
- def __vid_datetime_conv__(self, dt):
|
594
|
|
- try:
|
595
|
|
- dt = dt[:dt.index('.')]
|
596
|
|
- except ValueError:
|
597
|
|
- pass # time string seems to have no '.'
|
598
|
|
- dt = dt.replace('T', ' ').replace('/', '').replace('\\', '')
|
599
|
|
- if len(dt) == 16:
|
600
|
|
- dt += ':00'
|
601
|
|
- dt += '+0000'
|
602
|
|
- format_string = '%Y-%m-%d %H:%M:%S%z'
|
603
|
|
- return datetime.datetime.strptime(dt, format_string)
|
604
|
|
-
|
605
|
|
- def __ratio_conv__(self, ratio):
|
606
|
|
- ratio = ratio.replace('\\', '')
|
607
|
|
- num, denum = ratio.split(':')
|
608
|
|
- return float(num) / float(denum)
|
609
|
|
-
|
610
|
442
|
def __str__(self):
|
611
|
443
|
return 'Item: %s' % self.rel_path
|
612
|
444
|
|