~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to lib/frontends/cli/interpreter.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
import cmd
 
30
import threading
 
31
import time
 
32
import Queue
 
33
 
 
34
from miro import app
 
35
from miro import dialogs
 
36
from miro import eventloop
 
37
from miro import item
 
38
from miro import folder
 
39
from miro import util
 
40
from miro import tabs
 
41
from miro.frontends.cli import clidialog
 
42
from miro.plat import resources
 
43
 
 
44
def run_in_event_loop(func):
 
45
    def decorated(*args, **kwargs):
 
46
        return_hack = []
 
47
        event = threading.Event()
 
48
        def runThenSet():
 
49
            try:
 
50
                return_hack.append(func(*args, **kwargs))
 
51
            finally:
 
52
                event.set()
 
53
        eventloop.add_urgent_call(runThenSet, 'run in event loop')
 
54
        event.wait()
 
55
        if return_hack:
 
56
            return return_hack[0]
 
57
    decorated.__doc__ = func.__doc__
 
58
    return decorated
 
59
 
 
60
class FakeTab:
 
61
    def __init__(self, tab_type, tab_template_base):
 
62
        self.type = tab_type
 
63
        self.tab_template_base = tab_template_base
 
64
 
 
65
class MiroInterpreter(cmd.Cmd):
 
66
    def __init__(self):
 
67
        cmd.Cmd.__init__(self)
 
68
        self.quit_flag = False
 
69
        self.tab = None
 
70
        self.init_database_objects()
 
71
 
 
72
    def emptyline(self):
 
73
        print "Type \"help\" for help."
 
74
 
 
75
    @run_in_event_loop
 
76
    def init_database_objects(self):
 
77
        self.video_feed_tabs = tabs.TabOrder.video_feed_order()
 
78
        self.audio_feed_tabs = tabs.TabOrder.audio_feed_order()
 
79
        self.playlist_tabs = tabs.TabOrder.playlist_order()
 
80
        self.tab_changed()
 
81
 
 
82
    def tab_changed(self):
 
83
        """Calculate the current prompt.  This method access database objects,
 
84
        so it should only be called from the backend event loop
 
85
        """
 
86
        if self.tab is None:
 
87
            self.prompt = "> "
 
88
            self.selection_type = None
 
89
 
 
90
        elif self.tab.type == 'feed':
 
91
            if isinstance(self.tab, folder.ChannelFolder):
 
92
                self.prompt = "channel folder: %s > " % self.tab.get_title()
 
93
                self.selection_type = 'channel-folder'
 
94
            else:
 
95
                self.prompt = "channel: %s > " % self.tab.get_title()
 
96
                self.selection_type = 'feed'
 
97
 
 
98
        elif self.tab.type == 'playlist':
 
99
            self.prompt = "playlist: %s > " % self.tab.get_title()
 
100
            self.selection_type = 'playlist'
 
101
 
 
102
        elif (self.tab.type == 'statictab' and
 
103
              self.tab.tab_template_base == 'downloadtab'):
 
104
            self.prompt = "downloads > "
 
105
            self.selection_type = 'downloads'
 
106
        else:
 
107
            raise ValueError("Unknown tab type")
 
108
 
 
109
    def postcmd(self, stop, line):
 
110
        # HACK
 
111
        # If the last command results in a dialog, give it a little time to
 
112
        # pop up
 
113
        time.sleep(0.1)
 
114
        while True:
 
115
            try:
 
116
                dialog = app.cli_events.dialog_queue.get_nowait()
 
117
            except Queue.Empty:
 
118
                break
 
119
            clidialog.handle_dialog(dialog)
 
120
 
 
121
        return self.quit_flag
 
122
 
 
123
    def do_help(self, line):
 
124
        """help -- Lists commands and help."""
 
125
        commands = [m for m in dir(self) if m.startswith("do_")]
 
126
        for mem in commands:
 
127
            docstring = getattr(self, mem).__doc__
 
128
            print "  ", docstring.strip()
 
129
 
 
130
    def do_quit(self, line):
 
131
        """quit -- Quits Miro cli."""
 
132
        self.quit_flag = True
 
133
 
 
134
    @run_in_event_loop
 
135
    def do_feed(self, line):
 
