1# -*- coding: utf-8 -*- 2# Copyright (C) 2005 Michael Urman 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8 9import warnings 10 11from mutagen._util import DictMixin, loadfile 12from mutagen._compat import izip 13 14 15class FileType(DictMixin): 16 """FileType(filething, **kwargs) 17 18 Args: 19 filething (filething): A filename or a file-like object 20 21 Subclasses might take further options via keyword arguments. 22 23 An abstract object wrapping tags and audio stream information. 24 25 Each file format has different potential tags and stream 26 information. 27 28 FileTypes implement an interface very similar to Metadata; the 29 dict interface, save, load, and delete calls on a FileType call 30 the appropriate methods on its tag data. 31 32 Attributes: 33 info (`StreamInfo`): contains length, bitrate, sample rate 34 tags (`Tags`): metadata tags, if any, otherwise `None` 35 """ 36 37 __module__ = "mutagen" 38 39 info = None 40 tags = None 41 filename = None 42 _mimes = ["application/octet-stream"] 43 44 def __init__(self, *args, **kwargs): 45 if not args and not kwargs: 46 warnings.warn("FileType constructor requires a filename", 47 DeprecationWarning) 48 else: 49 self.load(*args, **kwargs) 50 51 @loadfile() 52 def load(self, filething, *args, **kwargs): 53 raise NotImplementedError 54 55 def __getitem__(self, key): 56 """Look up a metadata tag key. 57 58 If the file has no tags at all, a KeyError is raised. 59 """ 60 61 if self.tags is None: 62 raise KeyError(key) 63 else: 64 return self.tags[key] 65 66 def __setitem__(self, key, value): 67 """Set a metadata tag. 68 69 If the file has no tags, an appropriate format is added (but 70 not written until save is called). 71 """ 72 73 if self.tags is None: 74 self.add_tags() 75 self.tags[key] = value 76 77 def __delitem__(self, key): 78 """Delete a metadata tag key. 79 80 If the file has no tags at all, a KeyError is raised. 81 """ 82 83 if self.tags is None: 84 raise KeyError(key) 85 else: 86 del(self.tags[key]) 87 88 def keys(self): 89 """Return a list of keys in the metadata tag. 90 91 If the file has no tags at all, an empty list is returned. 92 """ 93 94 if self.tags is None: 95 return [] 96 else: 97 return self.tags.keys() 98 99 @loadfile(writable=True) 100 def delete(self, filething=None): 101 """delete(filething=None) 102 103 Remove tags from a file. 104 105 In cases where the tagging format is independent of the file type 106 (for example `mutagen.id3.ID3`) all traces of the tagging format will 107 be removed. 108 In cases where the tag is part of the file type, all tags and 109 padding will be removed. 110 111 The tags attribute will be cleared as well if there is one. 112 113 Does nothing if the file has no tags. 114 115 Raises: 116 mutagen.MutagenError: if deleting wasn't possible 117 """ 118 119 if self.tags is not None: 120 return self.tags.delete(filething) 121 122 @loadfile(writable=True) 123 def save(self, filething=None, **kwargs): 124 """save(filething=None, **kwargs) 125 126 Save metadata tags. 127 128 Raises: 129 MutagenError: if saving wasn't possible 130 """ 131 132 if self.tags is not None: 133 return self.tags.save(filething, **kwargs) 134 135 def pprint(self): 136 """ 137 Returns: 138 text: stream information and comment key=value pairs. 139 """ 140 141 stream = "%s (%s)" % (self.info.pprint(), self.mime[0]) 142 try: 143 tags = self.tags.pprint() 144 except AttributeError: 145 return stream 146 else: 147 return stream + ((tags and "\n" + tags) or "") 148 149 def add_tags(self): 150 """Adds new tags to the file. 151 152 Raises: 153 mutagen.MutagenError: 154 if tags already exist or adding is not possible. 155 """ 156 157 raise NotImplementedError 158 159 @property 160 def mime(self): 161 """A list of mime types (:class:`mutagen.text`)""" 162 163 mimes = [] 164 for Kind in type(self).__mro__: 165 for mime in getattr(Kind, '_mimes', []): 166 if mime not in mimes: 167 mimes.append(mime) 168 return mimes 169 170 @staticmethod 171 def score(filename, fileobj, header): 172 """Returns a score for how likely the file can be parsed by this type. 173 174 Args: 175 filename (fspath): a file path 176 fileobj (fileobj): a file object open in rb mode. Position is 177 undefined 178 header (bytes): data of undefined length, starts with the start of 179 the file. 180 181 Returns: 182 int: negative if definitely not a matching type, otherwise a score, 183 the bigger the more certain that the file can be loaded. 184 """ 185 186 raise NotImplementedError 187 188 189class StreamInfo(object): 190 """Abstract stream information object. 191 192 Provides attributes for length, bitrate, sample rate etc. 193 194 See the implementations for details. 195 """ 196 197 __module__ = "mutagen" 198 199 def pprint(self): 200 """ 201 Returns: 202 text: Print stream information 203 """ 204 205 raise NotImplementedError 206 207 208@loadfile(method=False) 209def File(filething, options=None, easy=False): 210 """File(filething, options=None, easy=False) 211 212 Guess the type of the file and try to open it. 213 214 The file type is decided by several things, such as the first 128 215 bytes (which usually contains a file type identifier), the 216 filename extension, and the presence of existing tags. 217 218 If no appropriate type could be found, None is returned. 219 220 Args: 221 filething (filething) 222 options: Sequence of :class:`FileType` implementations, 223 defaults to all included ones. 224 easy (bool): If the easy wrappers should be returnd if available. 225 For example :class:`EasyMP3 <mp3.EasyMP3>` instead of 226 :class:`MP3 <mp3.MP3>`. 227 228 Returns: 229 FileType: A FileType instance for the detected type or `None` in case 230 the type couln't be determined. 231 232 Raises: 233 MutagenError: in case the detected type fails to load the file. 234 """ 235 236 if options is None: 237 from mutagen.asf import ASF 238 from mutagen.apev2 import APEv2File 239 from mutagen.flac import FLAC 240 if easy: 241 from mutagen.easyid3 import EasyID3FileType as ID3FileType 242 else: 243 from mutagen.id3 import ID3FileType 244 if easy: 245 from mutagen.mp3 import EasyMP3 as MP3 246 else: 247 from mutagen.mp3 import MP3 248 from mutagen.oggflac import OggFLAC 249 from mutagen.oggspeex import OggSpeex 250 from mutagen.oggtheora import OggTheora 251 from mutagen.oggvorbis import OggVorbis 252 from mutagen.oggopus import OggOpus 253 if easy: 254 from mutagen.trueaudio import EasyTrueAudio as TrueAudio 255 else: 256 from mutagen.trueaudio import TrueAudio 257 from mutagen.wavpack import WavPack 258 if easy: 259 from mutagen.easymp4 import EasyMP4 as MP4 260 else: 261 from mutagen.mp4 import MP4 262 from mutagen.musepack import Musepack 263 from mutagen.monkeysaudio import MonkeysAudio 264 from mutagen.optimfrog import OptimFROG 265 from mutagen.aiff import AIFF 266 from mutagen.aac import AAC 267 from mutagen.smf import SMF 268 from mutagen.dsf import DSF 269 options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, 270 FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, 271 Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, 272 SMF, DSF] 273 274 if not options: 275 return None 276 277 fileobj = filething.fileobj 278 279 try: 280 header = fileobj.read(128) 281 except IOError: 282 header = b"" 283 284 # Sort by name after score. Otherwise import order affects 285 # Kind sort order, which affects treatment of things with 286 # equals scores. 287 results = [(Kind.score(filething.name, fileobj, header), Kind.__name__) 288 for Kind in options] 289 290 results = list(izip(results, options)) 291 results.sort() 292 (score, name), Kind = results[-1] 293 if score > 0: 294 try: 295 fileobj.seek(0, 0) 296 except IOError: 297 pass 298 return Kind(fileobj, filename=filething.filename) 299 else: 300 return None 301