1# -*- coding: utf-8 -*-
2# Copyright (C) 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 FLAC comments.
10
11This module handles FLAC files wrapped in an Ogg bitstream. The first
12FLAC stream found is used. For 'naked' FLACs, see mutagen.flac.
13
14This module is based off the specification at
15http://flac.sourceforge.net/ogg_mapping.html.
16"""
17
18__all__ = ["OggFLAC", "Open", "delete"]
19
20import struct
21
22from ._compat import cBytesIO
23
24from mutagen import StreamInfo
25from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError
26from mutagen._vorbis import VCommentDict
27from mutagen._util import loadfile, convert_error
28from mutagen.ogg import OggPage, OggFileType, error as OggError
29
30
31class error(OggError):
32    pass
33
34
35class OggFLACHeaderError(error):
36    pass
37
38
39class OggFLACStreamInfo(StreamInfo):
40    """OggFLACStreamInfo()
41
42    Ogg FLAC stream info.
43
44    Attributes:
45        length (`float`): File length in seconds, as a float
46        channels (`float`): Number of channels
47        sample_rate (`int`): Sample rate in Hz"
48    """
49
50    length = 0
51    channels = 0
52    sample_rate = 0
53
54    def __init__(self, fileobj):
55        page = OggPage(fileobj)
56        while not page.packets[0].startswith(b"\x7FFLAC"):
57            page = OggPage(fileobj)
58        major, minor, self.packets, flac = struct.unpack(
59            ">BBH4s", page.packets[0][5:13])
60        if flac != b"fLaC":
61            raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac)
62        elif (major, minor) != (1, 0):
63            raise OggFLACHeaderError(
64                "unknown mapping version: %d.%d" % (major, minor))
65        self.serial = page.serial
66
67        # Skip over the block header.
68        stringobj = cBytesIO(page.packets[0][17:])
69
70        try:
71            flac_info = FLACStreamInfo(stringobj)
72        except FLACError as e:
73            raise OggFLACHeaderError(e)
74
75        for attr in ["min_blocksize", "max_blocksize", "sample_rate",
76                     "channels", "bits_per_sample", "total_samples", "length"]:
77            setattr(self, attr, getattr(flac_info, attr))
78
79    def _post_tags(self, fileobj):
80        if self.length:
81            return
82        page = OggPage.find_last(fileobj, self.serial, finishing=True)
83        if page is None:
84            raise OggFLACHeaderError
85        self.length = page.position / float(self.sample_rate)
86
87    def pprint(self):
88        return u"Ogg FLAC, %.2f seconds, %d Hz" % (
89            self.length, self.sample_rate)
90
91
92class OggFLACVComment(VCommentDict):
93
94    def __init__(self, fileobj, info):
95        # data should be pointing at the start of an Ogg page, after
96        # the first FLAC page.
97        pages = []
98        complete = False
99        while not complete:
100            page = OggPage(fileobj)
101            if page.serial == info.serial:
102                pages.append(page)
103                complete = page.complete or (len(page.packets) > 1)
104        comment = cBytesIO(OggPage.to_packets(pages)[0][4:])
105        super(OggFLACVComment, self).__init__(comment, framing=False)
106
107    def _inject(self, fileobj, padding_func):
108        """Write tag data into the FLAC Vorbis comment packet/page."""
109
110        # Ogg FLAC has no convenient data marker like Vorbis, but the
111        # second packet - and second page - must be the comment data.
112        fileobj.seek(0)
113        page = OggPage(fileobj)
114        while not page.packets[0].startswith(b"\x7FFLAC"):
115            page = OggPage(fileobj)
116
117        first_page = page
118        while not (page.sequence == 1 and page.serial == first_page.serial):
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 == first_page.serial:
125                old_pages.append(page)
126
127        packets = OggPage.to_packets(old_pages, strict=False)
128
129        # Set the new comment block.
130        data = self.write(framing=False)
131        data = packets[0][:1] + struct.pack(">I", len(data))[-3:] + data
132        packets[0] = data
133
134        new_pages = OggPage.from_packets(packets, old_pages[0].sequence)
135        OggPage.replace(fileobj, old_pages, new_pages)
136
137
138class OggFLAC(OggFileType):
139    """OggFLAC(filething)
140
141    An Ogg FLAC file.
142
143    Arguments:
144        filething (filething)
145
146    Attributes:
147        info (`OggFLACStreamInfo`)
148        tags (`mutagen._vorbis.VCommentDict`)
149    """
150
151    _Info = OggFLACStreamInfo
152    _Tags = OggFLACVComment
153    _Error = OggFLACHeaderError
154    _mimes = ["audio/x-oggflac"]
155
156    info = None
157    tags = None
158
159    @staticmethod
160    def score(filename, fileobj, header):
161        return (header.startswith(b"OggS") * (
162            (b"FLAC" in header) + (b"fLaC" in header)))
163
164
165Open = OggFLAC
166
167
168@convert_error(IOError, error)
169@loadfile(method=False, writable=True)
170def delete(filething):
171    """ delete(filething)
172
173    Arguments:
174        filething (filething)
175    Raises:
176        mutagen.MutagenError
177
178    Remove tags from a file.
179    """
180
181    t = OggFLAC(filething)
182    filething.fileobj.seek(0)
183    t.delete(filething)
184