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 
21 import re
22 from ctypes import c_int, c_void_p, c_char_p, c_uint
23 
24 from discid.libdiscid import _LIB, FEATURES
25 from discid.util import _encode, _decode, _sectors_to_seconds
26 from 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 
33 FEATURES_IMPLEMENTED = list(_FEATURE_MAPPING.keys())
34 
35 def 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 
58 def 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 
80 class DiscError(IOError):
81  """:func:`read` will raise this exception when an error occured.
82  """
83  pass
84 
85 class TOCError(Exception):
86  """:func:`put` will raise this exception when illegal paramaters
87  are provided.
88  """
89  pass
90 
91 
92 class 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
213  def _get_toc_string(self):
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
325  def first_track_num(self):
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 __enter__(self)
Definition: disc.py:379
def _get_freedb_id(self)
Definition: disc.py:191
def sectors(self)
Definition: disc.py:335
def seconds(self)
Definition: disc.py:343
def _decode(byte_string)
Definition: discid/util.py:36
def _encode(string)
Definition: discid/util.py:25
def __init__(self)
Definition: disc.py:98
def __str__(self)
Definition: disc.py:107
def __exit__(self, exc_type, exc_value, traceback)
Definition: disc.py:383
def _get_error_msg(self)
Definition: disc.py:113
def id(self)
Definition: disc.py:270
def read(device=None, features=[])
Definition: disc.py:35
def put(self, first, last, disc_sectors, track_offsets)
Definition: disc.py:157
def submission_url(self)
Definition: disc.py:284
def _get_id(self)
Definition: disc.py:182
def toc_string(self)
Definition: disc.py:301
def _get_toc_string(self)
Definition: disc.py:213
def tracks(self)
Definition: disc.py:361
def put(first, last, disc_sectors, track_offsets)
Definition: disc.py:58
def _get_mcn(self)
Definition: disc.py:254
def _free(self)
Definition: disc.py:373
def __del__(self)
Definition: disc.py:387
def freedb_id(self)
Definition: disc.py:277
def last_track_num(self)
Definition: disc.py:330
def _get_first_track_num(self)
Definition: disc.py:227
def _get_sectors(self)
Definition: disc.py:243
def first_track_num(self)
Definition: disc.py:325
def _get_submission_url(self)
Definition: disc.py:200
def _sectors_to_seconds(sectors)
Definition: discid/util.py:46
def _get_last_track_num(self)
Definition: disc.py:235
def mcn(self)
Definition: disc.py:351
def read(self, device=None, features=[])
Definition: disc.py:127