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