1# Copyright 2006 Lukas Lalinsky
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8import struct
9
10import mutagen.asf
11
12from quodlibet.util.path import get_temp_cover_file
13
14from ._audio import AudioFile
15from ._image import EmbeddedImage, APICType
16from ._misc import AudioFileError, translate_errors
17
18
19class WMAFile(AudioFile):
20    mimes = ["audio/x-ms-wma", "audio/x-ms-wmv", "video/x-ms-asf",
21             "audio/x-wma", "video/x-wmv"]
22    format = "ASF"
23
24    #http://msdn.microsoft.com/en-us/library/dd743066%28VS.85%29.aspx
25    #http://msdn.microsoft.com/en-us/library/dd743063%28VS.85%29.aspx
26    #http://msdn.microsoft.com/en-us/library/dd743220%28VS.85%29.aspx
27    __translate = {
28        "WM/AlbumTitle": "album",
29        "Title": "title",
30        "Author": "artist",
31        "WM/AlbumArtist": "albumartist",
32        "WM/Composer": "composer",
33        "WM/Writer": "lyricist",
34        "WM/Conductor": "conductor",
35        "WM/ModifiedBy": "remixer",
36        "WM/Producer": "producer",
37        "WM/ContentGroupDescription": "grouping",
38        "WM/SubTitle": "discsubtitle",
39        "WM/TrackNumber": "tracknumber",
40        "WM/PartOfSet": "discnumber",
41        "WM/BeatsPerMinute": "bpm",
42        "Copyright": "copyright",
43        "WM/ISRC": "isrc",
44        "WM/Mood": "mood",
45        "WM/EncodedBy": "encodedby",
46        "MusicBrainz/Track Id": "musicbrainz_trackid",
47        "MusicBrainz/Release Track Id": "musicbrainz_releasetrackid",
48        "MusicBrainz/Album Id": "musicbrainz_albumid",
49        "MusicBrainz/Artist Id": "musicbrainz_artistid",
50        "MusicBrainz/Album Artist Id": "musicbrainz_albumartistid",
51        "MusicBrainz/TRM Id": "musicbrainz_trmid",
52        "MusicIP/PUID": "musicip_puid",
53        "MusicBrainz/Release Group Id": "musicbrainz_releasegroupid",
54        "WM/Year": "date",
55        "WM/OriginalArtist": "originalartist",
56        "WM/OriginalAlbumTitle": "originalalbum",
57        "WM/AlbumSortOrder": "albumsort",
58        "WM/ArtistSortOrder": "artistsort",
59        "WM/AlbumArtistSortOrder": "albumartistsort",
60        "WM/Genre": "genre",
61        "WM/Publisher": "publisher",
62        "WM/AuthorURL": "website",
63        "Description": "comment"
64    }
65    __rtranslate = dict([(v, k) for k, v in __translate.items()])
66
67    # http://msdn.microsoft.com/en-us/library/dd743065.aspx
68    # note: not all names here are used by QL
69    __multi_value_attr = {
70        "Author",
71        "WM/AlbumArtist",
72        "WM/AlbumCoverURL",
73        "WM/Category",
74        "WM/Composer",
75        "WM/Conductor",
76        "WM/Director",
77        "WM/Genre",
78        "WM/GenreID",
79        "WM/Language",
80        "WM/Lyrics_Synchronised",
81        "WM/Mood",
82        "WM/Picture",
83        "WM/Producer",
84        "WM/PromotionURL",
85        "WM/UserWebURL",
86        "WM/Writer",
87    }
88
89    __multi_value_keys = set()
90    for k, v in __translate.items():
91        if k in __multi_value_attr:
92            __multi_value_keys.add(v)
93
94    def __init__(self, filename, audio=None):
95        if audio is None:
96            with translate_errors():
97                audio = mutagen.asf.ASF(filename)
98        info = audio.info
99
100        self["~#length"] = info.length
101        self["~#bitrate"] = int(info.bitrate / 1000)
102        if info.channels:
103            self["~#channels"] = info.channels
104        self["~#samplerate"] = info.sample_rate
105
106        type_, name, desc = info.codec_type, info.codec_name, \
107            info.codec_description
108
109        if type_:
110            self["~codec"] = type_
111        encoding = u"\n".join(filter(None, [name, desc]))
112        if encoding:
113            self["~encoding"] = encoding
114
115        for name, values in audio.tags.items():
116            if name == "WM/Picture":
117                self.has_images = True
118            try:
119                name = self.__translate[name]
120            except KeyError:
121                continue
122            self[name] = u"\n".join(map(str, values))
123        self.sanitize(filename)
124
125    def write(self):
126        with translate_errors():
127            audio = mutagen.asf.ASF(self["~filename"])
128        for key in self.__translate.keys():
129            try:
130                del(audio[key])
131            except KeyError:
132                pass
133
134        for key in self.realkeys():
135            try:
136                name = self.__rtranslate[key]
137            except KeyError:
138                continue
139            audio.tags[name] = self.list(key)
140        with translate_errors():
141            audio.save()
142        self.sanitize()
143
144    def can_multiple_values(self, key=None):
145        if key is None:
146            return self.__multi_value_keys
147        return key in self.__multi_value_keys
148
149    def can_change(self, key=None):
150        OK = self.__rtranslate.keys()
151        if key is None:
152            return OK
153        else:
154            return super(WMAFile, self).can_change(key) and (key in OK)
155
156    def get_images(self):
157        images = []
158
159        try:
160            tag = mutagen.asf.ASF(self["~filename"])
161        except Exception:
162            return images
163
164        for image in tag.get("WM/Picture", []):
165            try:
166                (mime, desc, data, type_) = unpack_image(image.value)
167            except ValueError:
168                continue
169            f = get_temp_cover_file(data)
170            images.append(EmbeddedImage(f, mime, type_=type_))
171
172        images.sort(key=lambda c: c.sort_key)
173        return images
174
175    def get_primary_image(self):
176        """Returns the primary embedded image or None"""
177
178        try:
179            tag = mutagen.asf.ASF(self["~filename"])
180        except Exception:
181            return
182
183        for image in tag.get("WM/Picture", []):
184            try:
185                (mime, desc, data, type_) = unpack_image(image.value)
186            except ValueError:
187                continue
188            if type_ == APICType.COVER_FRONT:  # Only cover images
189                f = get_temp_cover_file(data)
190                return EmbeddedImage(f, mime, type_=type_)
191
192    can_change_images = True
193
194    def clear_images(self):
195        """Delete all embedded images"""
196
197        with translate_errors():
198            tag = mutagen.asf.ASF(self["~filename"])
199            tag.pop("WM/Picture", None)
200            tag.save()
201
202        self.has_images = False
203
204    def set_image(self, image):
205        """Replaces all embedded images by the passed image"""
206
207        with translate_errors():
208            tag = mutagen.asf.ASF(self["~filename"])
209
210        try:
211            imagedata = image.read()
212        except EnvironmentError as e:
213            raise AudioFileError(e)
214
215        # thumbnail gets used in WMP..
216        data = pack_image(image.mime_type, u"thumbnail",
217                          imagedata, APICType.COVER_FRONT)
218
219        value = mutagen.asf.ASFValue(data, mutagen.asf.BYTEARRAY)
220        tag["WM/Picture"] = [value]
221
222        with translate_errors():
223            tag.save()
224
225        self.has_images = True
226
227
228def unpack_image(data):
229    """
230    Helper function to unpack image data from a WM/Picture tag.
231
232    The data has the following format:
233    1 byte: Picture type (0-20), see ID3 APIC frame specification at
234    http://www.id3.org/id3v2.4.0-frames
235    4 bytes: Picture data length in LE format
236    MIME type, null terminated UTF-16-LE string
237    Description, null terminated UTF-16-LE string
238    The image data in the given length
239    """
240
241    try:
242        (type_, size) = struct.unpack_from("<bi", data)
243    except struct.error as e:
244        raise ValueError(e)
245    data = data[5:]
246
247    mime = b""
248    while data:
249        char, data = data[:2], data[2:]
250        if char == b"\x00\x00":
251            break
252        mime += char
253    else:
254        raise ValueError("mime: missing data")
255
256    mime = mime.decode("utf-16-le")
257
258    description = b""
259    while data:
260        char, data = data[:2], data[2:]
261        if char == b"\x00\x00":
262            break
263        description += char
264    else:
265        raise ValueError("desc: missing data")
266
267    description = description.decode("utf-16-le")
268
269    if size != len(data):
270        raise ValueError("image data size mismatch")
271
272    return (mime, description, data, type_)
273
274
275def pack_image(mime, description, imagedata, type_):
276    assert APICType.is_valid(type_)
277
278    size = len(imagedata)
279    data = struct.pack("<bi", type_, size)
280    data += mime.encode("utf-16-le") + b"\x00\x00"
281    data += description.encode("utf-16-le") + b"\x00\x00"
282    data += imagedata
283
284    return data
285
286
287loader = WMAFile
288types = [WMAFile]
289extensions = [".wma", ".asf", ".wmv"]
290