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