136
        """feed <name> -- Selects a feed by name."""
 
137
        for tab in self.video_feed_tabs.get_all_tabs():
 
138
            if tab.get_title() == line:
 
139
                self.tab = tab
 
140
                self.tab.type = "feed"
 
141
                self.tab_changed()
 
142
                return
 
143
        for tab in self.audio_feed_tabs.get_all_tabs():
 
144
            if tab.get_title() == line:
 
145
                self.tab = tab
 
146
                self.tab.type = "feed"
 
147
                self.tab_changed()
 
148
                return
 
149
        print "Error: %s not found." % line
 
150
 
 
151
    @run_in_event_loop
 
152
    def do_rmfeed(self, line):
 
153
        """rmfeed <name> -- Deletes a feed."""
 
154
        for tab in self.video_feed_tabs.get_all_tabs():
 
155
            if tab.get_title() == line:
 
156
                tab.remove()
 
157
                return
 
158
        for tab in self.audio_feed_tabs.get_all_tabs():
 
159
            if tab.get_title() == line:
 
160
                tab.remove()
 
161
                return
 
162
        print "Error: %s not found." % line
 
163
 
 
164
    @run_in_event_loop
 
165
    def complete_feed(self, text, line, begidx, endidx):
 
166
        return self.handle_tab_complete(text, list(self.video_feed_tabs.get_all_tabs()) + list(self.audio_feed_tabs.get_all_tabs()))
 
167
 
 
168
    @run_in_event_loop
 
169
    def complete_rmfeed(self, text, line, begidx, endidx):
 
170
        return self.handle_tab_complete(text, list(self.video_feed_tabs.get_all_tabs()) + list(self.audio_feed_tabs.get_all_tabs()))
 
171
 
 
172
    @run_in_event_loop
 
173
    def complete_playlist(self, text, line, begidx, endidx):
 
174
        return self.handle_tab_complete(text, self.playlist_tabs.get_all_tabs())
 
175
 
 
176
    def handle_tab_complete(self, text, view_items):
 
177
        text = text.lower()
 
178
        matches = []
 
179
        for tab in view_items:
 
180
            if tab.get_title().lower().startswith(text):
 
181
                matches.append(tab.get_title())
 
182
        return matches
 
183
 
 
184
    def handle_item_complete(self, text, view, filterFunc=lambda i: True):
 
185
        text = text.lower()
 
186
        matches = []
 
187
        for item in view:
 
188
            if (item.get_title().lower().startswith(text) and
 
189
                    filterFunc(item)):
 
190
                matches.append(item.get_title())
 
191
        return matches
 
192
 
 
193
    def _print_feeds(self, feeds):
 
194
        current_folder = None
 
195
        for tab in feeds:
 
196
            if isinstance(tab, folder.ChannelFolder):
 
197
                current_folder = tab
 
198
            elif tab.get_folder() is not current_folder:
 
199
                current_folder = None
 
200
            if current_folder is None:
 
201
                print " * " + tab.get_title()
 
202
            elif current_folder is tab:
 
203
                print " * [Folder] %s" % tab.get_title()
 
204
            else:
 
205
                print " * - %s" % tab.get_title()
 
206
 
 
207
    @run_in_event_loop
 
208
    def do_feeds(self, line):
 
209
        """feeds -- Lists all feeds."""
 
210
        print "VIDEO FEEDS"
 
211
        self._print_feeds(self.video_feed_tabs.get_all_tabs())
 
212
        print "AUDIO FEEDS"
 
213
        self._print_feeds(self.audio_feed_tabs.get_all_tabs())
 
214
 
 
215
    @run_in_event_loop
 
216
    def do_play(self, line):
 
217
        """play <name> -- Plays an item by name in an external player."""
 
218
        if self.selection_type is None:
 
219
            print "Error: No feed/playlist selected."
 
220
            return
 
221
        item = self._find_item(line)
 
222
        if item is None:
 
223
            print "No item named %r" % line
 
224
            return
 
225
        if item.is_downloaded():
 
226
            resources.open_file(item.get_video_filename())
 
227
        else:
 
228
            print '%s is not downloaded' % item.get_title()
 
229
 
 
230
    @run_in_event_loop
 
231
    def do_playlists(self, line):
 
