53fc28ae5928a2388222796a860051d0a22546ba
[hooke.git] / hooke / plugin / playlist.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """The ``playlist`` module provides :class:`PlaylistPlugin` and
20 several associated :class:`hooke.command.Command`\s for handling
21 :mod:`hooke.playlist` classes.
22 """
23
24 import glob
25 import logging
26 import os.path
27
28 from ..command import Command, Argument, Failure
29 from ..playlist import FilePlaylist
30 from ..curve import NotRecognized
31 from . import Builtin
32
33
34 class PlaylistPlugin (Builtin):
35     def __init__(self):
36         super(PlaylistPlugin, self).__init__(name='playlist')
37         self._commands = [
38             NextCommand(self), PreviousCommand(self), JumpCommand(self),
39             GetCommand(self), IndexCommand(self), CurveListCommand(self),
40             SaveCommand(self), LoadCommand(self),
41             AddCommand(self), AddGlobCommand(self),
42             RemoveCommand(self), ApplyCommandStack(self),
43             FilterCommand(self),
44             ]
45
46
47 # Define common or complicated arguments
48
49 def current_playlist_callback(hooke, command, argument, value):
50     if value != None:
51         return value
52     playlist = hooke.playlists.current()
53     if playlist == None:
54         raise Failure('No playlists loaded')
55     return playlist
56
57 PlaylistArgument = Argument(
58     name='playlist', type='playlist', callback=current_playlist_callback,
59     help="""
60 :class:`hooke.playlist.Playlist` to act on.  Defaults to the current
61 playlist.
62 """.strip())
63
64 def playlist_name_callback(hooke, command, argument, value):
65     if value != None:
66         return value
67     i = 0
68     names = [p.name for p in hooke.playlists]
69     while True:
70         name = 'playlist-%d' % i
71         if name not in names:
72             return name
73         i += 1
74
75 PlaylistNameArgument = Argument(
76     name='output playlist', type='string', optional=True,
77     callback=playlist_name_callback,
78     help="""
79 Name of the new playlist (defaults to an auto-generated name).
80 """.strip())
81
82 def all_drivers_callback(hooke, command, argument, value):
83     return hooke.drivers
84
85
86 # Define useful command subclasses
87
88 class PlaylistCommand (Command):
89     """A :class:`~hooke.command.Command` operating on a
90     :class:`~hooke.playlist.Playlist`.
91     """
92     def __init__(self, **kwargs):
93         if 'arguments' in kwargs:
94             kwargs['arguments'].insert(0, PlaylistArgument)
95         else:
96             kwargs['arguments'] = [PlaylistArgument]
97         super(PlaylistCommand, self).__init__(**kwargs)
98
99     def _playlist(self, hooke, params):
100         """Get the selected playlist.
101
102         Notes
103         -----
104         `hooke` is intended to attach the selected playlist to the
105         local hooke instance; the returned playlist should not be
106         effected by the state of `hooke`.
107         """
108         # HACK? rely on params['playlist'] being bound to the local
109         # hooke (i.e. not a copy, as you would get by passing a
110         # playlist through the queue).  Ugh.  Stupid queues.  As an
111         # alternative, we could pass lookup information through the
112         # queue...
113         return params['playlist']
114
115
116 class PlaylistAddingCommand (Command):
117     """A :class:`~hooke.command.Command` adding a
118     :class:`~hooke.playlist.Playlist`.
119     """
120     def __init__(self, **kwargs):
121         if 'arguments' in kwargs:
122             kwargs['arguments'].insert(0, PlaylistNameArgument)
123         else:
124             kwargs['arguments'] = [PlaylistNameArgument]
125         super(PlaylistAddingCommand, self).__init__(**kwargs)
126
127     def _set_playlist(self, hooke, params, playlist):
128         """Attach a new playlist.
129         """
130         playlist_names = [p.name for p in hooke.playlists]
131         if playlist.name in playlist_names or playlist.name == None:
132             playlist.name = params['output playlist']  # HACK: override input name.  How to tell if it is callback-generated?
133         hooke.playlists.append(playlist)
134
135
136 # Define commands
137
138 class NextCommand (PlaylistCommand):
139     """Move playlist to the next curve.
140     """
141     def __init__(self, plugin):
142         super(NextCommand, self).__init__(
143             name='next curve', help=self.__doc__, plugin=plugin)
144
145     def _run(self, hooke, inqueue, outqueue, params):
146         self._playlist(hooke, params).next()
147
148
149 class PreviousCommand (PlaylistCommand):
150     """Move playlist to the previous curve.
151     """
152     def __init__(self, plugin):
153         super(PreviousCommand, self).__init__(
154             name='previous curve', help=self.__doc__, plugin=plugin)
155
156     def _run(self, hooke, inqueue, outqueue, params):
157         self._playlist(hooke, params).previous()
158
159
160 class JumpCommand (PlaylistCommand):
161     """Move playlist to a given curve.
162     """
163     def __init__(self, plugin):
164         super(JumpCommand, self).__init__(
165             name='jump to curve',
166             arguments=[
167                 Argument(name='index', type='int', optional=False, help="""
168 Index of target curve.
169 """.strip()),
170                 ],
171             help=self.__doc__, plugin=plugin)
172
173     def _run(self, hooke, inqueue, outqueue, params):
174         self._playlist(hooke, params).jump(params['index'])
175
176
177 class IndexCommand (PlaylistCommand):
178     """Print the index of the current curve.
179
180     The first curve has index 0.
181     """
182     def __init__(self, plugin):
183         super(IndexCommand, self).__init__(
184             name='curve index', help=self.__doc__, plugin=plugin)
185
186     def _run(self, hooke, inqueue, outqueue, params):
187         outqueue.put(self._playlist(hooke, params).index())
188
189
190 class GetCommand (PlaylistCommand):
191     """Return a :class:`hooke.playlist.Playlist`.
192     """
193     def __init__(self, plugin):
194         super(GetCommand, self).__init__(
195             name='get playlist', help=self.__doc__, plugin=plugin)
196
197     def _run(self, hooke, inqueue, outqueue, params):
198         outqueue.put(self._playlist(hooke, params))
199
200
201 class CurveListCommand (PlaylistCommand):
202     """Get the curves in a playlist.
203     """
204     def __init__(self, plugin):
205         super(CurveListCommand, self).__init__(
206             name='playlist curves', help=self.__doc__, plugin=plugin)
207
208     def _run(self, hooke, inqueue, outqueue, params):
209         outqueue.put(list(self._playlist(hooke, params)))
210
211
212 class SaveCommand (PlaylistCommand):
213     """Save a playlist.
214     """
215     def __init__(self, plugin):
216         super(SaveCommand, self).__init__(
217             name='save playlist',
218             arguments=[
219                 Argument(name='output', type='file',
220                          help="""
221 File name for the output playlist.  Defaults to overwriting the input
222 playlist.  If the playlist does not have an input file (e.g. it was
223 created from scratch with 'new playlist'), this option is required.
224 """.strip()),
225                 ],
226             help=self.__doc__, plugin=plugin)
227
228     def _run(self, hooke, inqueue, outqueue, params):
229         self._playlist(hooke, params).save(params['output'])
230
231
232 class LoadCommand (PlaylistAddingCommand):
233     """Load a playlist.
234     """
235     def __init__(self, plugin):
236         super(LoadCommand, self).__init__(
237             name='load playlist',
238             arguments=[
239                 Argument(name='input', type='file', optional=False,
240                          help="""
241 File name for the input playlist.
242 """.strip()),
243                 Argument(name='drivers', type='driver', optional=True,
244                          count=-1, callback=all_drivers_callback,
245                          help="""
246 Drivers for loading curves.
247 """.strip()),
248                 ],
249             help=self.__doc__, plugin=plugin)
250
251     def _run(self, hooke, inqueue, outqueue, params):
252         p = FilePlaylist(drivers=params['drivers'], path=params['input'])
253         p.load(hooke=hooke)
254         self._set_playlist(hooke, params, p)
255         outqueue.put(p)
256
257
258 class AddCommand (PlaylistCommand):
259     """Add a curve to a playlist.
260     """
261     def __init__(self, plugin):
262         super(AddCommand, self).__init__(
263             name='add curve to playlist',
264             arguments=[
265                 Argument(name='input', type='file', optional=False,
266                          help="""
267 File name for the input :class:`hooke.curve.Curve`.
268 """.strip()),
269                 Argument(name='info', type='dict', optional=True,
270                          help="""
271 Additional information for the input :class:`hooke.curve.Curve`.
272 """.strip()),
273                 ],
274             help=self.__doc__, plugin=plugin)
275
276     def _run(self, hooke, inqueue, outqueue, params):
277         self._playlist(hooke, params).append_curve_by_path(
278             params['input'], params['info'], hooke=hooke)
279
280
281 class AddGlobCommand (PlaylistCommand):
282     """Add curves to a playlist with file globbing.
283
284     Adding lots of files one at a time can be tedious.  With this
285     command you can use globs (`data/curves/*.dat`) to add curves
286     for all matching files at once.
287     """
288     def __init__(self, plugin):
289         super(AddGlobCommand, self).__init__(
290             name='glob curves to playlist',
291             arguments=[
292                 Argument(name='input', type='string', optional=False,
293                          help="""
294 File name glob for the input :class:`hooke.curve.Curve`.
295 """.strip()),
296                 Argument(name='info', type='dict', optional=True,
297                          help="""
298 Additional information for the input :class:`hooke.curve.Curve`.
299 """.strip()),
300                 ],
301             help=self.__doc__, plugin=plugin)
302
303     def _run(self, hooke, inqueue, outqueue, params):
304         p = self._playlist(hooke, params)
305         for path in sorted(glob.glob(params['input'])):
306             try:
307                 p.append_curve_by_path(path, params['info'], hooke=hooke)
308             except NotRecognized, e:
309                 log = logging.getLogger('hooke')
310                 log.warn(unicode(e))
311                 continue
312             outqueue.put(p[-1])
313
314 class RemoveCommand (PlaylistCommand):
315     """Remove a curve from a playlist.
316     """
317     def __init__(self, plugin):
318         super(RemoveCommand, self).__init__(
319             name='remove curve from playlist',
320             arguments=[
321                 Argument(name='index', type='int', optional=False, help="""
322 Index of target curve.
323 """.strip()),
324                 ],
325             help=self.__doc__, plugin=plugin)
326
327     def _run(self, hooke, inqueue, outqueue, params):
328         self._playlist(hooke, params).pop(params['index'])
329         self._playlist(hooke, params).jump(params.index())
330
331
332 class ApplyCommandStack (PlaylistCommand):
333     """Apply a :class:`~hooke.command_stack.CommandStack` to each
334     curve in a playlist.
335
336     TODO: discuss `evaluate`.
337     """
338     def __init__(self, plugin):
339         super(ApplyCommandStack, self).__init__(
340             name='apply command stack',
341             arguments=[
342                 Argument(name='commands', type='command stack', optional=False,
343                          help="""
344 Command stack to apply to each curve.
345 """.strip()),
346                 Argument(name='evaluate', type='bool', default=False,
347                          help="""
348 Evaluate the applied command stack immediately.
349 """.strip()),
350                 ],
351             help=self.__doc__, plugin=plugin)
352
353     def _run(self, hooke, inqueue, outqueue, params):
354         if len(params['commands']) == 0:
355             return
356         p = self._playlist(hooke, params)
357         if params['evaluate'] == True:
358             for curve in p.items():
359                 for command in params['commands']:
360                     curve.command_stack.execute_command(hooke, command)
361                     curve.command_stack.append(command)
362         else:
363             for curve in p:
364                 curve.command_stack.extend(params['commands'])
365                 curve.unload()  # force command stack execution on next access.
366
367
368 class FilterCommand (PlaylistAddingCommand, PlaylistCommand):
369     """Create a subset playlist via a selection function.
370
371     Removing lots of curves one at a time can be tedious.  With this
372     command you can use a function `filter` to select the curves you
373     wish to keep.
374
375     Notes
376     -----
377     There are issues with pickling functions bound to class
378     attributes, because the pickle module doesn't know where those
379     functions were originally defined (where it should point the
380     loader).  Because of this, subclasses with hard-coded filter
381     functions are encouraged to define their filter function as a
382     method of their subclass.  See, for example,
383     :meth:`NoteFilterCommand.filter`.
384     """
385     def __init__(self, plugin, name='filter playlist'):
386         super(FilterCommand, self).__init__(
387             name=name, help=self.__doc__, plugin=plugin)
388         if not hasattr(self, 'filter'):
389             self.arguments.append(
390                 Argument(name='filter', type='function', optional=False,
391                          help="""
392 Function returning `True` for "good" curves.
393 `filter(curve, hooke, inqueue, outqueue, params) -> True/False`.
394 """.strip()))
395
396     def _run(self, hooke, inqueue, outqueue, params):
397         if not hasattr(self, 'filter'):
398             filter_fn = params['filter']
399         else:
400             filter_fn = self.filter
401         p = self._playlist(hooke, params).filter(filter_fn,
402             hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params)
403         self._set_playlist(hooke, params, p)
404         if hasattr(p, 'path') and p.path != None:
405             p.set_path(os.path.join(os.path.dirname(p.path), p.name))
406         outqueue.put(p)