Browse Source

Added som features for audio handling (cddb, rip, encode)

master
Dirk Alders 2 months ago
parent
commit
402e837551
7 changed files with 603 additions and 244 deletions
  1. 138
    0
      CDDB.py
  2. 41
    192
      __init__.py
  3. 36
    0
      common.py
  4. 101
    1
      convert.py
  5. 150
    0
      image.py
  6. 135
    51
      metadata.py
  7. 2
    0
      requirements.txt

+ 138
- 0
CDDB.py View File

@@ -0,0 +1,138 @@
1
+import urllib
2
+import socket
3
+import os
4
+import urllib.parse
5
+import urllib.request
6
+import subprocess
7
+import logging
8
+from .common import KEY_ALBUM, KEY_ARTIST, KEY_GENRE, KEY_TITLE, KEY_TRACK, KEY_YEAR
9
+
10
+try:
11
+    from config import APP_NAME as ROOT_LOGGER_NAME
12
+except ImportError:
13
+    ROOT_LOGGER_NAME = 'root'
14
+logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
15
+
16
+
17
+version = 2.0
18
+if 'EMAIL' in os.environ:
19
+    (default_user, hostname) = os.environ['EMAIL'].split('@')
20
+else:
21
+    default_user = os.environ['USER'] or os.geteuid() or 'user'
22
+    hostname = socket.gethostname() or 'host'
23
+
24
+proto = 6
25
+default_server = 'http://gnudb.gnudb.org/~cddb/cddb.cgi'
26
+
27
+
28
+def my_disc_metadata(**kwargs):
29
+    """Generate my disc metadata
30
+
31
+    kwargs needs to include the following data:
32
+        * KEY_ARTIST (str)
33
+        * KEY_ALBUM (str)
34
+        * KEY_YEAR (int) - will be converted here
35
+        * KEY_GENRE (str)
36
+        * "track_xx" (str) - where xx is the track number which will be converted to int here
37
+    """
38
+    main_dict = {}
39
+    for key in [KEY_ARTIST, KEY_ALBUM, KEY_YEAR, KEY_GENRE]:
40
+        try:
41
+            value = kwargs.pop(key)
42
+        except KeyError:
43
+            logger.error("Information is missing in kwargs - key=%s", key)
44
+            return None
45
+        if key in [KEY_YEAR]:
46
+            try:
47
+                main_dict[key] = int(value)
48
+            except ValueError:
49
+                logger.error("Can't convert %s (key=%s) to integer value", value, key)
50
+                return None
51
+        else:
52
+            main_dict[key] = value
53
+    rv = dict(main_dict)
54
+    rv["tracks"] = []
55
+    for key in list(kwargs):
56
+        value = kwargs.pop(key)
57
+        if key.startswith("track_"):
58
+            track = dict(main_dict)
59
+            try:
60
+                track[KEY_TRACK] = int(key[6:])
61
+            except ValueError:
62
+                logger.warning("Useless information kwargs - kwargs[%s] = %s", key, repr(value))
63
+            track[KEY_TITLE] = value
64
+            rv["tracks"].append(track)
65
+        else:
66
+            logger.warning("Useless information kwargs - key=%s", key)
67
+    return rv
68
+
69
+
70
+def query(data_str, server_url=default_server, user=default_user, host=hostname, client_name=ROOT_LOGGER_NAME, client_version=version):
71
+    url = f"{server_url}?cmd=cddb+query+{data_str}&hello={user}+{host}+{client_name}+{client_version}&proto={proto}"
72
+    response = urllib.request.urlopen(url)
73
+    header = response.readline().decode("utf-8").rstrip().split(" ", 3)
74
+    header[0] = int(header[0])
75
+
76
+    if header[0] not in (210, ):
77
+        logger.error("Error while querying cddb entry: \"%d - %s\"", header[0], header[3])
78
+        return None
79
+
80
+    rv = {}
81
+    for line in response.readlines():
82
+        line = line.decode("utf-8").rstrip()
83
+        if line == '.':                # end of matches
84
+            break
85
+        dummy, did, txt = line.split(" ", 2)
86
+        rv[did] = txt
87
+    return rv
88
+
89
+
90
+def cddb(disc_id, server_url=default_server, user=default_user, host=hostname, client_name=ROOT_LOGGER_NAME, client_version=version):
91
+    KEY_TRANSLATOR = {
92
+        "DGENRE": KEY_GENRE,
93
+        "DYEAR": KEY_YEAR
94
+    }
95
+    #
96
+    url = f"{server_url}?cmd=cddb+read+data+{disc_id}&hello={default_server}+{hostname}+{client_name}+{client_version}&proto={proto}"
97
+    response = urllib.request.urlopen(url)
98
+
99
+    header = response.readline().decode("utf-8").rstrip().split(" ", 3)
100
+    header[0] = int(header[0])
101
+
102
+    if header[0] not in (210, ):
103
+        logger.error("Error while reading cddb entry: \"%d - %s\"", header[1], header[3])
104
+        return None
105
+    data = {}
106
+    for line in response.readlines():
107
+        line = line.decode("utf-8").rstrip()
108
+        if line == '.':                # end of matches
109
+            break
110
+        if not line.startswith("#"):
111
+            match = line.split('=', 2)
112
+            key = KEY_TRANSLATOR.get(match[0])
113
+            value = match[1].strip()
114
+            if key:
115
+                if key == KEY_YEAR:
116
+                    value = int(value)
117
+                data[key] = value
118
+            elif match[0] == "DTITLE":
119
+                art_tit = value.split("/", 2)
120
+                data[KEY_ARTIST] = art_tit[0].strip()
121
+                data[KEY_ALBUM] = art_tit[1].strip()
122
+            elif match[0].startswith("TTITLE"):
123
+                data["track_%02d" % (int(match[0][6:]) + 1)] = value
124
+            else:
125
+                logger.debug("cddb line ignored: \"%s\"", line)
126
+    return my_disc_metadata(**data)
127
+
128
+
129
+def discid():
130
+    discid_cmd = subprocess.getoutput("which cd-discid")
131
+    if not discid_cmd:
132
+        logger.error("cd-discid is required for encoding. You need to install it to your system.")
133
+        return None
134
+    else:
135
+        try:
136
+            return subprocess.check_output(discid_cmd).decode("utf-8").strip().replace(" ", "+")
137
+        except subprocess.CalledProcessError as e:
138
+            return None

