"""The `playlist` module provides a :class:`Playlist` and its subclass :class:`FilePlaylist` for manipulating lists of :class:`hooke.curve.Curve`\s. """ import copy import hashlib import os.path import xml.dom.minidom from . import curve as curve class NoteIndexList (list): """A list that keeps track of a "current" item and additional notes. :attr:`index` (i.e. "bookmark") is the index of the currently current curve. Also keep a :class:`dict` of additional information (:attr:`info`). """ def __init__(self, name=None): super(NoteIndexList, self).__init__() self.name = name self.info = {} self._index = 0 def current(self): if len(self) == 0: return None return self[self._index] def jump(self, index): if len(self) == 0: self._index = 0 else: self._index = index % len(self) def next(self): self.jump(self._index + 1) def previous(self): self.jump(self._index - 1) def filter(self, keeper_fn=lambda item:True): c = copy.deepcopy(self) for item in reversed(c): if keeper_fn(item) != True: c.remove(item) try: # attempt to maintain the same current item c._index = c.index(self.current()) except ValueError: c._index = 0 return c class Playlist (NoteIndexList): """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s. Keeps a list of :attr:`drivers` for loading curves. """ def __init__(self, drivers, name=None): super(Playlist, self).__init__(name=name) self.drivers = drivers def append_curve_by_path(self, path, info=None, identify=True): if self.path != None: path = os.path.join(self.path, path) path = os.path.normpath(path) c = curve.Curve(path, info=info) if identify == True: c.identify(self.drivers) self.append(c) return c class FilePlaylist (Playlist): version = '0.1' def __init__(self, drivers, name=None, path=None): super(FilePlaylist, self).__init__(drivers, name) self.set_path(path) self._digest = None def set_path(self, path): if not path.endswith('.hkp'): path += '.hkp' if name == None and path != None: name = os.path.basename(path) def is_saved(self): return self.digest() == self._digest def digest(self): r"""Compute the sha1 digest of the flattened playlist representation. Examples -------- >>> root_path = os.path.sep + 'path' >>> p = FilePlaylist(drivers=[], ... path=os.path.join(root_path, 'to','playlist')) >>> p.info['note'] = 'An example playlist' >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one')) >>> c.info['note'] = 'The first curve' >>> p.append(c) >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two')) >>> c.info['note'] = 'The second curve' >>> p.append(c) >>> p.digest() "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1" """ string = self.flatten() return hashlib.sha1(string).digest() def flatten(self, absolute_paths=False): """Create a string representation of the playlist. A playlist is an XML document with the following syntax:: Relative paths are interpreted relative to the location of the playlist file. Examples -------- >>> root_path = os.path.sep + 'path' >>> p = FilePlaylist(drivers=[], ... path=os.path.join(root_path, 'to','playlist')) >>> p.info['note'] = 'An example playlist' >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one')) >>> c.info['note'] = 'The first curve' >>> p.append(c) >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two')) >>> c.info['note'] = 'The second curve' >>> p.append(c) >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF """ implementation = xml.dom.minidom.getDOMImplementation() # create the document DOM object and the root element doc = implementation.createDocument(None, 'playlist', None) root = doc.documentElement root.setAttribute('version', self.version) # store playlist version root.setAttribute('index', str(self._index)) for key,value in self.info.items(): # save info variables root.setAttribute(key, str(value)) for curve in self: # save curves and their attributes curve_element = doc.createElement('curve') root.appendChild(curve_element) path = os.path.abspath(os.path.expanduser(curve.path)) if absolute_paths == False: path = os.path.relpath( path, os.path.abspath(os.path.expanduser(self.path))) curve_element.setAttribute('path', path) for key,value in curve.info.items(): curve_element.setAttribute(key, str(value)) string = doc.toprettyxml(encoding='utf-8') root.unlink() # break circular references for garbage collection return string def _from_xml_doc(self, doc): """Load a playlist from an :class:`xml.dom.minidom.Document` instance. """ root = doc.documentElement for attribute,value in root.attributes.items(): if attribute == 'version': assert value == self.version, \ 'Cannot read v%s playlist with a v%s reader' \ % (value, self.version) elif attribute == 'index': self._index = int(value) else: self.info[attribute] = value for curve_element in doc.getElementsByTagName('curve'): path = curve_element.getAttribute('path') info = dict(curve_element.attributes.items()) info.pop('path') self.append_curve_by_path(path, info, identify=False) self.jump(self._index) # ensure valid index def from_string(self, string): """Load a playlist from a string. Examples -------- >>> string = ''' ... ... ... ... ... ''' >>> p = FilePlaylist(drivers=[], ... path=os.path.join('path', 'to','playlist')) >>> p.from_string(string) >>> p._index 1 >>> p.info {u'note': u'An example playlist'} >>> for curve in p: ... print curve.path path/to/curve/one path/to/curve/two """ doc = xml.dom.minidom.parseString(string) return self._from_xml_doc(doc) def load(self, path=None): """Load a playlist from a file. """ self.set_path(path) doc = xml.dom.minidom.parse(self.path) return self._from_xml_doc(doc) def save(self, path=None): """Saves the playlist in a XML file. """ self.set_path(path) f = file(self.path, 'w') f.write(self.flatten()) f.close()