1# -*- coding: utf-8 -*-
2# Copyright 2006 Joe Wreschnig
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
9"""Read and write Ogg Vorbis comments.
10
11This module handles Vorbis files wrapped in an Ogg bitstream. The
12first Vorbis stream found is used.
13
14Read more about Ogg Vorbis at http://vorbis.com/. This module is based
15on the specification at http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html.
16"""
17
18__all__ = ["OggVorbis", "Open", "delete"]
19
20import struct
21
22from mutagen import StreamInfo
23from mutagen._vorbis import VCommentDict
24from mutagen._util import get_size, loadfile, convert_error
25from mutagen._tags import PaddingInfo
26from mutagen.ogg import OggPage, OggFileType, error as OggError
27
28
29class error(OggError):
30    pass
31
32
33class OggVorbisHeaderError(error):
34    pass
35
36
37class OggVorbisInfo(StreamInfo):
38    """OggVorbisInfo()
39
40    Ogg Vorbis stream information.
41
42    Attributes:
43        length (`float`): File length in seconds, as a float
44        channels (`int`): Number of channels
45        bitrate (`int`): Nominal ('average') bitrate in bits per second
46        sample_Rate (`int`): Sample rate in Hz
47
48    """
49
50    length = 0.0
51    channels = 0
52    bitrate = 0
53    sample_rate = 0
54
55    def __init__(self, fileobj):
56        """Raises ogg.error, IOError"""
57
58        page = OggPage(fileobj)
59        while not page.packets[0].startswith(b"\x01vorbis"):
60            page = OggPage(fileobj)
61        if not page.first:
62            raise OggVorbisHeaderError(
63                "page has ID header, but doesn't start a stream")
64        (self.channels, self.sample_rate, max_bitrate, nominal_bitrate,
65         min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28])
66        self.serial = page.serial
67
68        max_bitrate = max(0, max_bitrate)
69        min_bitrate = max(0, min_bitrate)
70        nominal_bitrate = max(0, nominal_bitrate)
71
72        if nominal_bitrate == 0:
73            self.bitrate = (max_bitrate + min_bitrate) // 2
74        elif max_bitrate and max_bitrate < nominal_bitrate:
75            # If the max bitrate is less than the nominal, we know
76            # the nominal is wrong.
77            self.bitrate = max_bitrate
78        elif min_bitrate > nominal_bitrate:
79            self.bitrate = min_bitrate
80        else:
81            self.bitrate = nominal_bitrate
82
83    def _post_tags(self, fileobj):
84        """Raises ogg.error"""
85
86        page = OggPage.find_last(fileobj, self.serial, finishing=True)
87        if page is None:
88            raise OggVorbisHeaderError
89        self.length = page.position / float(self.sample_rate)
90
91    def pprint(self):
92        return u"Ogg Vorbis, %.2f seconds, %d bps" % (
93            self.length, self.bitrate)
94
95
96class OggVCommentDict(VCommentDict):
97    """Vorbis comments embedded in an Ogg bitstream."""
98
99    def __init__(self, fileobj, info):
100        pages = []
101        complete = False
102        while not complete:
103            page = OggPage(fileobj)
104            if page.serial == info.serial:
105                pages.append(page)
106                complete = page.complete or (len(page.packets) > 1)
107        data = OggPage.to_packets(pages)[0][7:]  # Strip off "\x03vorbis".
108        super(OggVCommentDict, self).__init__(data)
109        self._padding = len(data) - self._size
110
111    def _inject(self, fileobj, padding_func):
112        """Write tag data into the Vorbis comment packet/page."""
113
114        # Find the old pages in the file; we'll need to remove them,
115        # plus grab any stray setup packet data out of them.
116        fileobj.seek(0)
117        page = OggPage(fileobj)
118        while not page.packets[0].startswith(b"\x03vorbis"):
119            page = OggPage(fileobj)
120
121        old_pages = [page]
122        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
123            page = OggPage(fileobj)
124            if page.serial == old_pages[0].serial:
125                old_pages.append(page)
126
127        packets = OggPage.to_packets(old_pages, strict=False)
128
129        content_size = get_size(fileobj) - len(packets[0])  # approx
130        vcomment_data = b"\x03vorbis" + self.write()
131        padding_left = len(packets[0]) - len(vcomment_data)
132
133        info = PaddingInfo(padding_left, content_size)
134        new_padding = info._get_padding(padding_func)
135
136        # Set the new comment packet.
137        packets[0] = vcomment_data + b"\x00" * new_padding
138
139        new_pages = OggPage._from_packets_try_preserve(packets, old_pages)
140        OggPage.replace(fileobj, old_pages, new_pages)
141
142
143class OggVorbis(OggFileType):
144    """OggVorbis(filething)
145
146    Arguments:
147        filething (filething)
148
149    An Ogg Vorbis file.
150
151    Attributes:
152        info (`OggVorbisInfo`)
153        tags (`mutagen._vorbis.VCommentDict`)
154    """
155
156    _Info = OggVorbisInfo
157    _Tags = OggVCommentDict
158    _Error = OggVorbisHeaderError
159    _mimes = ["audio/vorbis", "audio/x-vorbis"]
160
161    info = None
162    tags = None
163
164    @staticmethod
165    def score(filename, fileobj, header):
166        return (header.startswith(b"OggS") * (b"\x01vorbis" in header))
167
168
169Open = OggVorbis
170
171
172@convert_error(IOError, error)
173@loadfile(method=False, writable=True)
174def delete(filething):
175    """ delete(filething)
176
177    Arguments:
178        filething (filething)
179    Raises:
180        mutagen.MutagenError
181
182    Remove tags from a file.
183    """
184
185    t = OggVorbis(filething)
186    filething.fileobj.seek(0)
187    t.delete(filething)
188