+ 41
- 192
__init__.py View File

@@ -22,203 +22,52 @@ media (Media Tools)
22 22
 
23 23
     See also the :download:`unittest <../../media/_testresults_/unittest.pdf>` documentation.
24 24
 """
25
+from .common import CALLBACK_CDDB_CHOICE
26
+from .common import CALLBACK_MAN_INPUT
27
+from .common import get_disc_device
28
+from .common import KEY_ALBUM
29
+from .common import KEY_APERTURE
30
+from .common import KEY_ARTIST
31
+from .common import KEY_BITRATE
32
+from .common import KEY_CAMERA
33
+from .common import KEY_DURATION
34
+from .common import KEY_EXPOSURE_PROGRAM
35
+from .common import KEY_EXPOSURE_TIME
36
+from .common import KEY_FLASH
37
+from .common import KEY_FOCAL_LENGTH
38
+from .common import KEY_GENRE
39
+from .common import KEY_GPS
40
+from .common import KEY_HEIGHT
41
+from .common import KEY_ISO
42
+from .common import KEY_ORIENTATION
43
+from .common import KEY_RATIO
44
+from .common import KEY_SIZE
45
+from .common import KEY_TIME
46
+from .common import KEY_TIME_IS_SUBSTITUTION
47
+from .common import KEY_TITLE
48
+from .common import KEY_TRACK
49
+from .common import KEY_WIDTH
50
+from .common import KEY_YEAR
51
+from .convert import disc_track_rip
52
+from .convert import wav_to_mp3
53
+from .convert import track_to_targetpath
54
+from .image import image
55
+from .image import ORIENTATION_HALF_ROTATED
56
+from .image import ORIENTATION_HORIZONTAL_MIRRORED
57
+from .image import ORIENTATION_LEFT_ROTATED
58
+from .image import ORIENTATION_NORMAL
59
+from .image import ORIENTATION_RIGHT_ROTATED
60
+from .image import ORIENTATION_VERTICAL_MIRRORED
61
+from .image import JOIN_BOT_LEFT, JOIN_BOT_RIGHT
62
+from .image import JOIN_CENTER
63
+from .image import JOIN_TOP_LEFT
64
+from .image import JOIN_TOP_RIGHT
65
+from .metadata import get_media_data
25 66
 __DEPENDENCIES__ = []
26 67
 
27
-import io
28
-import logging
29
-from PIL import Image, ImageEnhance, ExifTags
30
-
31
-try:
32
-    from config import APP_NAME as ROOT_LOGGER_NAME
33
-except ImportError:
34
-    ROOT_LOGGER_NAME = 'root'
35
-logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
36
-
37 68
 
38 69
 __DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with media files, like tags (e.g. exif, id3) and transformations.
39 70
 For more Information read the documentation.""" % __name__.replace('_', '\_')
40 71
 """The Module Description"""
41 72
 __INTERPRETER__ = (3, )
42 73
 """The Tested Interpreter-Versions"""
