1 """Defines :class:`PlaylistPlugin` several associated
2 :class:`hooke.plugin.Command`\s.
12 from .. import curve as curve
13 from ..plugin import Plugin, Command, Argument
15 class PlaylistPlugin (Plugin):
17 super(PlaylistPlugin, self).__init__(name='playlist')
20 return [NextCommand(), PreviousCommand(), JumpCommand(),
21 SaveCommand(), LoadCommand(),
22 AddCommand(), RemoveCommand(), FilterCommand()]
24 class Playlist (list):
25 """A list of :class:`hooke.curve.Curve`\s.
27 Keeps a list of :attr:`drivers` for loading curves, the
28 :attr:`index` (i.e. "bookmark") of the currently active curve, and
29 a :class:`dict` of additional informtion (:attr:`info`).
31 def __init__(self, drivers, name=None):
32 super(Playlist, self).__init__()
33 self.drivers = drivers
38 def append_curve_by_path(self, path, info=None, identify=True):
40 path = os.path.join(self.path, path)
41 path = os.path.normpath(path)
42 c = curve.Curve(path, info=info)
44 c.identify(self.drivers)
48 def active_curve(self):
49 return self[self._index]
54 def jump(self, index):
58 self._index = index % len(self)
61 self.jump(self._index + 1)
64 self.jump(self._index - 1)
66 def filter(self, keeper_fn=lambda curve:True):
67 playlist = copy.deepcopy(self)
68 for curve in reversed(playlist.curves):
69 if keeper_fn(curve) != True:
70 playlist.curves.remove(curve)
71 try: # attempt to maintain the same active curve
72 playlist._index = playlist.index(self.active_curve())
77 class FilePlaylist (Playlist):
80 def __init__(self, drivers, name=None, path=None):
81 if name == None and path != None:
82 name = os.path.basename(path)
83 super(FilePlaylist, self).__init__(drivers, name)
88 return self.digest() == self._digest
91 r"""Compute the sha1 digest of the flattened playlist
97 >>> root_path = os.path.sep + 'path'
98 >>> p = FilePlaylist(drivers=[],
99 ... path=os.path.join(root_path, 'to','playlist'))
100 >>> p.info['note'] = 'An example playlist'
101 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
102 >>> c.info['note'] = 'The first curve'
104 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
105 >>> c.info['note'] = 'The second curve'
108 "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
110 string = self.flatten()
111 return hashlib.sha1(string).digest()
113 def flatten(self, absolute_paths=False):
114 """Create a string representation of the playlist.
116 A playlist is an XML document with the following syntax::
118 <?xml version="1.0" encoding="utf-8"?>
119 <playlist attribute="value">
120 <curve path="/my/file/path/"/ attribute="value" ...>
124 Relative paths are interpreted relative to the location of the
130 >>> root_path = os.path.sep + 'path'
131 >>> p = FilePlaylist(drivers=[],
132 ... path=os.path.join(root_path, 'to','playlist'))
133 >>> p.info['note'] = 'An example playlist'
134 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
135 >>> c.info['note'] = 'The first curve'
137 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
138 >>> c.info['note'] = 'The second curve'
140 >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
141 <?xml version="1.0" encoding="utf-8"?>
142 <playlist index="0" note="An example playlist" version="0.1">
143 <curve note="The first curve" path="../curve/one"/>
144 <curve note="The second curve" path="../curve/two"/>
147 >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
148 <?xml version="1.0" encoding="utf-8"?>
149 <playlist index="0" note="An example playlist" version="0.1">
150 <curve note="The first curve" path="/path/to/curve/one"/>
151 <curve note="The second curve" path="/path/to/curve/two"/>
155 implementation = xml.dom.minidom.getDOMImplementation()
156 # create the document DOM object and the root element
157 doc = implementation.createDocument(None, 'playlist', None)
158 root = doc.documentElement
159 root.setAttribute('version', self.version) # store playlist version
160 root.setAttribute('index', str(self._index))
161 for key,value in self.info.items(): # save info variables
162 root.setAttribute(key, str(value))
163 for curve in self: # save curves and their attributes
164 curve_element = doc.createElement('curve')
165 root.appendChild(curve_element)
166 path = os.path.abspath(os.path.expanduser(curve.path))
167 if absolute_paths == False:
168 path = os.path.relpath(
170 os.path.abspath(os.path.expanduser(self.path)))
171 curve_element.setAttribute('path', path)
172 for key,value in curve.info.items():
173 curve_element.setAttribute(key, str(value))
174 string = doc.toprettyxml(encoding='utf-8')
175 root.unlink() # break circular references for garbage collection
178 def _from_xml_doc(self, doc):
179 """Load a playlist from an :class:`xml.dom.minidom.Document`
182 root = doc.documentElement
183 for attribute,value in root.attributes.items():
184 if attribute == 'version':
185 assert value == self.version, \
186 'Cannot read v%s playlist with a v%s reader' \
187 % (value, self.version)
188 elif attribute == 'index':
189 self._index = int(value)
191 self.info[attribute] = value
192 for curve_element in doc.getElementsByTagName('curve'):
193 path = curve_element.getAttribute('path')
194 info = dict(curve_element.attributes.items())
196 self.append_curve_by_path(path, info, identify=False)
197 self.jump(self._index) # ensure valid index
199 def from_string(self, string):
200 """Load a playlist from a string.
205 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
206 ... <playlist index="1" note="An example playlist" version="0.1">
207 ... <curve note="The first curve" path="../curve/one"/>
208 ... <curve note="The second curve" path="../curve/two"/>
211 >>> p = FilePlaylist(drivers=[],
212 ... path=os.path.join('path', 'to','playlist'))
213 >>> p.from_string(string)
217 {u'note': u'An example playlist'}
223 doc = xml.dom.minidom.parseString(string)
224 return self._from_xml_doc(doc)
226 def load(self, path=None):
227 """Load a playlist from a file.
231 if self.name == None:
232 self.name = os.path.basename(self.path)
233 doc = xml.dom.minidom.parse(path)
234 return self._from_xml_doc(doc)
236 def save(self, path):
237 """Saves the playlist in a XML file.
240 f.write(self.flatten())
243 class NextCommand (Command):
244 """Move playlist to the next curve.
247 super(NextCommand, self).__init__(
250 Argument(name='playlist', type='playlist', optional=False,
252 :class:``hooke.plugin.playlist.Playlist`` to act on.
257 def _run(inqueue, outqueue, params):
258 params['playlist'].next()
260 class PreviousCommand (Command):
261 """Move playlist to the previous curve.
264 super(PreviousCommand, self).__init__(
265 name='previous curve',
267 Argument(name='playlist', type='playlist', optional=False,
269 :class:``hooke.plugin.playlist.Playlist`` to act on.
274 def _run(inqueue, outqueue, params):
275 params['playlist'].previous()
277 class JumpCommand (Command):
278 """Move playlist to a given curve.
281 super(JumpCommand, self).__init__(
282 name='jump to curve',
284 Argument(name='playlist', type='playlist', optional=False,
286 :class:``hooke.plugin.playlist.Playlist`` to act on.
288 Argument(name='index', type='int', optional=False, help="""
289 Index of target curve.
294 def _run(inqueue, outqueue, params):
295 params['playlist'].jump(params['index'])
297 class SaveCommand (Command):
301 super(SaveCommand, self).__init__(
302 name='save playlist',
304 Argument(name='playlist', type='playlist', optional=False,
306 :class:``hooke.plugin.playlist.Playlist`` to act on.
308 Argument(name='output', type='file',
310 File name for the output playlist. Defaults to overwring the input
316 def _run(inqueue, outqueue, params):
317 params['playlist'].save(params['output'])
319 class LoadCommand (Command):
323 super(LoadCommand, self).__init__(
324 name='load playlist',
326 Argument(name='input', type='file', optional=False,
328 File name for the input playlist.
330 Argument(name='digests', type='digest', optional=False,
333 Digests for loading curves.
338 def _run(inqueue, outqueue, params):
339 p = FilePlaylist(drivers=params['drivers'], path=params['input'])
343 class AddCommand (Command):
344 """Add a curve to a playlist.
347 super(AddCommand, self).__init__(
348 name='add curve to playlist',
350 Argument(name='playlist', type='playlist', optional=False,
352 :class:``hooke.plugin.playlist.Playlist`` to act on.
354 Argument(name='input', type='file', optional=False,
356 File name for the input :class:`hooke.curve.Curve`.
358 Argument(name='info', type='dict', optional=True,
360 Additional information for the input :class:`hooke.curve.Curve`.
365 def _run(inqueue, outqueue, params):
366 params['playlist'].append_curve_by_path(params['input'],
369 class RemoveCommand (Command):
370 """Remove a curve from a playlist.
373 super(RemoveCommand, self).__init__(
374 name='remove curve from playlist',
376 Argument(name='playlist', type='playlist', optional=False,
378 :class:``hooke.plugin.playlist.Playlist`` to act on.
380 Argument(name='index', type='int', optional=False, help="""
381 Index of target curve.
386 def _run(inqueue, outqueue, params):
387 params['playlist'].pop(params['index'])
388 params['playlist'].jump(params._index)
390 class FilterCommand (Command):
391 """Create a subset playlist via a selection function.
394 super(FilterCommand, self).__init__(
395 name='filter playlist',
397 Argument(name='playlist', type='playlist', optional=False,
399 :class:``hooke.plugin.playlist.Playlist`` to act on.
401 Argument(name='filter', type='function', optional=False,
403 Function returning `True` for "good" curves.
408 def _run(inqueue, outqueue, params):
409 p = params['playlist'].filter(params['filter'])