浏览代码

Usage of pylib media for getting metadata from media files

master
Dirk Alders 4 年前
父节点
当前提交
3061da63ad
共有 6 个文件被更改,包括 73 次插入223 次删除
  1. 2
    4
      admin.py
  2. 1
    2
      help.py
  3. 22
    0
      migrations/0003_auto_20200130_2203.py
  4. 46
    214
      models.py
  5. 1
    2
      search.py
  6. 1
    1
      views/infoviews.py

+ 2
- 4
admin.py 查看文件

@@ -22,8 +22,7 @@ class ItemAdmin(admin.ModelAdmin):
22 22
         'num_other_c',
23 23
         'num_videos_c',
24 24
         'sil_c',
25
-        'camera_vendor_c',
26
-        'camera_model_c',
25
+        'camera_c',
27 26
         'width_c',
28 27
         'height_c',
29 28
         'exposure_program_c',
@@ -75,8 +74,7 @@ class ItemAdmin(admin.ModelAdmin):
75 74
                 ]
76 75
             if obj.type == TYPE_IMAGE:
77 76
                 rv += [
78
-                    'camera_vendor_c',
79
-                    'camera_model_c',
77
+                    'camera_c',
80 78
                     'width_c',
81 79
                     'height_c',
82 80
                     'lon_c',

+ 1
- 2
help.py 查看文件

@@ -74,8 +74,7 @@ more in the different type depending lists.
74 74
 * lat (NUMERIC):
75 75
 * height (NUMERIC):
76 76
 * iso (NUMERIC):
77
-* camera_vendor (TEXT):
78
-* camera_model (TEXT):
77
+* camera (TEXT):
79 78
 * orientation (NUMERIC):
80 79
 * width (NUMERIC):
81 80
 

+ 22
- 0
migrations/0003_auto_20200130_2203.py 查看文件

@@ -0,0 +1,22 @@
1
+# Generated by Django 2.2.9 on 2020-01-30 22:03
2
+
3
+from django.db import migrations
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('pygal', '0002_setting_suspend_puplic'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.RenameField(
14
+            model_name='item',
15
+            old_name='camera_model_c',
16
+            new_name='camera_c',
17
+        ),
18
+        migrations.RemoveField(
19
+            model_name='item',
20
+            name='camera_vendor_c',
21
+        ),
22
+    ]

+ 46
- 214
models.py 查看文件

@@ -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
 

+ 1
- 2
search.py 查看文件

@@ -31,8 +31,7 @@ SCHEMA = Schema(
31 31
     lat=NUMERIC,
32 32
     height=NUMERIC,
33 33
     iso=NUMERIC,
34
-    camera_vendor=TEXT,
35
-    camera_model=TEXT,
34
+    camera=TEXT,
36 35
     orientation=NUMERIC,
37 36
     width=NUMERIC,
38 37
     # Audio Cache

+ 1
- 1
views/infoviews.py 查看文件

@@ -75,7 +75,7 @@ class image_view(base_view):
75 75
     @property
76 76
     def item_information(self):
77 77
         rv = []
78
-        rv.append({'description': _('Camera'), 'data': '%s - %s' % (self.item.item_data.camera_vendor, self.item.item_data.camera_model), 'url': None})
78
+        rv.append({'description': _('Camera'), 'data': '%s' % (self.item.item_data.camera), 'url': None})
79 79
         rv.append({'description': _('Resolution'), 'data': '%d x %d' % (self.item.item_data.width, self.item.item_data.height), 'url': None})
80 80
         gps_data = self.item.item_data.gps
81 81
         if gps_data is not None:

正在加载...
取消
保存