43
-
44
-
45
-KEY_ALBUM = 'album'
46
-KEY_APERTURE = 'aperture'
47
-KEY_ARTIST = 'artist'
48
-KEY_BITRATE = 'bitrate'
49
-KEY_CAMERA = 'camera'
50
-KEY_DURATION = 'duration'
51
-KEY_EXPOSURE_PROGRAM = 'exposure_program'
52
-KEY_EXPOSURE_TIME = 'exposure_time'
53
-KEY_FLASH = 'flash'
54
-KEY_FOCAL_LENGTH = 'focal_length'
55
-KEY_GENRE = 'genre'
56
-KEY_GPS = 'gps'
57
-KEY_HEIGHT = 'height'
58
-KEY_ISO = 'iso'
59
-KEY_ORIENTATION = 'orientation'
60
-KEY_RATIO = 'ratio'
61
-KEY_SIZE = 'size'
62
-KEY_TIME = 'time'   # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp
63
-KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst'
64
-KEY_TITLE = 'title'
65
-KEY_TRACK = 'track'
66
-KEY_WIDTH = 'width'
67
-KEY_YEAR = 'year'
68
-
69
-
70
-def get_media_data(full_path):
71
-    from media.metadata import get_audio_data, get_image_data, get_video_data
72
-    from media.common import get_filetype, FILETYPE_AUDIO, FILETYPE_IMAGE, FILETYPE_VIDEO
73
-    #
74
-    ft = get_filetype(full_path)
75
-    #
76
-    if ft == FILETYPE_AUDIO:
77
-        return get_audio_data(full_path)
78
-    elif ft == FILETYPE_IMAGE:
79
-        return get_image_data(full_path)
80
-    elif ft == FILETYPE_VIDEO:
81
-        return get_video_data(full_path)
82
-    else:
83
-        logger.warning('Filetype not known: %s', full_path)
84
-
85
-
86
-ORIENTATION_NORMAL = 1
87
-ORIENTATION_VERTICAL_MIRRORED = 2
88
-ORIENTATION_HALF_ROTATED = 3
89
-ORIENTATION_HORIZONTAL_MIRRORED = 4
90
-ORIENTATION_LEFT_ROTATED = 6
91
-ORIENTATION_RIGHT_ROTATED = 8
92
-
93
-JOIN_TOP_LEFT = 1
94
-JOIN_TOP_RIGHT = 2
95
-JOIN_BOT_LEFT = 3
96
-JOIN_BOT_RIGHT = 4
97
-JOIN_CENTER = 5
98
-
99
-
100
-class image(object):
101
-    def __init__(self, media_instance=None):
102
-        if media_instance is not None:
103
-            self.load_from_file(media_instance)
104
-        else:
105
-            self._im = None
106
-
107
-    def load_from_file(self, media_instance):
108
-        from media.convert import get_pil_image
109
-        #
110
-        self._im = get_pil_image(media_instance)
111
-        if self._im is None:
112
-            return False
113
-        try:
114
-            self._exif = dict(self._im._getexif().items())
115
-        except AttributeError:
116
-            self._exif = {}
117
-        if type(self._im) is not Image.Image:
118
-            self._im = self._im.copy()
119
-        logger.debug('loading image from %s', repr(media_instance))
120
-        return True
121
-
122
-    def save(self, full_path):
123
-        if self._im is None:
124
-            logger.warning('No image available to be saved (%s)', repr(full_path))
125
-            return False
126
-        else:
127
-            logger.debug('Saving image to %s', repr(full_path))
128
-            with open(full_path, 'w') as fh:
129
-                im = self._im.convert('RGB')
130
-                im.save(fh, 'JPEG')
131
-        return True
132
-
133
-    def image_data(self):
134
-        im = self._im.copy().convert('RGB')
135
-        output = io.BytesIO()
136
-        im.save(output, format='JPEG')
137
-        return output.getvalue()
138
-
139
-    def resize(self, max_size):
140
-        if self._im is None:
141
-            logger.warning('No image available to be resized')
142
-            return False
143
-        else:
144
-            logger.debug('Resizing picture to max %d pixel in whatever direction', max_size)
145
-            x, y = self._im.size
146
-            xy_max = max(x, y)
147
-            self._im = self._im.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0)
148
-        return True
149
-
150
-    def rotate_by_orientation(self, orientation=None):
151
-        if self._im is None:
152
-            logger.warning('No image available, rotation not possible')
153
-            return False
154
-
155
-        if orientation is None:
156
-            exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items())
157
-            try:
158
-                orientation = self._exif[exif_tags['Orientation']]
159
-                logger.debug("No orientation given, orientation %s extract from exif data", repr(orientation))
160
-            except KeyError:
161
-                return False
162
-
163
-        if orientation == ORIENTATION_HALF_ROTATED:
164
-            angle = 180
165
-        elif orientation == ORIENTATION_LEFT_ROTATED:
166
-            angle = 270
167
-        elif orientation == ORIENTATION_RIGHT_ROTATED:
168
-            angle = 90
169
-        else:
170
-            if type(orientation) == int and orientation > 8:
171
-                logger.warning('Orientation %s unknown for rotation', repr(orientation))
172
-            return False
173
-        logger.debug('Rotating picture by %d (deg)', angle)
174
-        self._im = self._im.rotate(angle, expand=True)
175
-        return True
176
-
177
-    def join(self, join_image, join_pos=JOIN_TOP_RIGHT, opacity=0.7):
178
-        from media.convert import get_pil_image
179
-
180
-        def rgba_copy(im):
181
-            if im.mode != 'RGBA':
182
-                return im.convert('RGBA')
183
-            else:
184
-                return im.copy()
185
-
186
-        if self._im is None:
187
-            logger.warning('No image available, joining not possible')
188
-            return False
189
-
190
-        # ensure type of join_image is PIL.Image
191
-        join_image = get_pil_image(join_image)
192
-        if join_image is None:
193
-            logger.warning('Image to be joined is not supported %s', repr(join_image))
194
-            return False
195
-
196
-        im2 = rgba_copy(join_image)
197
-        # change opacity of im2
198
-        alpha = im2.split()[3]
199
-        alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
200
-        im2.putalpha(alpha)
201
-
202
-        self._im = rgba_copy(self._im)
203
-
204
-        # create a transparent layer
205
-        layer = Image.new('RGBA', self._im.size, (0, 0, 0, 0))
206
-        # draw im2 in layer
207
-        if join_pos == JOIN_TOP_LEFT:
208
-            layer.paste(im2, (0, 0))
209
-        elif join_pos == JOIN_TOP_RIGHT:
210
-            layer.paste(im2, ((self._im.size[0] - im2.size[0]), 0))
211
-        elif join_pos == JOIN_BOT_LEFT:
212
-            layer.paste(im2, (0, (self._im.size[1] - im2.size[1])))
213
-        elif join_pos == JOIN_BOT_RIGHT:
214
-            layer.paste(im2, ((self._im.size[0] - im2.size[0]), (self._im.size[1] - im2.size[1])))
215
-        elif join_pos == JOIN_CENTER:
216
-            layer.paste(im2, (int((self._im.size[0] - im2.size[0]) / 2), int((self._im.size[1] - im2.size[1]) / 2)))
217
-        else:
218
-            logger.warning("Join position value %s is not supported", join_pos)
219
-            return False
220
-
221
-        logger.debug('Joining two images')
222
-        self._im = Image.composite(layer, self._im, layer)
223
-
224
-        return True

