MythTV master
disc.py
Go to the documentation of this file.
1# Copyright (C) 2013 Johannes Dewender
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15#
16# Please submit bug reports to GitHub:
17# https://github.com/JonnyJD/python-discid/issues
18"""Disc class
19"""
20
21import re
22from ctypes import c_int, c_void_p, c_char_p, c_uint
23
24from discid.libdiscid import _LIB, FEATURES
25from discid.util import _encode, _decode, _sectors_to_seconds
26from discid.track import Track
27
28
29# our implemented of libdiscid's enum discid_feature
30_FEATURE_MAPPING = {"read": 1 << 0, "mcn": 1 << 1, "isrc": 1 << 2}
31
32
33FEATURES_IMPLEMENTED = list(_FEATURE_MAPPING.keys())
34
35def read(device=None, features=[]):
36 """Reads the TOC from the device given as string
37 and returns a :class:`Disc` object.
38
39 That string can be either of:
40 :obj:`str <python:str>`, :obj:`unicode` or :obj:`bytes`.
41 However, it should in no case contain non-ASCII characters.
42 If no device is given, a default, also given by :func:`get_default_device`
43 is used.
44
45 You can optionally add a subset of the features in
46 :data:`FEATURES` or the whole list to read more than just the TOC.
47 In contrast to libdiscid, :func:`read` won't read any
48 of the additional features by default.
49
50 A :exc:`DiscError` exception is raised when the reading fails,
51 and :exc:`NotImplementedError` when libdiscid doesn't support
52 reading discs on the current platform.
53 """
54 disc = Disc()
55 disc.read(device, features)
56 return disc
57
58def put(first, last, disc_sectors, track_offsets):
59 """Creates a TOC based on the information given
60 and returns a :class:`Disc` object.
61
62 Takes the `first` track and `last` **audio** track as :obj:`int`.
63 `disc_sectors` is the end of the last audio track,
64 normally the total sector count of the disc.
65 `track_offsets` is a list of all audio track offsets.
66
67 Depending on how you get the total sector count,
68 you might have to substract 11400 (2:32 min.) for discs with data tracks.
69
70 A :exc:`TOCError` exception is raised when illegal parameters
71 are provided.
72
73 .. seealso:: :musicbrainz:`Disc ID Calculation`
74 """
75 disc = Disc()
76 disc.put(first, last, disc_sectors, track_offsets)
77 return disc
78
79
80class DiscError(IOError):
81 """:func:`read` will raise this exception when an error occured.
82 """
83 pass
84
85class TOCError(Exception):
86 """:func:`put` will raise this exception when illegal paramaters
87 are provided.
88 """
89 pass
90
91
92class Disc(object):
93 """The class of the object returned by :func:`read` or :func:`put`.
94 """
95
96 _LIB.discid_new.argtypes = ()
97 _LIB.discid_new.restype = c_void_p
98 def __init__(self):
99 """The initialization will reserve some memory
100 for internal data structures.
101 """
102 self._handle = c_void_p(_LIB.discid_new())
103 self._success = False
105 assert self._handle.value is not None
106
107 def __str__(self):
108 assert self._success
109 return self.id
110
111 _LIB.discid_get_error_msg.argtypes = (c_void_p, )
112 _LIB.discid_get_error_msg.restype = c_char_p
113 def _get_error_msg(self):
114 """Get the error message for the last error with the object.
115 """
116 error = _LIB.discid_get_error_msg(self._handle)
117 return _decode(error)
118
119
120 _LIB.discid_read.argtypes = (c_void_p, c_char_p)
121 _LIB.discid_read.restype = c_int
122 try:
123 _LIB.discid_read_sparse.argtypes = (c_void_p, c_char_p, c_uint)
124 _LIB.discid_read_sparse.restype = c_int
125 except AttributeError:
126 pass
127 def read(self, device=None, features=[]):
128 """Reads the TOC from the device given as string
129
130 The user is supposed to use :func:`discid.read`.
131 """
132 if "read" not in FEATURES:
133 raise NotImplementedError("discid_read not implemented on platform")
134
135 # only use features implemented on this platform and in this module
136 self._requested_features = list(set(features) & set(FEATURES)
137 & set(FEATURES_IMPLEMENTED))
138
139 # create the bitmask for libdiscid
140 c_features = 0
141 for feature in features:
142 c_features += _FEATURE_MAPPING.get(feature, 0)
143
144 # device = None will use the default device (internally)
145 try:
146 result = _LIB.discid_read_sparse(self._handle, _encode(device),
147 c_features) == 1
148 except AttributeError:
149 result = _LIB.discid_read(self._handle, _encode(device)) == 1
150 self._success = result
151 if not self._success:
152 raise DiscError(self._get_error_msg())
153 return self._success
154
155 _LIB.discid_put.argtypes = (c_void_p, c_int, c_int, c_void_p)
156 _LIB.discid_put.restype = c_int
157 def put(self, first, last, disc_sectors, track_offsets):
158 """Creates a TOC based on the input given.
159
160 The user is supposed to use :func:`discid.put`.
161 """
162 # check for common usage errors
163 if len(track_offsets) != last - first + 1:
164 raise TOCError("Invalid number of track offsets")
165 elif False in [disc_sectors >= off for off in track_offsets]:
166 raise TOCError("Disc sector count too low")
167
168 # only the "read" (= TOC) feature is supported by put
169 self._requested_features = ["read"]
170
171 offsets = [disc_sectors] + track_offsets
172 c_offsets = (c_int * len(offsets))(*tuple(offsets))
173 result = _LIB.discid_put(self._handle, first, last, c_offsets) == 1
174 self._success = result
175 if not self._success:
176 raise TOCError(self._get_error_msg())
177 return self._success
178
179
180 _LIB.discid_get_id.argtypes = (c_void_p, )
181 _LIB.discid_get_id.restype = c_char_p
182 def _get_id(self):
183 """Gets the current MusicBrainz disc ID
184 """
185 assert self._success
186 result = _LIB.discid_get_id(self._handle)
187 return _decode(result)
188
189 _LIB.discid_get_freedb_id.argtypes = (c_void_p, )
190 _LIB.discid_get_freedb_id.restype = c_char_p
191 def _get_freedb_id(self):
192 """Gets the current FreeDB disc ID
193 """
194 assert self._success
195 result = _LIB.discid_get_freedb_id(self._handle)
196 return _decode(result)
197
198 _LIB.discid_get_submission_url.argtypes = (c_void_p, )
199 _LIB.discid_get_submission_url.restype = c_char_p
201 """Give an URL to submit the current TOC
202 as a new Disc ID to MusicBrainz.
203 """
204 assert self._success
205 result = _LIB.discid_get_submission_url(self._handle)
206 return _decode(result)
207
208 try:
209 _LIB.discid_get_toc_string.argtypes = (c_void_p, )
210 _LIB.discid_get_toc_string.restype = c_char_p
211 except AttributeError:
212 pass
214 """The TOC suitable as value of the `toc parameter`
215 when accessing the MusicBrainz Web Service.
216 """
217 assert self._success
218 try:
219 result = _LIB.discid_get_toc_string(self._handle)
220 except AttributeError:
221 return None
222 else:
223 return _decode(result)
224
225 _LIB.discid_get_first_track_num.argtypes = (c_void_p, )
226 _LIB.discid_get_first_track_num.restype = c_int
228 """Gets the first track number
229 """
230 assert self._success
231 return _LIB.discid_get_first_track_num(self._handle)
232
233 _LIB.discid_get_last_track_num.argtypes = (c_void_p, )
234 _LIB.discid_get_last_track_num.restype = c_int
236 """Gets the last track number
237 """
238 assert self._success
239 return _LIB.discid_get_last_track_num(self._handle)
240
241 _LIB.discid_get_sectors.argtypes = (c_void_p, )
242 _LIB.discid_get_sectors.restype = c_int
243 def _get_sectors(self):
244 """Gets the total number of sectors on the disc
245 """
246 assert self._success
247 return _LIB.discid_get_sectors(self._handle)
248
249 try:
250 _LIB.discid_get_mcn.argtypes = (c_void_p, )
251 _LIB.discid_get_mcn.restype = c_char_p
252 except AttributeError:
253 pass
254 def _get_mcn(self):
255 """Gets the current Media Catalogue Number (MCN/UPC/EAN)
256 """
257 assert self._success
258 if "mcn" in self._requested_features:
259 try:
260 result = _LIB.discid_get_mcn(self._handle)
261 except AttributeError:
262 return None
263 else:
264 return _decode(result)
265 else:
266 return None
267
268
269 @property
270 def id(self):
271 """This is the MusicBrainz :musicbrainz:`Disc ID`,
272 a :obj:`unicode` or :obj:`str <python:str>` object.
273 """
274 return self._get_id()
275
276 @property
277 def freedb_id(self):
278 """This is the :musicbrainz:`FreeDB` Disc ID (without category),
279 a :obj:`unicode` or :obj:`str <python:str>` object.
280 """
281 return self._get_freedb_id()
282
283 @property
284 def submission_url(self):
285 """Disc ID / TOC Submission URL for MusicBrainz
286
287 With this url you can submit the current TOC
288 as a new MusicBrainz :musicbrainz:`Disc ID`.
289 This is a :obj:`unicode` or :obj:`str <python:str>` object.
290 """
291 url = self._get_submission_url()
292 if url is None:
293 return None
294 else:
295 # update submission url, which saves a couple of redirects
296 url = url.replace("//mm.", "//")
297 url = url.replace("/bare/cdlookup.html", "/cdtoc/attach")
298 return url
299
300 @property
301 def toc_string(self):
302 """The TOC suitable as value of the `toc parameter`
303 when accessing the MusicBrainz Web Service.
304
305 This is a :obj:`unicode` or :obj:`str <python:str>` object
306 and enables fuzzy searching when the actual Disc ID is not found.
307
308 Note that this is the unencoded value, which still contains spaces.
309
310 .. seealso:: `MusicBrainz Web Service <http://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#discid>`_
311 """
312 toc_string = self._get_toc_string()
313 if toc_string is None and self.submission_url:
314 # probably an old version of libdiscid (< 0.6.0)
315 # gather toc string from submission_url
316 match = re.search("toc=([0-9+]+)", self.submission_url)
317 if match is None:
318 raise ValueError("can't get toc string from submission url")
319 else:
320 return match.group(1).replace("+", " ")
321 else:
322 return toc_string
323
324 @property
326 """Number of the first track"""
327 return self._get_first_track_num()
328
329 @property
330 def last_track_num(self):
331 """Number of the last **audio** track"""
332 return self._get_last_track_num()
333
334 @property
335 def sectors(self):
336 """Total length in sectors"""
337 return self._get_sectors()
338
339 length = sectors
340 """This is an alias for :attr:`sectors`"""
341
342 @property
343 def seconds(self):
344 """Total length in seconds"""
345 if self.sectors is None:
346 return None
347 else:
348 return _sectors_to_seconds(self.sectors)
349
350 @property
351 def mcn(self):
352 """This is the Media Catalogue Number (MCN/UPC/EAN)
353
354 It is set after the `"mcn"` feature was requested on a read
355 and supported by the platform or :obj:`None`.
356 If set, this is a :obj:`unicode` or :obj:`str <python:str>` object.
357 """
358 return self._get_mcn()
359
360 @property
361 def tracks(self):
362 """A list of :class:`Track` objects for this Disc.
363 """
364 tracks = []
365 assert self._success
366 for number in range(self.first_track_num, self.last_track_num + 1):
367 tracks.append(Track(self, number))
368 return tracks
369
370
371 _LIB.discid_free.argtypes = (c_void_p, )
372 _LIB.discid_free.restype = None
373 def _free(self):
374 """This will free the internal allocated memory for the object.
375 """
376 _LIB.discid_free(self._handle)
377 self._handle = None
378
379 def __enter__(self):
380 """deprecated :keyword:`with` usage"""
381 return self
382
383 def __exit__(self, exc_type, exc_value, traceback):
384 """deprecated :keyword:`with` usage"""
385 pass
386
387 def __del__(self):
388 self._free()
389
390
391# vim:set shiftwidth=4 smarttab expandtab:
def _get_first_track_num(self)
Definition: disc.py:227
def seconds(self)
Definition: disc.py:343
def _get_mcn(self)
Definition: disc.py:254
def __exit__(self, exc_type, exc_value, traceback)
Definition: disc.py:383
def read(self, device=None, features=[])
Definition: disc.py:127
def first_track_num(self)
Definition: disc.py:325
def _get_sectors(self)
Definition: disc.py:243
def _get_toc_string(self)
Definition: disc.py:213
def _get_submission_url(self)
Definition: disc.py:200
def _get_id(self)
Definition: disc.py:182
def __init__(self)
Definition: disc.py:98
def _get_freedb_id(self)
Definition: disc.py:191
def mcn(self)
Definition: disc.py:351
def __str__(self)
Definition: disc.py:107
def _get_last_track_num(self)
Definition: disc.py:235
def id(self)
Definition: disc.py:270
def toc_string(self)
Definition: disc.py:301
def submission_url(self)
Definition: disc.py:284
def put(self, first, last, disc_sectors, track_offsets)
Definition: disc.py:157
def __del__(self)
Definition: disc.py:387
def _free(self)
Definition: disc.py:373
def tracks(self)
Definition: disc.py:361
def __enter__(self)
Definition: disc.py:379
def last_track_num(self)
Definition: disc.py:330
def _get_error_msg(self)
Definition: disc.py:113
def freedb_id(self)
Definition: disc.py:277
def sectors(self)
Definition: disc.py:335
def read(device=None, features=[])
Definition: disc.py:35
def put(first, last, disc_sectors, track_offsets)
Definition: disc.py:58
def _decode(byte_string)
Definition: util.py:36
def _encode(string)
Definition: util.py:25
def _sectors_to_seconds(sectors)
Definition: util.py:46