232
        """playlists -- Lists all playlists."""
 
233
        for tab in self.playlistTabs.getView():
 
234
            print tab.obj.get_title()
 
235
 
 
236
    @run_in_event_loop
 
237
    def do_playlist(self, line):
 
238
        """playlist <name> -- Selects a playlist."""
 
239
        for tab in self.playlistTabs.getView():
 
240
            if tab.obj.get_title() == line:
 
241
                self.tab = tab
 
242
                self.tab_changed()
 
243
                return
 
244
        print "Error: %s not found." % line
 
245
 
 
246
    @run_in_event_loop
 
247
    def do_items(self, line):
 
248
        """items -- Lists the items in the feed/playlist/tab selected."""
 
249
        if self.selection_type is None:
 
250
            print "Error: No tab/feed/playlist selected."
 
251
            return
 
252
        elif self.selection_type == 'feed':
 
253
            feed = self.tab
 
254
            view = feed.items
 
255
            self.printout_item_list(view)
 
256
        elif self.selection_type == 'playlist':
 
257
            playlist = self.tab.obj
 
258
            self.printout_item_list(playlist.getView())
 
259
        elif self.selection_type == 'downloads':
 
260
            self.printout_item_list(item.Item.downloading_view(),
 
261
                                    item.Item.paused_view())
 
262
        elif self.selection_type == 'channel-folder':
 
263
            folder = self.tab.obj
 
264
            allItems = views.items.filterWithIndex(
 
265
                    indexes.itemsByChannelFolder, folder)
 
266
            allItemsSorted = allItems.sort(folder.itemSort.sort)
 
267
            self.printout_item_list(allItemsSorted)
 
268
            allItemsSorted.unlink()
 
269
        else:
 
270
            raise ValueError("Unknown tab type")
 
271
 
 
272
    @run_in_event_loop
 
273
    def do_downloads(self, line):
 
274
        """downloads -- Selects the downloads tab."""
 
275
        self.tab = FakeTab("statictab", "downloadtab")
 
276
        self.tab_changed()
 
277
 
 
278
    def printout_item_list(self, *views):
 
279
        totalItems = 0
 
280
        for view in views:
 
281
            totalItems += view.count()
 
282
        if totalItems > 0:
 
283
            print "%-20s %-10s %s" % ("State", "Size", "Name")
 
284
            print "-" * 70
 
285
            for view in views:
 
286
                for item in view:
 
287
                    state = item.get_state()
 
288
                    if state == 'downloading':
 
289
                        state += ' (%0.0f%%)' % item.download_progress()
 
290
                    print "%-20s %-10s %s" % (state, item.get_size_for_display(),
 
291
                            item.get_title())
 
292
            print
 
293
        else:
 
294
            print "No items"
 
295
 
 
296
    def _get_item_view(self):
 
297
        if self.selection_type == 'feed':
 
298
            return item.Item.visible_feed_view(self.tab.id)
 
299
        elif self.selection_type == 'playlist':
 
300
            return item.Item.playlist_view(self.tab.id)
 
301
        elif self.selection_type == 'downloads':
 
302
            return item.Item.downloading_view()
 
303
        elif self.selection_type == 'channel-folder':
 
304
            folder = self.tab
 
305
            return item.Item.visible_folder_view(folder.id)
 
306
        else:
 
307
            raise ValueError("Unknown selection type")
 
308
 
 
309
    def _find_item(self, line):
 
310
        line = line.lower()
 
311
        for item in self._get_item_view():
 
312
            if item.get_title().lower() == line:
 
313
                return item
 
314
 
 
315
    @run_in_event_loop
 
316
    def do_stop(self, line):
 
317
        """stop <name> -- Stops download by name."""
 
318
        if self.selection_type is None:
 
319
            print "Error: No feed/playlist selected."
 
320
            return
 
321
        item = self._find_item(line)
 
322
        if item is None:
 
323
            print "No item named %r" % line
 
324
            return
 
325
        if item.get_state() in ('downloading', 'paused'):
 
326
            item.expire()
 
327
        else:
 
328
            print '%s is not being downloaded' % item.get_title()
 
329
 
 
330
    @run_in_event_loop
 
331
    def complete_stop(self, text, line, begidx, endidx):
 