+ 36
- 0
common.py View File

@@ -1,12 +1,42 @@
1 1
 import os
2
+import discid
3
+
4
+KEY_ALBUM = 'album'
5
+KEY_APERTURE = 'aperture'
6
+KEY_ARTIST = 'artist'
7
+KEY_BITRATE = 'bitrate'
8
+KEY_CAMERA = 'camera'
9
+KEY_DURATION = 'duration'
10
+KEY_EXPOSURE_PROGRAM = 'exposure_program'
11
+KEY_EXPOSURE_TIME = 'exposure_time'
12
+KEY_FLASH = 'flash'
13
+KEY_FOCAL_LENGTH = 'focal_length'
14
+KEY_GENRE = 'genre'
15
+KEY_GPS = 'gps'
16
+KEY_HEIGHT = 'height'
17
+KEY_ISO = 'iso'
18
+KEY_ORIENTATION = 'orientation'
19
+KEY_RATIO = 'ratio'
20
+KEY_SIZE = 'size'
21
+KEY_TIME = 'time'   # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp
22
+KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst'
23
+KEY_TITLE = 'title'
24
+KEY_TRACK = 'track'
25
+KEY_WIDTH = 'width'
26
+KEY_YEAR = 'year'
2 27
 
3 28
 FILETYPE_AUDIO = 'audio'
4 29
 FILETYPE_IMAGE = 'image'
5 30
 FILETYPE_VIDEO = 'video'
31
+FILETYPE_DISC = 'disc'
32
+
33
+CALLBACK_CDDB_CHOICE = 0
34
+CALLBACK_MAN_INPUT = 1
6 35
 
7 36
 EXTENTIONS_AUDIO = ['.mp3', ]
8 37
 EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ]
9 38
 EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ]
39
+PREFIX_DISC = '/dev/'
10 40
 
11 41
 
12 42
 def get_filetype(full_path):
@@ -17,3 +47,9 @@ def get_filetype(full_path):
17 47
         return FILETYPE_IMAGE
18 48
     elif ext in EXTENTIONS_VIDEO:
19 49
         return FILETYPE_VIDEO
50
+    elif full_path.startswith(PREFIX_DISC):
51
+        return FILETYPE_DISC
52
+
53
+
54
+def get_disc_device():
55
+    return discid.get_default_device()

+ 101
- 1
convert.py View File

@@ -1,8 +1,17 @@
1 1
 import io
2
-from media import common, logger
2
+from media import common
3 3
 from PIL import Image
4 4
 import subprocess
5 5
 import platform
6
+import logging
7
+import os
8
+import subprocess
9
+
10
+try:
11
+    from config import APP_NAME as ROOT_LOGGER_NAME
12
+except ImportError:
13
+    ROOT_LOGGER_NAME = 'root'
14
+logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
6 15
 
7 16
 
8 17
 def get_pil_image(media_instance):
@@ -33,3 +42,94 @@ def get_pil_image(media_instance):
33 42
         return media_instance.copy()
34 43
     else:
35 44
         logger.warning('Instance type is not supported: %s' % type(media_instance))
