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