332
        return self.handle_item_complete(text, self._get_item_view(),
 
333
                lambda i: i.get_state() in ('downloading', 'paused'))
 
334
 
 
335
    @run_in_event_loop
 
336
    def do_download(self, line):
 
337
        """download <name> -- Downloads an item by name in the feed/playlist selected."""
 
338
        if self.selection_type is None:
 
339
            print "Error: No feed/playlist selected."
 
340
            return
 
341
        item = self._find_item(line)
 
342
        if item is None:
 
343
            print "No item named %r" % line
 
344
            return
 
345
        if item.get_state() == 'downloading':
 
346
            print '%s is currently being downloaded' % item.get_title()
 
347
        elif item.is_downloaded():
 
348
            print '%s is already downloaded' % item.get_title()
 
349
        else:
 
350
            item.download()
 
351
 
 
352
    @run_in_event_loop
 
353
    def complete_download(self, text, line, begidx, endidx):
 
354
        return self.handle_item_complete(text, self._get_item_view(),
 
355
                lambda i: i.is_downloadable())
 
356
 
 
357
    @run_in_event_loop
 
358
    def do_pause(self, line):
 
359
        """pause <name> -- Pauses a download by name."""
 
360
        if self.selection_type is None:
 
361
            print "Error: No feed/playlist selected."
 
362
            return
 
363
        item = self._find_item(line)
 
364
        if item is None:
 
365
            print "No item named %r" % line
 
366
            return
 
367
        if item.get_state() == 'downloading':
 
368
            item.pause()
 
369
        else:
 
370
            print '%s is not being downloaded' % item.get_title()
 
371
 
 
372
    @run_in_event_loop
 
373
    def complete_pause(self, text, line, begidx, endidx):
 
374
        return self.handle_item_complete(text, self._get_item_view(),
 
375
                lambda i: i.get_state() == 'downloading')
 
376
 
 
377
    @run_in_event_loop
 
378
    def do_resume(self, line):
 
379
        """resume <name> -- Resumes a download by name."""
 
380
        if self.selection_type is None:
 
381
            print "Error: No feed/playlist selected."
 
382
            return
 
383
        item = self._find_item(line)
 
384
        if item is None:
 
385
            print "No item named %r" % line
 
386
            return
 
387
        if item.get_state() == 'paused':
 
388
            item.resume()
 
389
        else:
 
390
            print '%s is not a paused download' % item.get_title()
 
391
 
 
392
    @run_in_event_loop
 
393
    def complete_resume(self, text, line, begidx, endidx):
 
394
        return self.handle_item_complete(text, self._get_item_view(),
 
395
                lambda i: i.get_state() == 'paused')
 
396
 
 
397
    @run_in_event_loop
 
398
    def do_rm(self, line):
 
399
        """rm <name> -- Removes an item by name in the feed/playlist selected."""
 
400
        if self.selection_type is None:
 
401
            print "Error: No feed/playlist selected."
 
402
            return
 
403
        item = self._find_item(line)
 
404
        if item is None:
 
405
            print "No item named %r" % line
 
406
            return
 
407
        if item.is_downloaded():
 
408
            item.expire()
 
409
        else:
 
410
            print '%s is not downloaded' % item.get_title()
 
411
 
 
412
    @run_in_event_loop
 
413
    def complete_rm(self, text, line, begidx, endidx):
 
414
        return self.handle_item_complete(text, self._get_item_view(),
 
415
                lambda i: i.is_downloaded())
 
416
 
 
417
    @run_in_event_loop
 
418
    def do_testdialog(self, line):
 
419
        """testdialog -- Tests the cli dialog system."""
 
420
        d = dialogs.ChoiceDialog("Hello", "I am a test dialog",
 
421
                dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL)
 
422
        def callback(dialog):
 
423
            print "TEST CHOICE: %s" % dialog.choice
 
424
        d.run(callback)
 
425
 
 
426
    @run_in_event_loop
 
427
    def do_dumpdatabase(self, line):
 
428
        """dumpdatabase -- Dumps the database."""
 
429
        from miro import database
 
430
        print "Dumping database...."
 
431
        database.defaultDatabase.liveStorage.dumpDatabase(database.defaultDatabase)
 
432
        print "Done."