45
+
46
+
47
+def FilenameFilter(filename: str) -> str:
48
+    # WHITELIST = [os.path.sep, os.path.extsep]
49
+    WHITELIST = [chr(x) for x in range(ord('0'), ord('9') + 1)]
50
+    WHITELIST += [chr(x) for x in range(ord('a'), ord('z') + 1)]
51
+    WHITELIST += ["ä", "ö", "ü", "ß"]
52
+    #
53
+    rv = ""
54
+    for c in filename.lower():
55
+        rv += c if c in WHITELIST else '_'
56
+    return rv
57
+
58
+
59
+def track_to_targetpath(basepath: str, track: dict, ext: str):
60
+    return os.path.join(
61
+        basepath,
62
+        FilenameFilter(track[common.KEY_ARTIST]),
63
+        "%04d_" % track[common.KEY_YEAR] + FilenameFilter(track[common.KEY_ALBUM]),
64
+        "%02d_" % track[common.KEY_TRACK] + FilenameFilter(track[common.KEY_TITLE]) + "." + ext
65
+    )
66
+
67
+
68
+def disc_track_rip(track_num: int, target_file: str, progress_callback):
69
+    FAC_SEC_VAL = 1224
70
+    #
71
+    cdp_cmd = subprocess.getoutput("which cdparanoia")
72
+    if not cdp_cmd:
73
+        logger.error("cdparanoia is required for ripping. You need to install it to your system.")
74
+    else:
75
+        cmd = [cdp_cmd, "-e", "-X", "%d" % track_num, target_file]
76
+        cdp = subprocess.Popen(cmd, text=True, stderr=subprocess.PIPE)
77
+        #
78
+        rval = 0
79
+        min_sec = None
80
+        max_sec = None
81
+        min_read = None
82
+        while (out := cdp.stderr.readline()) != "":
83
+            out = out.strip()
84
+            # identify minimum sector
85
+            if ("Ripping from sector" in out):
86
+                min_sec = int(list(filter(None, out.split(" ")))[3])
87
+            # identify maximum sector
88
+            if ("to sector" in out):
89
+                max_sec = int(list(filter(None, out.split(" ")))[2])
90
+            # identify progress
91
+            if "[read]" in out:
92
+                val = int(out.split(" ")[-1])
93
+                if not min_read:
94
+                    min_read = val
95
+                rval = max(val, rval)
96
+                try:
97
+                    dsec = max_sec - min_sec
98
+                except TypeError:
99
+                    logger.exception("Error while parsing cdparanoia. Start and End sector could not be detrmined.")
100
+                else:
101
+                    p = (rval - min_read) / FAC_SEC_VAL / dsec
102
+                    p = min(p, 1)
103
+                    progress_callback(p)
104
+        progress_callback(1)
105
+        return cdp.wait()
106
+
107
+
108
+def wav_to_mp3(infile: str, basepath: str, track_information, progress_callback, bitrate=256, vbr=0, quaulity=0):
109
+    lame_parameter = {
110
+        common.KEY_ARTIST: '--ta',
111
+        common.KEY_ALBUM: '--tl',
112
+        common.KEY_YEAR: '--ty',
113
+        common.KEY_GENRE: '--tg',
114
+        common.KEY_TRACK: '--tn',
115
+        common.KEY_TITLE: '--tt'
116
+    }
117
+    lame_cmd = subprocess.getoutput("which lame")
118
+    if not lame_cmd:
119
+        logger.error("lame is required for encoding. You need to install it to your system.")
120
+    else:
121
+        outfile = track_to_targetpath(basepath, track_information, 'mp3')
122
+        cmd = [lame_cmd, "-b", str(bitrate), "-V", str(vbr), "--vbr-old", "-q", str(quaulity), infile, outfile]
123
+        cmd.extend(["--tc", "Encoded by lame"])
124
+        for key in track_information:
125
+            cmd.extend([lame_parameter[key], str(track_information[key])])
126
+        lame = subprocess.Popen(cmd, text=True, stderr=subprocess.PIPE)
127
+        while (out := lame.stderr.readline()) != "":
128
+            out = out.strip()
129
+            posb = out.find("(")
130
+            posp = out.find("%")
131
+            if posb >= 0 and posp >= 0:
132
+                p = int(out[posb+1:posp]) / 100
133
+                progress_callback(p)
134
+        progress_callback(1)
135
+        return lame.wait()

+ 150
- 0
image.py View File

@@ -0,0 +1,150 @@
1
+import io
2
+import logging
3
+from PIL import Image, ImageEnhance, ExifTags
4
+
5
+try:
6
+    from config import APP_NAME as ROOT_LOGGER_NAME
7
+except ImportError:
8
+    ROOT_LOGGER_NAME = 'root'
9
+logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
10
+
11
+
12
+ORIENTATION_NORMAL = 1
13
+ORIENTATION_VERTICAL_MIRRORED = 2
14
+ORIENTATION_HALF_ROTATED = 3
15
+ORIENTATION_HORIZONTAL_MIRRORED = 4
16
+ORIENTATION_LEFT_ROTATED = 6
17
+ORIENTATION_RIGHT_ROTATED = 8
18
+
19
+JOIN_TOP_LEFT = 1
20
+JOIN_TOP_RIGHT = 2
21
+JOIN_BOT_LEFT = 3
22
+JOIN_BOT_RIGHT = 4
23
+JOIN_CENTER = 5
24
+
25
+
26
+class image(object):
27
+    def __init__(self, media_instance=None):
28
+        if media_instance is not None:
29
+            self.load_from_file(media_instance)
30
+        else:
31
+            self._im = None
32
+
33
+    def load_from_file(self, media_instance):
34
+        from media.convert import get_pil_image
35
+        #
36
+        self._im = get_pil_image(media_instance)
37
+        if self._im is None:
38
+            return False
39
+        try:
40
+            self._exif = dict(self._im._getexif().items())
41
+        except AttributeError:
42
+            self._exif = {}
43
+        if type(self._im) is not Image.Image:
44
+            self._im = self._im.copy()
45
+        logger.debug('loading image from %s', repr(media_instance))
46
+        return True
47
+
48
+    def save(self, full_path):
49
+        if self._im is None:
50
+            logger.warning('No image available to be saved (%s)', repr(full_path))
51
+            return False
52
+        else:
53
+            logger.debug('Saving image to %s', repr(full_path))
54
+            with open(full_path, 'w') as fh:
55
+                im = self._im.convert('RGB')
56
+                im.save(fh, 'JPEG')
57
+        return True
58
+
59
+    def image_data(self):
60
+        im = self._im.copy().convert('RGB')
61
+        output = io.BytesIO()
62
+        im.save(output, format='JPEG')
63
+        return output.getvalue()
64
+
65
+    def resize(self, max_size):
66
+        if self._im is None:
67
+            logger.warning('No image available to be resized')
68
+            return False
69
+        else:
70
+            logger.debug('Resizing picture to max %d pixel in whatever direction', max_size)
71
+            x, y = self._im.size
72
+            xy_max = max(x, y)
73
+            self._im = self._im.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0)
74
+        return True
75
+
76
+    def rotate_by_orientation(self, orientation=None):
77
+        if self._im is None:
78
+            logger.warning('No image available, rotation not possible')
79
+            return False
80
+
81
+        if orientation is None:
82
+            exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items())
83
+            try:
84
+                orientation = self._exif[exif_tags['Orientation']]
85
+                logger.debug("No orientation given, orientation %s extract from exif data", repr(orientation))
86
+            except KeyError:
87
+                return False
88
+
89
+        if orientation == ORIENTATION_HALF_ROTATED:
90
+            angle = 180
91
+        elif orientation == ORIENTATION_LEFT_ROTATED:
92
+            angle = 270
93
+        elif orientation == ORIENTATION_RIGHT_ROTATED:
94
+            angle = 90
95
+        else:
96
+            if type(orientation) == int and orientation > 8:
97
+                logger.warning('Orientation %s unknown for rotation', repr(orientation))
98
+            return False
99
+        logger.debug('Rotating picture by %d (deg)', angle)
100
+        self._im = self._im.rotate(angle, expand=True)
101
+        return True
102
+
103
+    def join(self, join_image, join_pos=JOIN_TOP_RIGHT, opacity=0.7):
104
+        from media.convert import get_pil_image
105
+
106
+        def rgba_copy(im):
107
+            if im.mode != 'RGBA':
108
+                return im.convert('RGBA')
109
+            else:
110
+                return im.copy()
111
+
112
+        if self._im is None:
113
+            logger.warning('No image available, joining not possible')
114
+            return False
115
+
116
+        # ensure type of join_image is PIL.Image
117
+        join_image = get_pil_image(join_image)
118
+        if join_image is None:
119
+            logger.warning('Image to be joined is not supported %s', repr(join_image))
120
+            return False
121
+
122
+        im2 = rgba_copy(join_image)
123
+        # change opacity of im2
124
+        alpha = im2.split()[3]
125
+        alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
126
+        im2.putalpha(alpha)
127
+
128
+        self._im = rgba_copy(self._im)
129
+
130
+        # create a transparent layer
131
+        layer = Image.new('RGBA', self._im.size, (0, 0, 0, 0))
132
+        # draw im2 in layer
133
+        if join_pos == JOIN_TOP_LEFT:
134
+            layer.paste(im2, (0, 0))
135
+        elif join_pos == JOIN_TOP_RIGHT:
136
+            layer.paste(im2, ((self._im.size[0] - im2.size[0]), 0))
137
+        elif join_pos == JOIN_BOT_LEFT:
138
+            layer.paste(im2, (0, (self._im.size[1] - im2.size[1])))
139
+        elif join_pos == JOIN_BOT_RIGHT:
140
+            layer.paste(im2, ((self._im.size[0] - im2.size[0]), (self._im.size[1] - im2.size[1])))
141
+        elif join_pos == JOIN_CENTER:
142
+            layer.paste(im2, (int((self._im.size[0] - im2.size[0]) / 2), int((self._im.size[1] - im2.size[1]) / 2)))
143
+        else:
144
+            logger.warning("Join position value %s is not supported", join_pos)
145
+            return False
146
+
147
+        logger.debug('Joining two images')
148
+        self._im = Image.composite(layer, self._im, layer)
149
+
150
+        return True

