afd2ef9dc56558a7e213c6c171fb7ad8936b0ad8
[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), NoteFilterCommand(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.name = params['output playlist']
131         hooke.playlists.append(playlist)
132
133
134 # Define commands
135
136 class NextCommand (PlaylistCommand):
137     """Move playlist to the next curve.
138     """
139     def __init__(self, plugin):
140         super(NextCommand, self).__init__(
141             name='next curve', help=self.__doc__, plugin=plugin)
142
143     def _run(self, hooke, inqueue, outqueue, params):
144         self._playlist(hooke, params).next()
145
146
147 class PreviousCommand (PlaylistCommand):
148     """Move playlist to the previous curve.
149     """
150     def __init__(self, plugin):
151         super(PreviousCommand, self).__init__(
152             name='previous curve', help=self.__doc__, plugin=plugin)
153
154     def _run(self, hooke, inqueue, outqueue, params):
155         self._playlist(hooke, params).previous()
156
157
158 class JumpCommand (PlaylistCommand):
159     """Move playlist to a given curve.
160     """
161     def __init__(self, plugin):
162         super(JumpCommand, self).__init__(
163             name='jump to curve',
164             arguments=[
165                 Argument(name='index', type='int', optional=False, help="""
166 Index of target curve.
167 """.strip()),
168                 ],
169             help=self.__doc__, plugin=plugin)
170
171     def _run(self, hooke, inqueue, outqueue, params):
172         self._playlist(hooke, params).jump(params['index'])
173
174
175 class IndexCommand (PlaylistCommand):
176     """Print the index of the current curve.
177
178     The first curve has index 0.
179     """
180     def __init__(self, plugin):
181         super(IndexCommand, self).__init__(
182             name='curve index', help=self.__doc__, plugin=plugin)
183
184     def _run(self, hooke, inqueue, outqueue, params):
185         outqueue.put(self._playlist(hooke, params).index())
186
187
188 class GetCommand (PlaylistCommand):
189     """Return a :class:`hooke.playlist.Playlist`.
190     """
191     def __init__(self, plugin):
192         super(GetCommand, self).__init__(
193             name='get playlist', help=self.__doc__, plugin=plugin)
194
195     def _run(self, hooke, inqueue, outqueue, params):
196         outqueue.put(self._playlist(hooke, params))
197
198
199 class CurveListCommand (PlaylistCommand):
200     """Get the curves in a playlist.
201     """
202     def __init__(self, plugin):
203         super(CurveListCommand, self).__init__(
204             name='playlist curves', help=self.__doc__, plugin=plugin)
205
206     def _run(self, hooke, inqueue, outqueue, params):
207         outqueue.put(list(self._playlist(hooke, params)))
208
209
210 class SaveCommand (PlaylistCommand):
211     """Save a playlist.
212     """
213     def __init__(self, plugin):
214         super(SaveCommand, self).__init__(
215             name='save playlist',
216             arguments=[
217                 Argument(name='output', type='file',
218                          help="""
219 File name for the output playlist.  Defaults to overwriting the input
220 playlist.  If the playlist does not have an input file (e.g. it was
221 created from scratch with 'new playlist'), this option is required.
222 """.strip()),
223                 ],
224             help=self.__doc__, plugin=plugin)
225
226     def _run(self, hooke, inqueue, outqueue, params):
227         self._playlist(hooke, params).save(params['output'])
228
229
230 class LoadCommand (PlaylistAddingCommand):
231     """Load a playlist.
232     """
233     def __init__(self, plugin):
234         super(LoadCommand, self).__init__(
235             name='load playlist',
236             arguments=[
237                 Argument(name='input', type='file', optional=False,
238                          help="""
239 File name for the input playlist.
240 """.strip()),
241                 Argument(name='drivers', type='driver', optional=True,
242                          count=-1, callback=all_drivers_callback,
243                          help="""
244 Drivers for loading curves.
245 """.strip()),
246                 ],
247             help=self.__doc__, plugin=plugin)
248
249     def _run(self, hooke, inqueue, outqueue, params):
250         p = FilePlaylist(drivers=params['drivers'], path=params['input'])
251         p.load(hooke=hooke)
252         playlist_names = [playlist.name for playlist in hooke.playlists]
253         if p.name in playlist_names or p.name == None:
254             p.name = params['output playlist']  # HACK: override input name.  How to tell if it is callback-generated?
255         self._set_playlist(hooke, params, p)
256         outqueue.put(p)
257
258
259 class AddCommand (PlaylistCommand):
260     """Add a curve to a playlist.
261     """
262     def __init__(self, plugin):
263         super(AddCommand, self).__init__(
264             name='add curve to playlist',
265             arguments=[
266                 Argument(name='input', type='file', optional=False,
267                          help="""
268 File name for the input :class:`hooke.curve.Curve`.
269 """.strip()),
270                 Argument(name='info', type='dict', optional=True,
271                          help="""
272 Additional information for the input :class:`hooke.curve.Curve`.
273 """.strip()),
274                 ],
275             help=self.__doc__, plugin=plugin)
276
277     def _run(self, hooke, inqueue, outqueue, params):
278         self._playlist(hooke, params).append_curve_by_path(
279             params['input'], params['info'], hooke=hooke)
280
281
282 class AddGlobCommand (PlaylistCommand):
283     """Add curves to a playlist with file globbing.
284
285     Adding lots of files one at a time can be tedious.  With this
286     command you can use globs (`data/curves/*.dat`) to add curves
287     for all matching files at once.
288     """
289     def __init__(self, plugin):
290         super(AddGlobCommand, self).__init__(
291             name='glob curves to playlist',
292             arguments=[
293                 Argument(name='input', type='string', optional=False,
294                          help="""
295 File name glob for the input :class:`hooke.curve.Curve`.
296 """.strip()),
297                 Argument(name='info', type='dict', optional=True,
298                          help="""
299 Additional information for the input :class:`hooke.curve.Curve`.
300 """.strip()),
301                 ],
302             help=self.__doc__, plugin=plugin)
303
304     def _run(self, hooke, inqueue, outqueue, params):
305         p = self._playlist(hooke, params)
306         for path in sorted(glob.glob(params['input'])):
307             try:
308                 p.append_curve_by_path(path, params['info'], hooke=hooke)
309             except NotRecognized, e:
310                 log = logging.getLogger('hooke')
311                 log.warn(unicode(e))
312                 continue
313             outqueue.put(p[-1])
314
315 class RemoveCommand (PlaylistCommand):
316     """Remove a curve from a playlist.
317     """
318     def __init__(self, plugin):
319         super(RemoveCommand, self).__init__(
320             name='remove curve from playlist',
321             arguments=[
322                 Argument(name='index', type='int', optional=False, help="""
323 Index of target curve.
324 """.strip()),
325                 ],
326             help=self.__doc__, plugin=plugin)
327
328     def _run(self, hooke, inqueue, outqueue, params):
329         self._playlist(hooke, params).pop(params['index'])
330         self._playlist(hooke, params).jump(params.index())
331
332
333 class ApplyCommandStack (PlaylistCommand):
334     """Apply a :class:`~hooke.command_stack.CommandStack` to each
335     curve in a playlist.
336
337     TODO: discuss `evaluate`.
338     """
339     def __init__(self, plugin):
340         super(ApplyCommandStack, self).__init__(
341             name='apply command stack',
342             arguments=[
343                 Argument(name='commands', type='command stack', optional=False,
344                          help="""
345 Command stack to apply to each curve.
346 """.strip()),
347                 Argument(name='evaluate', type='bool', default=False,
348                          help="""
349 Evaluate the applied command stack immediately.
350 """.strip()),
351                 ],
352             help=self.__doc__, plugin=plugin)
353
354     def _run(self, hooke, inqueue, outqueue, params):
355         if len(params['commands']) == 0:
356             return
357         p = self._playlist(hooke, params)
358         if params['evaluate'] == True:
359             for curve in p.items():
360                 for command in params['commands']:
361                     curve.command_stack.execute_command(hooke, command)
362                     curve.command_stack.append(command)
363         else:
364             for curve in p:
365                 curve.command_stack.extend(params['commands'])
366                 curve.unload()  # force command stack execution on next access.
367
368
369 class FilterCommand (PlaylistAddingCommand, PlaylistCommand):
370     """Create a subset playlist via a selection function.
371
372     Removing lots of curves one at a time can be tedious.  With this
373     command you can use a function `filter` to select the curves you
374     wish to keep.
375
376     Notes
377     -----
378     There are issues with pickling functions bound to class
379     attributes, because the pickle module doesn't know where those
380     functions were originally defined (where it should point the
381     loader).  Because of this, subclasses with hard-coded filter
382     functions are encouraged to define their filter function as a
383     method of their subclass.  See, for example,
384     :meth:`NoteFilterCommand.filter`.
385     """
386     def __init__(self, plugin, name='filter playlist'):
387         super(FilterCommand, self).__init__(
388             name=name, help=self.__doc__, plugin=plugin)
389         if not hasattr(self, 'filter'):
390             self.arguments.append(
391                 Argument(name='filter', type='function', optional=False,
392                          help="""
393 Function returning `True` for "good" curves.
394 `filter(curve, hooke, inqueue, outqueue, params) -> True/False`.
395 """.strip()))
396
397     def _run(self, hooke, inqueue, outqueue, params):
398         if not hasattr(self, 'filter'):
399             filter_fn = params['filter']
400         else:
401             filter_fn = self.filter
402         p = self._playlist(hooke, params).filter(filter_fn,
403             hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params)
404         p.name = params['name']
405         if hasattr(p, 'path') and p.path != None:
406             p.set_path(os.path.join(os.path.dirname(p.path), p.name))
407         self._set_playlist(hooke, params, p)
408         outqueue.put(p)
409
410
411 class NoteFilterCommand (FilterCommand):
412     """Create a subset playlist of curves with `.info['note'] != None`.
413     """
414     def __init__(self, plugin):
415         super(NoteFilterCommand, self).__init__(
416             plugin, name='note filter playlist')
417
418     def filter(self, curve, hooke, inqueue, outqueue, params):
419         return 'note' in curve.info and curve.info['note'] != None