+ 135
- 51
metadata.py View File

@@ -1,47 +1,131 @@
1
-import logging
2
-import os
3
-from PIL import Image
4
-import math
5
-import media
6
-import subprocess
1
+import media.CDDB
7 2
 import time
3
+import subprocess
4
+from media import common
5
+import math
6
+from PIL import Image
7
+import os
8
+import logging
9
+import sys
8 10
 
9 11
 
10
-logger = media.logger
11
-
12
+try:
13
+    from config import APP_NAME as ROOT_LOGGER_NAME
14
+except ImportError:
15
+    ROOT_LOGGER_NAME = 'root'
16
+logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
17
+try:
18
+    import discid
19
+except OSError:
20
+    logger.exception("You might install python3-libdiscid")
12 21
 
13 22
 __KEY_CAMERA_VENDOR__ = 'camera_vendor'
14 23
 __KEY_CAMERA_MODEL__ = 'camera_model'
15 24
 
16 25
 
26
+def get_media_data(full_path, user_callback=None):
27
+    #
28
+    ft = common.get_filetype(full_path)
29
+    #
30
+    if ft == common.FILETYPE_AUDIO:
31
+        return get_audio_data(full_path)
32
+    elif ft == common.FILETYPE_IMAGE:
33
+        return get_image_data(full_path)
34
+    elif ft == common.FILETYPE_VIDEO:
35
+        return get_video_data(full_path)
36
+    elif ft == common.FILETYPE_DISC:
37
+        return get_disc_data(full_path, user_callback)
38
+    else:
39
+        logger.warning('Filetype not known: %s', full_path)
40
+
41
+
17 42
 def get_audio_data(full_path):
18 43
     conv_key_dict = {}
19
-    conv_key_dict['album'] = (str, media.KEY_ALBUM)
20
-    conv_key_dict['TAG:album'] = (str, media.KEY_ALBUM)
21
-    conv_key_dict['TAG:artist'] = (str, media.KEY_ARTIST)
22
-    conv_key_dict['artist'] = (str, media.KEY_ARTIST)
23
-    conv_key_dict['bit_rate'] = (__int_conv__, media.KEY_BITRATE)
24
-    conv_key_dict['duration'] = (float, media.KEY_DURATION)
25
-    conv_key_dict['TAG:genre'] = (str, media.KEY_GENRE)
26
-    conv_key_dict['genre'] = (str, media.KEY_GENRE)
27
-    conv_key_dict['TAG:title'] = (str, media.KEY_TITLE)
28
-    conv_key_dict['title'] = (str, media.KEY_TITLE)
29
-    conv_key_dict['TAG:track'] = (__int_conv__, media.KEY_TRACK)
30
-    conv_key_dict['track'] = (__int_conv__, media.KEY_TRACK)
31
-    conv_key_dict['TAG:date'] = (__int_conv__, media.KEY_YEAR)
32
-    conv_key_dict['date'] = (__int_conv__, media.KEY_YEAR)
44
+    conv_key_dict['album'] = (str, common.KEY_ALBUM)
45
+    conv_key_dict['TAG:album'] = (str, common.KEY_ALBUM)
46
+    conv_key_dict['TAG:artist'] = (str, common.KEY_ARTIST)
47
+    conv_key_dict['artist'] = (str, common.KEY_ARTIST)
48
+    conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE)
49
+    conv_key_dict['duration'] = (float, common.KEY_DURATION)
50
+    conv_key_dict['TAG:genre'] = (str, common.KEY_GENRE)
51
+    conv_key_dict['genre'] = (str, common.KEY_GENRE)
52
+    conv_key_dict['TAG:title'] = (str, common.KEY_TITLE)
53
+    conv_key_dict['title'] = (str, common.KEY_TITLE)
54
+    conv_key_dict['TAG:track'] = (__int_conv__, common.KEY_TRACK)
55
+    conv_key_dict['track'] = (__int_conv__, common.KEY_TRACK)
56
+    conv_key_dict['TAG:date'] = (__int_conv__, common.KEY_YEAR)
57
+    conv_key_dict['date'] = (__int_conv__, common.KEY_YEAR)
33 58
     return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
34 59
 
35 60
 
61
+def get_disc_data(full_path, user_callback):
62
+    #
63
+    # Read Information from CDDB database
64
+    #
65
+    did = media.CDDB.discid()
66
+    if did is None:
67
+        logger.error("Could not determine disc id...")
68
+        sys.exit(1)
69
+    q = media.CDDB.query(did)
70
+    if q is None:
71
+        data = {
72
+            common.KEY_ARTIST: None,
73
+            common.KEY_ALBUM: None,
74
+            common.KEY_YEAR: None,
75
+            common.KEY_GENRE: None
76
+        }
77
+        for i in range(0, int(did.split('+')[1])):
78
+            data["track_%02d" % (i + 1)] = None
79
+        data = user_callback(common.CALLBACK_MAN_INPUT, data)
80
+        return media.CDDB.my_disc_metadata(**data)
81
+
82
+    if len(q) == 1:
83
+        # Only one entry
84
+        did = tuple(q.keys())[0]
85
+    else:
86
+        # multiple entries (choose)
87
+        if user_callback is None:
88
+            logger.warning("No usercallback to handle multiple cddb choices...")
89
+            sys.exit(1)
90
+        did = user_callback(common.CALLBACK_CDDB_CHOICE, q)
91
+
92
+    return media.CDDB.cddb(did)
93
+    """
94
+    musicbrainzngs.set_useragent("pyrip", "0.1", "your@mail")
95
+    disc = discid.read()
96
+    disc_id = disc.id
97
+    disc_data = {}
98
+    try:
99
+        result = musicbrainzngs.get_releases_by_discid(disc_id, includes=["artists", "recordings"])
100
+    except musicbrainzngs.ResponseError:
101
+        logger.exception("disc not found or bad response")
102
+        sys.exit(1)
103
+    else:
104
+        disc_data[common.KEY_ARTIST] = result["disc"]["release-list"][0]["artist-credit-phrase"]
105
+        disc_data[common.KEY_ALBUM] = result["disc"]["release-list"][0]["title"]
106
+        disc_data[common.KEY_YEAR] = int(result["disc"]["release-list"][0]["date"][:4])
107
+        data_copy = dict(disc_data)
108
+        disc_data["id"] = result["disc"]["release-list"][0]["id"]
109
+        disc_data["tracks"] = []
110
+        # get tracklist
111
+        for entry in result["disc"]["release-list"][0]["medium-list"][0]["track-list"]:
112
+            track = dict(data_copy)
113
+            track[common.KEY_TITLE] = entry["recording"]["title"]
114
+            track[common.KEY_TRACK] = int(entry['number'])
115
+            disc_data["tracks"].append(track)
116
+    return disc_data
117
+    """
118
+
119
+
36 120
 def get_video_data(full_path):
37 121
     conv_key_dict = {}
38
-    conv_key_dict['creation_time'] = (__vid_datetime_conv__, media.KEY_TIME)
39
-    conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, media.KEY_TIME)
40
-    conv_key_dict['bit_rate'] = (__int_conv__, media.KEY_BITRATE)
41
-    conv_key_dict['duration'] = (float, media.KEY_DURATION)
42
-    conv_key_dict['height'] = (__int_conv__, media.KEY_HEIGHT)
43
-    conv_key_dict['width'] = (__int_conv__, media.KEY_WIDTH)
44
-    conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, media.KEY_RATIO)
122
+    conv_key_dict['creation_time'] = (__vid_datetime_conv__, common.KEY_TIME)
123
+    conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, common.KEY_TIME)
124
+    conv_key_dict['bit_rate'] = (__int_conv__, common.KEY_BITRATE)
125
+    conv_key_dict['duration'] = (float, common.KEY_DURATION)
126
+    conv_key_dict['height'] = (__int_conv__, common.KEY_HEIGHT)
127
+    conv_key_dict['width'] = (__int_conv__, common.KEY_WIDTH)
128
+    conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, common.KEY_RATIO)
45 129
     return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
46 130
 
47 131
 
@@ -50,25 +134,25 @@ def get_image_data(full_path):
50 134
 
51 135
 
52 136
 def __adapt__data__(data, full_path):
53
-    data[media.KEY_SIZE] = os.path.getsize(full_path)
137
+    data[common.KEY_SIZE] = os.path.getsize(full_path)
54 138
     # Join Camera Vendor and Camera Model
55 139
     if __KEY_CAMERA_MODEL__ in data and __KEY_CAMERA_VENDOR__ in data:
56 140
         model = data.pop(__KEY_CAMERA_MODEL__)
57 141
         vendor = data.pop(__KEY_CAMERA_VENDOR__)
58
-        data[media.KEY_CAMERA] = '%s: %s' % (vendor, model)
142
+        data[common.KEY_CAMERA] = '%s: %s' % (vendor, model)
59 143
     # Add time if not exists
60
-    if media.KEY_TIME not in data:
61
-        if media.KEY_YEAR in data and media.KEY_TRACK in data:
62
-            if data[media.KEY_YEAR] != 0:  # ignore year 0 - must be wrong
144
+    if common.KEY_TIME not in data:
145
+        if common.KEY_YEAR in data and common.KEY_TRACK in data:
146
+            if data[common.KEY_YEAR] != 0:  # ignore year 0 - must be wrong
63 147
                 # Use a date where track 1 is the newest in the given year
64
-                minute = int(data[media.KEY_TRACK] / 60)
65
-                second = (data[media.KEY_TRACK] - 60 * minute) % 60
148
+                minute = int(data[common.KEY_TRACK] / 60)
149
+                second = (data[common.KEY_TRACK] - 60 * minute) % 60
66 150
                 #
67
-                data[media.KEY_TIME] = int(time.mktime((data[media.KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0)))
68
-                data[media.KEY_TIME_IS_SUBSTITUTION] = True
151
+                data[common.KEY_TIME] = int(time.mktime((data[common.KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0)))
152
+                data[common.KEY_TIME_IS_SUBSTITUTION] = True
69 153
         else:
70
-            data[media.KEY_TIME] = int(os.path.getmtime(full_path))
71
-            data[media.KEY_TIME_IS_SUBSTITUTION] = True
154
+            data[common.KEY_TIME] = int(os.path.getmtime(full_path))
155
+            data[common.KEY_TIME_IS_SUBSTITUTION] = True
72 156
     return data
73 157
 
74 158
 
@@ -114,19 +198,19 @@ def __get_exif_data__(full_path):
114 198
     else:
115 199
         conv_key_dict = {}
116 200
         # IMAGE
117
-        conv_key_dict[0x9003] = (__datetime_conv__, media.KEY_TIME)
118
-        conv_key_dict[0x8822] = (__exposure_program_conv__, media.KEY_EXPOSURE_PROGRAM)
119
-        conv_key_dict[0x829A] = (__num_denum_conv__, media.KEY_EXPOSURE_TIME)
120
-        conv_key_dict[0x9209] = (__flash_conv__, media.KEY_FLASH)
121
-        conv_key_dict[0x829D] = (__num_denum_conv__, media.KEY_APERTURE)
122
-        conv_key_dict[0x920A] = (__num_denum_conv__, media.KEY_FOCAL_LENGTH)
123
-        conv_key_dict[0x8825] = (__gps_conv__, media.KEY_GPS)
124
-        conv_key_dict[0xA003] = (__int_conv__, media.KEY_HEIGHT)
125
-        conv_key_dict[0x8827] = (__int_conv__, media.KEY_ISO)
201
+        conv_key_dict[0x9003] = (__datetime_conv__, common.KEY_TIME)
202
+        conv_key_dict[0x8822] = (__exposure_program_conv__, common.KEY_EXPOSURE_PROGRAM)
203
+        conv_key_dict[0x829A] = (__num_denum_conv__, common.KEY_EXPOSURE_TIME)
204
+        conv_key_dict[0x9209] = (__flash_conv__, common.KEY_FLASH)
205
+        conv_key_dict[0x829D] = (__num_denum_conv__, common.KEY_APERTURE)
206
+        conv_key_dict[0x920A] = (__num_denum_conv__, common.KEY_FOCAL_LENGTH)
207
+        conv_key_dict[0x8825] = (__gps_conv__, common.KEY_GPS)
208
+        conv_key_dict[0xA003] = (__int_conv__, common.KEY_HEIGHT)
209
+        conv_key_dict[0x8827] = (__int_conv__, common.KEY_ISO)
126 210
         conv_key_dict[0x010F] = (str, __KEY_CAMERA_VENDOR__)
127 211
         conv_key_dict[0x0110] = (str, __KEY_CAMERA_MODEL__)
128
-        conv_key_dict[0x0112] = (__int_conv__, media.KEY_ORIENTATION)
129
-        conv_key_dict[0xA002] = (__int_conv__, media.KEY_WIDTH)
212
+        conv_key_dict[0x0112] = (__int_conv__, common.KEY_ORIENTATION)
213
+        conv_key_dict[0xA002] = (__int_conv__, common.KEY_WIDTH)
130 214
         for key in conv_key_dict:
131 215
             if key in exif:
132 216
                 tp, name = conv_key_dict[key]

+ 2
- 0
requirements.txt View File

@@ -0,0 +1,2 @@
1
+pillow
2
+discid

Loading…
Cancel
Save