~ubuntu-branches/ubuntu/wily/remuco-server/wily

« back to all changes in this revision

Viewing changes to base/module/remuco/adapter.py

  • Committer: Bazaar Package Importer
  • Author(s): Chow Loong Jin
  • Date: 2009-03-30 00:59:36 UTC
  • Revision ID: james.westby@ubuntu.com-20090330005936-hkxki384hm0d33gj
Tags: upstream-0.8.2.1
ImportĀ upstreamĀ versionĀ 0.8.2.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# =============================================================================
 
2
#
 
3
#    Remuco - A remote control system for media players.
 
4
#    Copyright (C) 2006-2009 Oben Sonne <obensonne@googlemail.com>
 
5
#
 
6
#    This file is part of Remuco.
 
7
#
 
8
#    Remuco is free software: you can redistribute it and/or modify
 
9
#    it under the terms of the GNU General Public License as published by
 
10
#    the Free Software Foundation, either version 3 of the License, or
 
11
#    (at your option) any later version.
 
12
#
 
13
#    Remuco is distributed in the hope that it will be useful,
 
14
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
#    GNU General Public License for more details.
 
17
#
 
18
#    You should have received a copy of the GNU General Public License
 
19
#    along with Remuco.  If not, see <http://www.gnu.org/licenses/>.
 
20
#
 
21
# =============================================================================
 
22
 
 
23
import inspect
 
24
import subprocess
 
25
import urllib
 
26
import urlparse
 
27
 
 
28
import gobject
 
29
 
 
30
from remuco import art
 
31
from remuco import command
 
32
from remuco import config
 
33
from remuco import files
 
34
from remuco import log
 
35
from remuco import message
 
36
from remuco import net
 
37
from remuco import serial
 
38
 
 
39
from remuco.defs import *
 
40
from remuco.features import *
 
41
 
 
42
from remuco.data import PlayerInfo, PlayerState, Progress, ItemList, Item
 
43
from remuco.data import Control, Action, Tagging, Request
 
44
 
 
45
from remuco.manager import DummyManager
 
46
 
 
47
# =============================================================================
 
48
# media browser actions
 
49
# =============================================================================
 
50
 
 
51
class ListAction(object):
 
52
    """List related action for a client's media browser.
 
53
    
 
54
    A list action defines an action a client may apply to a list from the
 
55
    player's media library. If possible, player adapters may define list
 
56
    actions and send them to clients via PlayerAdapter.replay_mlib_request()
 
57
    Clients may then use these actions which results in a call to
 
58
    PlayerAdapter.action_mlib_list().
 
59
    
 
60
    @see: PlayerAdapter.action_mlib_list()
 
61
     
 
62
    """
 
63
    __id_counter = 0
 
64
    
 
65
    def __init__(self, label):
 
66
        """Create a new action for lists from a player's media library.
 
67
        
 
68
        @param label:
 
69
            label of the action (keep short, ideally this is just a single word
 
70
            like 'Load', ..)
 
71
        
 
72
        """
 
73
        ListAction.__id_counter -= 1
 
74
        self.__id = ListAction.__id_counter
 
75
        
 
76
        self.label = label
 
77
        self.help = None
 
78
        
 
79
    def __str__(self):
 
80
        
 
81
        return "(%d, %s)" % (self.__id, self.__label)
 
82
        
 
83
    # === property: id ===
 
84
    
 
85
    def __pget_id(self):
 
86
        """ID of the action (auto-generated, read only)"""
 
87
        return self.__id
 
88
    
 
89
    id = property(__pget_id, None, None, __pget_id.__doc__)
 
90
    
 
91
class ItemAction(object):
 
92
    """Item related action for a client's media browser.
 
93
    
 
94
    An item action defines an action a client may apply to a file from the
 
95
    local file system, to an item from the playlist, to an item from the play
 
96
    queue or to an item from the player's media library.
 
97
      
 
98
    If possible, player adapters should define item actions and send them to
 
99
    clients by setting the keyword 'file_actions' in PlayerAdapter.__init__(),
 
100
    via PlayerAdapter.reply_playlist_request(), via
 
101
    PlayerAdapter.reply_queue_request() or via
 
102
    PlayerAdapter.reply_mlib_request(). Clients may then use these actions
 
103
    which results in a call to PlayerAdapter.action_files(),
 
104
    PlayerAdapter.action_playlist_item(), PlayerAdapter.action_queue_item() or
 
105
    PlayerAdapter.action_mlib_item().
 
106
    
 
107
    @see: PlayerAdapter.action_files()
 
108
    @see: PlayerAdapter.action_playlist()
 
109
    @see: PlayerAdapter.action_queue()
 
110
    @see: PlayerAdapter.action_mlib_item() 
 
111
    
 
112
    """
 
113
    __id_counter = 0
 
114
    
 
115
    def __init__(self, label, multiple=False):
 
116
        """Create a new action for items or files.
 
117
        
 
118
        @param label:
 
119
            label of the action (keep short, ideally this is just a single word
 
120
            like 'Enqueue', 'Play', ..)
 
121
        @keyword multiple:
 
122
            if the action may be applied to multiple items/files or only to a
 
123
            single item/file
 
124
        
 
125
        """
 
126
        ItemAction.__id_counter += 1
 
127
        self.__id = ItemAction.__id_counter
 
128
        
 
129
        self.label = label
 
130
        self.help = None
 
131
        
 
132
        self.multiple = multiple
 
133
        
 
134
    def __str__(self):
 
135
        
 
136
        return "(%d, %s, %s)" % (self.id, self.label, self.__multiple)
 
137
        
 
138
    # === property: id ===
 
139
    
 
140
    def __pget_id(self):
 
141
        """ID of the action (auto-generated, read only)"""
 
142
        return self.__id
 
143
    
 
144
    id = property(__pget_id, None, None, __pget_id.__doc__)
 
145
    
 
146
# =============================================================================
 
147
# player adapter
 
148
# =============================================================================
 
149
 
 
150
class PlayerAdapter(object):
 
151
    '''Base class for Remuco player adapters.
 
152
    
 
153
    Remuco player adapters must subclass this class and override certain
 
154
    methods to implement player specific behavior. Additionally PlayerAdapter
 
155
    provides methods to interact with Remuco clients. Following is a summary
 
156
    of all relevant methods, grouped by functionality. 
 
157
    
 
158
    ===========================================================================
 
159
    Methods to extend to manage life cycle
 
160
    ===========================================================================
 
161
    
 
162
        * start()
 
163
        * stop()
 
164
    
 
165
        A PlayerAdapter can be started and stopped with start() and stop().
 
166
        The same instance of a PlayerAdapter should be startable and stoppable
 
167
        multiple times.
 
168
        
 
169
        Subclasses of PlayerAdapter may override these methods as needed but
 
170
        must always call the super class implementations too!
 
171
    
 
172
    ===========================================================================
 
173
    Methods to override to control the media player:
 
174
    ===========================================================================
 
175
    
 
176
        * ctrl_toggle_playing()
 
177
        * ctrl_toggle_repeat()
 
178
        * ctrl_toggle_shuffle()
 
179
        * ctrl_toggle_fullscreen()
 
180
        * ctrl_next()
 
181
        * ctrl_previous()
 
182
        * ctrl_seek()
 
183
        * ctrl_volume()
 
184
        * ctrl_rate()
 
185
        * ctrl_tag()
 
186
        
 
187
        * action_files()
 
188
        * action_playlist_item()
 
189
        * action_queue_item()
 
190
        * action_mlib_item()
 
191
        * action_mlib_list()
 
192
        
 
193
        Player adapters only need to implement only a *subset* of these
 
194
        methods - depending on what is possible and what makes sense.
 
195
        
 
196
        Remuco checks which methods have been overridden and uses this
 
197
        information to notify Remuco clients about capabilities of player
 
198
        adapters. 
 
199
 
 
200
    ===========================================================================
 
201
    Methods to override to provide information from the media player:
 
202
    ===========================================================================
 
203
    
 
204
        * request_playlist()
 
205
        * request_queue()
 
206
        * request_mlib()
 
207
    
 
208
        As above, only override the methods which make sense for the
 
209
        corresponding media player.
 
210
    
 
211
    ===========================================================================
 
212
    Methods to call to respond to the requests above:
 
213
    ===========================================================================
 
214
 
 
215
        * reply_playlist_request()
 
216
        * reply_queue_request()
 
217
        * reply_mlib_request()
 
218
        
 
219
    ===========================================================================
 
220
    Methods to call to synchronize media player state information with clients:
 
221
    ===========================================================================
 
222
    
 
223
        * update_playback()
 
224
        * update_repeat()
 
225
        * update_shuffle()
 
226
        * update_item()
 
227
        * update_position()
 
228
        * update_progress()
 
229
        
 
230
        These methods should be called whenever the corresponding information
 
231
        has changed in the media player (it is safe to call these methods also
 
232
        if there actually is no change, internally a change check is done
 
233
        before sending any data to clients).
 
234
        
 
235
        Subclasses of PlayerAdapter may override the method poll() to
 
236
        periodically check a player's state.
 
237
        
 
238
    ===========================================================================
 
239
    Finally some utility methods:
 
240
    ===========================================================================
 
241
    
 
242
        * find_image()
 
243
        
 
244
    '''
 
245
 
 
246
    # =========================================================================
 
247
    # constructor 
 
248
    # =========================================================================
 
249
    
 
250
    def __init__(self, name, playback_known=False, volume_known=False,
 
251
                 repeat_known=False, shuffle_known=False, progress_known=False,
 
252
                 max_rating=0, poll=2.5, file_actions=None, mime_types=None):
 
253
        """Create a new player adapter and configure its capabilities.
 
254
        
 
255
        Just does some early initializations. Real job starts with start().
 
256
        
 
257
        @param name:
 
258
            name of the media player
 
259
        @keyword playback_known:
 
260
            indicates if the player's playback state can be provided (see
 
261
            update_playback())
 
262
        @keyword volume_known:
 
263
            indicates if the player's volume can be provided (see
 
264
            update_volume())
 
265
        @keyword repeat_known:
 
266
            indicates if the player's repeat mode can be provided (see
 
267
            update_repeat())
 
268
        @keyword shuffle_known:
 
269
            indicates if the player's shuffle mode can be provided (see
 
270
            update_shuffle())
 
271
        @keyword progress_known:
 
272
            indicates if the player's playback progress can be provided (see
 
273
            update_progress())
 
274
        @keyword max_rating:
 
275
            maximum possible rating value for items
 
276
        @keyword poll:
 
277
            interval in seconds to call poll()
 
278
        @keyword file_actions:
 
279
            list of ItemAction which can be applied to files from the local
 
280
            file system (actions like play a file or append files to the
 
281
            playlist) - this keyword is only relevant if the method
 
282
            action_files() gets overridden
 
283
        @keyword mime_types:
 
284
            list of mime types specifying the files to which the actions given
 
285
            by the keyword 'file_actions' can be applied, this may be general
 
286
            types like 'audio' or 'video' but also specific types like
 
287
            'audio/mp3' or 'video/quicktime' (setting this to None means all
 
288
            mime types are supported) - this keyword is only relevant if the
 
289
            method action_files() gets overridden
 
290
        
 
291
        @attention: When overriding, call super class implementation first!
 
292
        
 
293
        """
 
294
        
 
295
        self.__name = name
 
296
        
 
297
        # init config (config inits logging)
 
298
        
 
299
        self.__config = config.Config(self.__name)
 
300
 
 
301
        # init misc fields
 
302
        
 
303
        serial.Bin.HOST_ENCODING = self.__config.encoding
 
304
        
 
305
        self.__clients = []
 
306
        
 
307
        self.__state = PlayerState()
 
308
        self.__progress = Progress()
 
309
        self.__item = Item(img_size=self.__config.image_size,
 
310
                           img_type=self.__config.image_type)
 
311
        
 
312
        flags = self.__util_calc_flags(playback_known, volume_known,
 
313
            repeat_known, shuffle_known, progress_known)
 
314
        
 
315
        self.__info = PlayerInfo(name, flags, max_rating, file_actions)
 
316
        
 
317
        self.__sync_triggers = {}
 
318
        
 
319
        self.__poll_ival = max(500, int(poll * 1000))
 
320
        self.__poll_sid = 0
 
321
        
 
322
        self.__stopped = True
 
323
        
 
324
        self.__server_bluetooth = None
 
325
        self.__server_wifi = None
 
326
        
 
327
        if self.__config.fb:
 
328
            self.__filelib = files.FileSystemLibrary(
 
329
                self.__config.fb_root_dirs, mime_types,
 
330
                use_user_dirs=self.__config.fb_xdg_user_dirs, 
 
331
                show_extensions=self.__config.fb_extensions)
 
332
        
 
333
        self.__manager = DummyManager()
 
334
        
 
335
        log.debug("init done")
 
336
    
 
337
    def start(self):
 
338
        """Start the player adapter.
 
339
        
 
340
        @attention: When overriding, call super class implementation first!
 
341
        
 
342
        """
 
343
        
 
344
        if not self.__stopped:
 
345
            log.debug("ignore start, already running")
 
346
            return
 
347
        
 
348
        self.__stopped = False
 
349
        
 
350
        # set up server
 
351
        
 
352
        if self.__config.bluetooth:
 
353
            self.__server_bluetooth = net.BluetoothServer(self.__clients,
 
354
                    self.__info, self.__handle_message, self.__config)
 
355
        else:
 
356
            self.__server_bluetooth = None
 
357
 
 
358
        if self.__config.wifi:
 
359
            self.__server_wifi = net.WifiServer(self.__clients,
 
360
                    self.__info, self.__handle_message, self.__config)
 
361
        else:
 
362
            self.__server_wifi = None
 
363
            
 
364
        # set up polling
 
365
        
 
366
        if self.__poll_ival > 0:
 
367
            log.debug("poll every %d milli seconds" % self.__poll_ival)
 
368
            self.__poll_sid = gobject.timeout_add(self.__poll_ival, self.__poll)
 
369
            
 
370
        
 
371
        log.debug("start done")
 
372
    
 
373
    def stop(self):
 
374
        """Shutdown the player adapter.
 
375
        
 
376
        Disconnects all clients and shuts down the Bluetooth and WiFi server.
 
377
        Also ignores any subsequent calls to an update or reply method (e.g.
 
378
        update_volume(), ..., reply_playlist_request(), ...). 
 
379
        
 
380
        @note: The same player adapter instance can be started again with
 
381
            start().
 
382
 
 
383
        @attention: When overriding, call super class implementation first!
 
384
        
 
385
        """
 
386
 
 
387
        if self.__stopped: return
 
388
        
 
389
        self.__stopped = True
 
390
        
 
391
        for c in self.__clients:
 
392
            c.disconnect(remove_from_list=False, send_bye_msg=True)
 
393
            
 
394
        self.__clients = []
 
395
        
 
396
        if self.__server_bluetooth is not None:
 
397
            self.__server_bluetooth.down()
 
398
            self.__server_bluetooth = None
 
399
        if self.__server_wifi is not None:
 
400
            self.__server_wifi.down()
 
401
            self.__server_wifi = None
 
402
            
 
403
        for sid in self.__sync_triggers.values():
 
404
            if sid is not None:
 
405
                gobject.source_remove(sid)
 
406
                
 
407
        self.__sync_triggers = {}
 
408
 
 
409
        if self.__poll_sid > 0:
 
410
            gobject.source_remove(self.__poll_sid)
 
411
            
 
412
        log.debug("stop done")
 
413
    
 
414
    def poll(self):
 
415
        """Does nothing by default.
 
416
        
 
417
        If player adapters override this method, it gets called periodically
 
418
        in the interval specified by the keyword 'poll' in __init__().
 
419
        
 
420
        A typical use case of this method is to detect the playback progress of
 
421
        the current item and then call update_progress(). It can also be used
 
422
        to poll any other player state information when a player does not
 
423
        provide signals for all or certain state information changes.
 
424
        
 
425
        """
 
426
        raise NotImplementedError
 
427
    
 
428
    def __poll(self):
 
429
        
 
430
        try:
 
431
            self.poll()
 
432
        except NotImplementedError:
 
433
            return False
 
434
        
 
435
        return True
 
436
    
 
437
    # =========================================================================
 
438
    # utility methods which may be useful for player adapters
 
439
    # =========================================================================
 
440
    
 
441
    def find_image(self, resource, prefer_thumbnail=False):
 
442
        """Find a local art image file related to a resource.
 
443
        
 
444
        This method first looks in the resource' folder for typical art image
 
445
        files (e.g. 'cover.png', 'front.jpg', ...). If there is no such file it
 
446
        then looks into the user's thumbnail directory (~/.thumbnails).
 
447
        
 
448
        @param resource:
 
449
            resource to find an art image for (may be a file name or URI)
 
450
        @keyword prefer_thumbnail:
 
451
            True means first search in thumbnails, False means first search in
 
452
            the resource' folder
 
453
                                   
 
454
        @return: an image file name (which can be used for update_item()) or
 
455
            None if no image file has been found or if 'resource' is not local
 
456
        
 
457
        """
 
458
        
 
459
        file = art.get_art(resource, prefer_thumbnail=prefer_thumbnail)
 
460
        log.debug("image for '%s': %s" % (resource, file))
 
461
        return file
 
462
    
 
463
    # =========================================================================
 
464
    # control interface 
 
465
    # =========================================================================
 
466
    
 
467
    def ctrl_toggle_playing(self):
 
468
        """Toggle play and pause. 
 
469
        
 
470
        @note: Override if it is possible and makes sense.
 
471
        
 
472
        """
 
473
        log.error("** BUG ** in feature handling")
 
474
    
 
475
    def ctrl_toggle_repeat(self):
 
476
        """Toggle repeat mode. 
 
477
        
 
478
        @note: Override if it is possible and makes sense.
 
479
        
 
480
        @see: update_repeat()
 
481
               
 
482
        """
 
483
        log.error("** BUG ** in feature handling")
 
484
    
 
485
    def ctrl_toggle_shuffle(self):
 
486
        """Toggle shuffle mode. 
 
487
        
 
488
        @note: Override if it is possible and makes sense.
 
489
        
 
490
        @see: update_shuffle()
 
491
               
 
492
        """
 
493
        log.error("** BUG ** in feature handling")
 
494
    
 
495
    def ctrl_toggle_fullscreen(self):
 
496
        """Toggle full screen mode. 
 
497
        
 
498
        @note: Override if it is possible and makes sense.
 
499
        
 
500
        """
 
501
        log.error("** BUG ** in feature handling")
 
502
 
 
503
    def ctrl_next(self):
 
504
        """Play the next item. 
 
505
        
 
506
        @note: Override if it is possible and makes sense.
 
507
        
 
508
        """
 
509
        log.error("** BUG ** in feature handling")
 
510
    
 
511
    def ctrl_previous(self):
 
512
        """Play the previous item. 
 
513
        
 
514
        @note: Override if it is possible and makes sense.
 
515
        
 
516
        """
 
517
        log.error("** BUG ** in feature handling")
 
518
    
 
519
    def ctrl_seek(self, direction):
 
520
        """Seek forward or backward some seconds. 
 
521
        
 
522
        The number of seconds to seek should be reasonable for the current
 
523
        item's length (if known).
 
524
        
 
525
        If the progress of the current item is known, it should get
 
526
        synchronized immediately with clients by calling update_progress().
 
527
        
 
528
        @param direction:
 
529
            * -1: seek backward 
 
530
            * +1: seek forward
 
531
        
 
532
        @note: Override if it is possible and makes sense.
 
533
        
 
534
        """
 
535
        log.error("** BUG ** in feature handling")
 
536
    
 
537
    def ctrl_rate(self, rating):
 
538
        """Rate the currently played item. 
 
539
        
 
540
        @param rating:
 
541
            rating value (int)
 
542
        
 
543
        @note: Override if it is possible and makes sense.
 
544
        
 
545
        """
 
546
        log.error("** BUG ** in feature handling")
 
547
    
 
548
    def ctrl_tag(self, id, tags):
 
549
        """Attach some tags to an item.
 
550
        
 
551
        @param id:
 
552
            ID of the item to attach the tags to
 
553
        @param tags:
 
554
            a list of tags
 
555
        
 
556
        @note: Tags does not mean ID3 tags or similar. It means the general
 
557
            idea of tags (e.g. like used at last.fm). 
 
558
 
 
559
        @note: Override if it is possible and makes sense.
 
560
               
 
561
        """
 
562
        log.error("** BUG ** in feature handling")
 
563
    
 
564
    def ctrl_volume(self, direction):
 
565
        """Adjust volume. 
 
566
        
 
567
        @param volume:
 
568
            * -1: decrease by some percent (5 is a good value)
 
569
            *  0: mute volume
 
570
            * +1: increase by some percent (5 is a good value)
 
571
        
 
572
        @note: Override if it is possible and makes sense.
 
573
               
 
574
        """
 
575
        log.error("** BUG ** in feature handling")
 
576
        
 
577
    def __ctrl_shutdown_system(self):
 
578
        
 
579
        shutdown_cmd = config.get_system_shutdown_command()
 
580
        if shutdown_cmd:
 
581
            log.debug("run shutdown command")
 
582
            try:
 
583
                subprocess.Popen(shutdown_cmd, shell=True)
 
584
            except OSError, e:
 
585
                log.warning("failed to run shutdown command (%s)", e)
 
586
                return
 
587
            self.stop()
 
588
 
 
589
    # =========================================================================
 
590
    # actions interface
 
591
    # =========================================================================
 
592
    
 
593
    def action_files(self, action_id, files, uris):
 
594
        """Do an action on one or more files.
 
595
        
 
596
        The files are specified redundantly by 'files' and 'uris' - use
 
597
        whatever fits better. If the specified action is not applicable to
 
598
        multiple files, then 'files' and 'uris' are one element lists.
 
599
         
 
600
        The files in 'files' and 'uris' may be any files from the local file
 
601
        system that have one of the mime types specified by the keyword
 
602
        'mime_types' in __init__().
 
603
        
 
604
        @param action_id:
 
605
            ID of the action to do - this specifies one of the actions passed
 
606
            previously to __init__() by the keyword 'file_actions'
 
607
        @param files:
 
608
            list of files to apply the action to (regular path names) 
 
609
        @param uris:
 
610
            list of files to apply the action to (URI notation) 
 
611
        
 
612
        @note: Override if file item actions gets passed to __init__().
 
613
        
 
614
        """
 
615
        log.error("** BUG ** action_files() not implemented")
 
616
    
 
617
    def action_playlist_item(self, action_id, positions, ids):
 
618
        """Do an action on one or more items from the playlist.
 
619
        
 
620
        The items are specified redundantly by 'positions' and 'ids' - use
 
621
        whatever fits better. If the specified action is not applicable to
 
622
        multiple items, then 'positions' and 'ids' are one element lists. 
 
623
        
 
624
        @param action_id:
 
625
            ID of the action to do - this specifies one of the actions passed
 
626
            previously to reply_playlist_request() by the keyword 'item_actions'
 
627
        @param positions:
 
628
            list of positions to apply the action to
 
629
        @param ids:
 
630
            list of IDs to apply the action to
 
631
 
 
632
        @note: Override if item actions gets passed to reply_playlist_request().
 
633
        
 
634
        """
 
635
        log.error("** BUG ** action_item() not implemented")
 
636
    
 
637
    def action_queue_item(self, action_id, positions, ids):
 
638
        """Do an action on one or more items from the play queue.
 
639
        
 
640
        The items are specified redundantly by 'positions' and 'ids' - use
 
641
        whatever fits better. If the specified action is not applicable to
 
642
        multiple items, then 'positions' and 'ids' are one element lists. 
 
643
        
 
644
        @param action_id:
 
645
            ID of the action to do - this specifies one of the actions passed
 
646
            previously to reply_queue_request() by the keyword 'item_actions'
 
647
        @param positions:
 
648
            list of positions to apply the action to 
 
649
        @param ids:
 
650
            list of IDs to apply the action to
 
651
 
 
652
        @note: Override if item actions gets passed to reply_queue_request().
 
653
        
 
654
        """
 
655
        log.error("** BUG ** action_item() not implemented")
 
656
    
 
657
    def action_mlib_item(self, action_id, path, positions, ids):
 
658
        """Do an action on one or more items from the player's media library.
 
659
        
 
660
        The items are specified redundantly by 'positions' and 'ids' - use
 
661
        whatever fits better. If the specified action is not applicable to
 
662
        multiple items, then 'positions' and 'ids' are one element lists. 
 
663
        
 
664
        @param action_id:
 
665
            ID of the action to do - this specifies one of the actions passed
 
666
            previously to reply_mlib_request() by the keyword 'item_actions'
 
667
        @param path:
 
668
            the library path that contains the items
 
669
        @param positions:
 
670
            list of positions to apply the action to 
 
671
        @param ids:
 
672
            list of IDs to apply the action to
 
673
 
 
674
        @note: Override if item actions gets passed to reply_mlib_request().
 
675
                
 
676
        """
 
677
        log.error("** BUG ** action_item() not implemented")
 
678
    
 
679
    def action_mlib_list(self, action_id, path):
 
680
        """Do an action on a list from the player's media library.
 
681
        
 
682
        @param action_id:
 
683
            ID of the action to do - this specifies one of the actions passed
 
684
            previously to reply_mlib_request() by the keyword 'list_actions'
 
685
        @param path:
 
686
            path specifying the list to apply the action to
 
687
            
 
688
        @note: Override if list actions gets passed to reply_mlib_request().
 
689
                
 
690
        """
 
691
        log.error("** BUG ** action_mlib_list() not implemented")
 
692
    
 
693
    # =========================================================================
 
694
    # request interface 
 
695
    # =========================================================================
 
696
    
 
697
    def __request_item(self, client, id):
 
698
        info = {}
 
699
        info[INFO_TITLE] = "Sorry, item request is disabled."
 
700
        self.__reply_item_request(client, "NoID", info)
 
701
        
 
702
    def request_playlist(self, client):
 
703
        """Request the content of the currently active playlist.
 
704
        
 
705
        @param client:
 
706
            the requesting client (needed for reply)
 
707
        
 
708
        @note: Override if it is possible and makes sense.
 
709
               
 
710
        @see: reply_playlist_request() for sending back the result
 
711
        
 
712
        """
 
713
        log.error("** BUG ** in feature handling")
 
714
 
 
715
    def request_queue(self, client):
 
716
        """Request the content of the play queue. 
 
717
        
 
718
        @param client:
 
719
            the requesting client (needed for reply)
 
720
        
 
721
        @note: Override if it is possible and makes sense.
 
722
               
 
723
        @see: reply_queue_request() for sending back the result
 
724
        
 
725
        """
 
726
        log.error("** BUG ** in feature handling")
 
727
 
 
728
    def request_mlib(self, client, path):
 
729
        """Request contents of a specific level from the player's media library.
 
730
        
 
731
        @param client: the requesting client (needed for reply)
 
732
        @param path: path of the requested level (string list)
 
733
        
 
734
        @note: path is a list of strings which describe a specific level in
 
735
            the player's playlist tree. If path is an empty list, the root of
 
736
            the player's library (all top level playlists) are requested.
 
737
 
 
738
            A player may have a media library structure like this:
 
739
 
 
740
               |- Radio
 
741
               |- Genres
 
742
                  |- Jazz
 
743
                  |- ...
 
744
               |- Dynamic
 
745
                  |- Never played
 
746
                  |- Played recently
 
747
                  |- ...
 
748
               |- Playlists
 
749
                  |- Party
 
750
                     |- Sue's b-day
 
751
                     |- ...
 
752
               |- ...
 
753
 
 
754
             Here possibles values for path are [ "Radio" ] or
 
755
             [ "Playlists", "Party", "Sue's b-day" ] or ...
 
756
               
 
757
        @note: Override if it is possible and makes sense.
 
758
               
 
759
        @see: reply_list_request() for sending back the result
 
760
        
 
761
        """
 
762
        log.error("** BUG ** in feature handling")
 
763
        
 
764
    # =========================================================================
 
765
    # player side synchronization
 
766
    # =========================================================================    
 
767
 
 
768
    def update_position(self, position, queue=False):
 
769
        """Set the current item's position in the playlist or queue. 
 
770
        
 
771
        @param position:
 
772
            position of the currently played item (starting at 0)
 
773
        @keyword queue:
 
774
            True if currently played item is from the queue, False if it is
 
775
            from the currently active playlist
 
776
                        
 
777
        @note: Call to synchronize player state with remote clients.
 
778
        
 
779
        """
 
780
        change = self.__state.queue != queue
 
781
        change |= self.__state.position != position
 
782
        
 
783
        if change:
 
784
            self.__state.queue = queue
 
785
            self.__state.position = position
 
786
            self.__sync_trigger(self.__sync_state)
 
787
        
 
788
    def update_playback(self, playback):
 
789
        """Set the current playback state.
 
790
        
 
791
        @param playback:
 
792
            playback mode
 
793
            
 
794
        @see: remuco.PLAYBACK_...
 
795
        
 
796
        @note: Call to synchronize player state with remote clients.
 
797
        
 
798
        """
 
799
        change = self.__state.playback != playback
 
800
        
 
801
        if change:
 
802
            self.__state.playback = playback
 
803
            self.__sync_trigger(self.__sync_state)
 
804
    
 
805
    def update_repeat(self, repeat):
 
806
        """Set the current repeat mode. 
 
807
        
 
808
        @param repeat: True means play indefinitely, False means stop after the
 
809
            last playlist item
 
810
        
 
811
        @note: Call to synchronize player state with remote clients.
 
812
        
 
813
        """
 
814
        change = self.__state.repeat != repeat
 
815
        
 
816
        if change:
 
817
            self.__state.repeat = repeat
 
818
            self.__sync_trigger(self.__sync_state)
 
819
    
 
820
    def update_shuffle(self, shuffle):
 
821
        """Set the current shuffle mode. 
 
822
        
 
823
        @param shuffle: True means play in non-linear order, False means play
 
824
            in linear order
 
825
        
 
826
        @note: Call to synchronize player state with remote clients.
 
827
        
 
828
        """
 
829
        change = self.__state.shuffle != shuffle
 
830
        
 
831
        if change:
 
832
            self.__state.shuffle = shuffle
 
833
            self.__sync_trigger(self.__sync_state)
 
834
    
 
835
    def update_volume(self, volume):
 
836
        """Set the current volume.
 
837
        
 
838
        @param volume: the volume in percent
 
839
        
 
840
        @note: Call to synchronize player state with remote clients.
 
841
        
 
842
        """
 
843
        volume = int(volume)
 
844
        change = self.__state.volume != volume
 
845
        
 
846
        if change:
 
847
            self.__state.volume = volume
 
848
            self.__sync_trigger(self.__sync_state)
 
849
    
 
850
    def update_progress(self, progress, length):
 
851
        """Set the current playback progress.
 
852
        
 
853
        @param progress:
 
854
            number of currently elapsed seconds
 
855
        @keyword length:
 
856
            item length in seconds (maximum possible progress value)
 
857
        
 
858
        @note: Call to synchronize player state with remote clients.
 
859
        
 
860
        """
 
861
        # sanitize progress (to a multiple of 5)
 
862
        length = max(0, int(length))
 
863
        progress = max(0, int(progress))
 
864
        off = progress % 5
 
865
        if off < 3:
 
866
            progress -= off
 
867
        else:
 
868
            progress += (5 - off)
 
869
        progress = min(length, progress)
 
870
        
 
871
        #diff = abs(self.__progress.progress - progress)
 
872
        
 
873
        change = self.__progress.length != length
 
874
        #change |= diff >= 5
 
875
        change |= self.__progress.progress != progress
 
876
        
 
877
        if change:
 
878
            self.__progress.progress = progress
 
879
            self.__progress.length = length
 
880
            self.__sync_trigger(self.__sync_progress)
 
881
    
 
882
    def update_item(self, id, info, img):
 
883
        """Set currently played item.
 
884
        
 
885
        @param id:
 
886
            item ID (str)
 
887
        @param info:
 
888
            meta information (dict)
 
889
        @param img:
 
890
            image / cover art (either a file name or URI or an instance of
 
891
            Image.Image)
 
892
        
 
893
        @note: Call to synchronize player state with remote clients.
 
894
 
 
895
        @see: find_image() for finding image files for an item.
 
896
        
 
897
        @see: remuco.INFO_... for keys to use for 'info'
 
898
               
 
899
        """
 
900
        
 
901
        log.debug("new item: (%s, %s %s)" % (id, info, img))
 
902
        
 
903
        change = self.__item.id != id
 
904
        change |= self.__item.info != info
 
905
        change |= self.__item.img != img
 
906
        
 
907
        if change:
 
908
            self.__item.id = id
 
909
            self.__item.info = info
 
910
            self.__item.img = img
 
911
            self.__sync_trigger(self.__sync_item)
 
912
            
 
913
    # =========================================================================
 
914
    # request replies
 
915
    # =========================================================================    
 
916
 
 
917
    def __reply_item_request(self, client, id, info):
 
918
        """Currently unused."""
 
919
        
 
920
        if self.__stopped:
 
921
            return
 
922
        
 
923
        item = Item()
 
924
        item.id = id
 
925
        item.info = info
 
926
 
 
927
        msg = net.build_message(message.REQ_ITEM, item)
 
928
        
 
929
        gobject.idle_add(self.__reply, client, msg, "item")
 
930
        
 
931
    def reply_playlist_request(self, client, ids, names, item_actions=None):
 
932
        """Send the reply to a playlist request back to the client.
 
933
        
 
934
        @param client:
 
935
            the client to reply to
 
936
        @param ids:
 
937
            IDs of the items contained in the playlist
 
938
        @param names:
 
939
            names of the items contained in the playlist
 
940
        @keyword item_actions:
 
941
            a list of ItemAction which can be applied to items in the playlist
 
942
        
 
943
        @see: request_playlist()
 
944
        
 
945
        """ 
 
946
        if self.__stopped:
 
947
            return
 
948
        
 
949
        playlist = ItemList(None, None, ids, names, item_actions, None)
 
950
        
 
951
        msg = net.build_message(message.REQ_PLAYLIST, playlist)
 
952
        
 
953
        gobject.idle_add(self.__reply, client, msg, "playlist")
 
954
    
 
955
    def reply_queue_request(self, client, ids, names, item_actions=None):
 
956
        """Send the reply to a queue request back to the client.
 
957
        
 
958
        @param client:
 
959
            the client to reply to
 
960
        @param ids:
 
961
            IDs of the items contained in the queue
 
962
        @param names:
 
963
            names of the items contained in the queue
 
964
        @keyword item_actions:
 
965
            a list of ItemAction which can be applied to items in the queue
 
966
        
 
967
        @see: request_queue()
 
968
        
 
969
        """ 
 
970
        if self.__stopped:
 
971
            return
 
972
        
 
973
        queue = ItemList(None, None, ids, names, item_actions, None)
 
974
        
 
975
        msg = net.build_message(message.REQ_QUEUE, queue)
 
976
        
 
977
        gobject.idle_add(self.__reply, client, msg, "queue")
 
978
    
 
979
    def reply_mlib_request(self, client, path, nested, ids, names,
 
980
                               item_actions=None, list_actions=None):
 
981
        """Send the reply to a media library request back to the client.
 
982
        
 
983
        @param client:
 
984
            the client to reply to
 
985
        @param path:
 
986
            path of the requested library level
 
987
        @param nested:
 
988
            names of nested lists at the requested path
 
989
        @param ids:
 
990
            IDs of the items at the requested path
 
991
        @param names:
 
992
            names of the items at the requested path
 
993
        @keyword item_actions:
 
994
            a list of ItemAction which can be applied to items at the
 
995
            requested path 
 
996
        @keyword list_actions:
 
997
            a list of ListAction which can be applied to nested lists at the
 
998
            requested path 
 
999
        
 
1000
        @see: request_mlib()
 
1001
        
 
1002
        """ 
 
1003
        if self.__stopped:
 
1004
            return
 
1005
        
 
1006
        lib = ItemList(path, nested, ids, names, item_actions, list_actions)
 
1007
        
 
1008
        msg = net.build_message(message.REQ_MLIB, lib)
 
1009
        
 
1010
        gobject.idle_add(self.__reply, client, msg, "mlib")
 
1011
        
 
1012
    def __reply_files_request(self, client, path, nested, ids, names):
 
1013
 
 
1014
        files = ItemList(path, nested, ids, names, None, None)
 
1015
        
 
1016
        msg = net.build_message(message.REQ_FILES, files)
 
1017
        
 
1018
        gobject.idle_add(self.__reply, client, msg, "files")
 
1019
        
 
1020
    def __reply(self, client, msg, name):
 
1021
 
 
1022
        log.debug("send %s reply to %s" % (name, client))
 
1023
        
 
1024
        client.send(msg)
 
1025
    
 
1026
    # =========================================================================
 
1027
    # synchronization (outbound communication)
 
1028
    # =========================================================================
 
1029
    
 
1030
    def __sync_trigger(self, sync_fn):
 
1031
        
 
1032
        if self.__stopped:
 
1033
            return
 
1034
        
 
1035
        if sync_fn in self.__sync_triggers:
 
1036
            log.debug("trigger for %s already active" % sync_fn.func_name)
 
1037
            return
 
1038
        
 
1039
        self.__sync_triggers[sync_fn] = \
 
1040
            gobject.idle_add(sync_fn, priority=gobject.PRIORITY_LOW)
 
1041
        
 
1042
    def __sync_state(self):
 
1043
        
 
1044
        msg = net.build_message(message.SYNC_STATE, self.__state)
 
1045
        
 
1046
        self.__sync(msg, self.__sync_state, "state", self.__state)
 
1047
        
 
1048
        return False
 
1049
    
 
1050
    def __sync_progress(self):
 
1051
        
 
1052
        msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
 
1053
        
 
1054
        self.__sync(msg, self.__sync_progress, "progress", self.__progress)
 
1055
        
 
1056
        return False
 
1057
    
 
1058
    def __sync_item(self):
 
1059
 
 
1060
        msg = net.build_message(message.SYNC_ITEM, self.__item)
 
1061
        
 
1062
        self.__sync(msg, self.__sync_item, "item", self.__item)
 
1063
        
 
1064
        return False
 
1065
    
 
1066
    def __sync(self, msg, sync_fn, name, data):
 
1067
        
 
1068
        del self.__sync_triggers[sync_fn]
 
1069
        
 
1070
        if msg is None:
 
1071
            return
 
1072
        
 
1073
        log.debug("broadcast new %s to clients: %s" % (name, data))
 
1074
        
 
1075
        for c in self.__clients: c.send(msg)
 
1076
        
 
1077
    # =========================================================================
 
1078
    # handling client message (inbound communication)
 
1079
    # =========================================================================
 
1080
    
 
1081
    def __handle_message(self, client, id, bindata):
 
1082
        
 
1083
        if message.is_control(id):
 
1084
 
 
1085
            log.debug("control from client %s" % client)
 
1086
 
 
1087
            self.__handle_message_control(id, bindata)
 
1088
            
 
1089
        elif message.is_action(id):
 
1090
 
 
1091
            log.debug("action from client %s" % client)
 
1092
 
 
1093
            self.__handle_message_action(id, bindata)
 
1094
            
 
1095
        elif message.is_request(id):
 
1096
            
 
1097
            log.debug("request from client %s" % client)
 
1098
 
 
1099
            self.__handle_message_request(client, id, bindata)
 
1100
            
 
1101
        elif id == message.PRIV_INITIAL_SYNC:
 
1102
            
 
1103
            msg = net.build_message(message.SYNC_STATE, self.__state)
 
1104
            client.send(msg)
 
1105
            
 
1106
            msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
 
1107
            client.send(msg)
 
1108
            
 
1109
            msg = net.build_message(message.SYNC_ITEM, self.__item)
 
1110
            client.send(msg)
 
1111
            
 
1112
        else:
 
1113
            log.error("** BUG ** unexpected message: %d" % id)
 
1114
    
 
1115
    def __handle_message_control(self, id, bindata):
 
1116
    
 
1117
        if id == message.CTRL_PLAYPAUSE:
 
1118
            
 
1119
            self.ctrl_toggle_playing()
 
1120
            
 
1121
        elif id == message.CTRL_NEXT:
 
1122
            
 
1123
            self.ctrl_next()
 
1124
            
 
1125
        elif id == message.CTRL_PREV:
 
1126
            
 
1127
            self.ctrl_previous()
 
1128
            
 
1129
        elif id == message.CTRL_SEEK:
 
1130
            
 
1131
            control = serial.unpack(Control, bindata)
 
1132
            if control is None:
 
1133
                return
 
1134
            
 
1135
            self.ctrl_seek(control.param)
 
1136
            
 
1137
        elif id == message.CTRL_VOLUME:
 
1138
            
 
1139
            control = serial.unpack(Control, bindata)
 
1140
            if control is None:
 
1141
                return
 
1142
            
 
1143
            self.ctrl_volume(control.param)
 
1144
            
 
1145
        elif id == message.CTRL_REPEAT:
 
1146
            
 
1147
            self.ctrl_toggle_repeat()
 
1148
            
 
1149
        elif id == message.CTRL_SHUFFLE:
 
1150
            
 
1151
            self.ctrl_toggle_shuffle()
 
1152
 
 
1153
        elif id == message.CTRL_RATE:
 
1154
            
 
1155
            control = serial.unpack(Control, bindata)
 
1156
            if control is None:
 
1157
                return
 
1158
            
 
1159
            self.ctrl_rate(control.param)
 
1160
            
 
1161
        elif id == message.CTRL_TAG:
 
1162
            
 
1163
            tag = serial.unpack(Tagging, bindata)
 
1164
            if tag is None:
 
1165
                return
 
1166
            
 
1167
            self.ctrl_tag(tag.id, tag.tags)
 
1168
            
 
1169
        elif id == message.CTRL_FULLSCREEN:
 
1170
            
 
1171
            self.ctrl_toggle_fullscreen()
 
1172
 
 
1173
        elif id == message.CTRL_SHUTDOWN:
 
1174
            
 
1175
            self.__ctrl_shutdown_system()
 
1176
            
 
1177
        else:
 
1178
            log.error("** BUG ** unexpected control message: %d" % id)
 
1179
            
 
1180
    def __handle_message_action(self, id, bindata):
 
1181
    
 
1182
        a = serial.unpack(Action, bindata)
 
1183
        if a is None:
 
1184
            return
 
1185
        
 
1186
        if id == message.ACT_PLAYLIST:
 
1187
            
 
1188
            self.action_playlist_item(a.id, a.positions, a.items)
 
1189
            
 
1190
        elif id == message.ACT_QUEUE:
 
1191
            
 
1192
            self.action_queue_item(a.id, a.positions, a.items)
 
1193
            
 
1194
        elif id == message.ACT_MLIB and a.id < 0:
 
1195
            
 
1196
            self.action_mlib_list(a.id, a.path)
 
1197
                
 
1198
        elif id == message.ACT_MLIB and a.id > 0:
 
1199
            
 
1200
            self.action_mlib_item(a.id, a.path, a.positions, a.items)
 
1201
                
 
1202
        elif id == message.ACT_FILES:
 
1203
        
 
1204
            uris = self.__util_files_to_uris(a.items)
 
1205
            
 
1206
            self.action_files(a.id, a.items, uris)
 
1207
        
 
1208
        else:
 
1209
            log.error("** BUG ** unexpected action message: %d" % id)
 
1210
            
 
1211
    def __handle_message_request(self, client, id, bindata):
 
1212
 
 
1213
        if id == message.REQ_ITEM:
 
1214
            
 
1215
            request = serial.unpack(Request, bindata)    
 
1216
            if request is None:
 
1217
                return
 
1218
            
 
1219
            self.__request_item(client, request.id)
 
1220
        
 
1221
        elif id == message.REQ_PLAYLIST:
 
1222
            
 
1223
            self.request_playlist(client)
 
1224
            
 
1225
        elif id == message.REQ_QUEUE:
 
1226
            
 
1227
            self.request_queue(client)
 
1228
            
 
1229
        elif id == message.REQ_MLIB:
 
1230
            
 
1231
            request = serial.unpack(Request, bindata)    
 
1232
            if request is None:
 
1233
                return
 
1234
            
 
1235
            self.request_mlib(client, request.path)
 
1236
            
 
1237
        elif id == message.REQ_FILES:
 
1238
            
 
1239
            request = serial.unpack(Request, bindata)    
 
1240
            if request is None:
 
1241
                return
 
1242
            
 
1243
            nested, ids, names = self.__filelib.get_level(request.path)
 
1244
            
 
1245
            self.__reply_files_request(client, request.path, nested, ids, names)
 
1246
            
 
1247
        else:
 
1248
            log.error("** BUG ** unexpected request message: %d" % id)
 
1249
            
 
1250
    # =========================================================================
 
1251
    # miscellaneous 
 
1252
    # =========================================================================
 
1253
    
 
1254
    def __util_files_to_uris(self, files):
 
1255
        
 
1256
        def file_to_uri(file):
 
1257
            url = urllib.pathname2url(file)
 
1258
            return urlparse.urlunparse(("file", None, url, None, None, None))
 
1259
        
 
1260
        if not files:
 
1261
            return []
 
1262
        
 
1263
        uris = []
 
1264
        for file in files:
 
1265
            uris.append(file_to_uri(file))
 
1266
            
 
1267
        return uris
 
1268
    
 
1269
    def __util_calc_flags(self, playback_known, volume_known, repeat_known,
 
1270
                          shuffle_known, progress_known):
 
1271
        """ Check player adapter capabilities.
 
1272
        
 
1273
        Most capabilities get detected by testing which methods have been
 
1274
        overridden by a subclassing player adapter.
 
1275
        
 
1276
        """ 
 
1277
        
 
1278
        def ftc(cond, feature):
 
1279
            if inspect.ismethod(cond): # check if overridden
 
1280
                enabled = cond.__module__ != __name__
 
1281
            else:
 
1282
                enabled = cond
 
1283
            if enabled:
 
1284
                return feature
 
1285
            else:
 
1286
                return 0  
 
1287
        
 
1288
        features = (
 
1289
                                           
 
1290
            # --- 'is known' features ---
 
1291
            
 
1292
            ftc(playback_known, FT_KNOWN_PLAYBACK),
 
1293
            ftc(volume_known, FT_KNOWN_VOLUME),
 
1294
            ftc(repeat_known, FT_KNOWN_REPEAT),
 
1295
            ftc(shuffle_known, FT_KNOWN_SHUFFLE),
 
1296
            ftc(progress_known, FT_KNOWN_PROGRESS),
 
1297
 
 
1298
            # --- misc control features ---
 
1299
 
 
1300
            ftc(self.ctrl_toggle_playing, FT_CTRL_PLAYBACK),
 
1301
            ftc(self.ctrl_volume, FT_CTRL_VOLUME),
 
1302
            ftc(self.ctrl_seek, FT_CTRL_SEEK),
 
1303
            ftc(self.ctrl_tag, FT_CTRL_TAG),
 
1304
            ftc(self.ctrl_rate, FT_CTRL_RATE),
 
1305
            ftc(self.ctrl_toggle_repeat, FT_CTRL_REPEAT),
 
1306
            ftc(self.ctrl_toggle_shuffle, FT_CTRL_SHUFFLE),
 
1307
            ftc(self.ctrl_next, FT_CTRL_NEXT),
 
1308
            ftc(self.ctrl_previous, FT_CTRL_PREV),
 
1309
            ftc(self.ctrl_toggle_fullscreen, FT_CTRL_FULLSCREEN),
 
1310
        
 
1311
            # --- request features ---
 
1312
 
 
1313
            ftc(self.__request_item, FT_REQ_ITEM),
 
1314
            ftc(self.request_playlist, FT_REQ_PL),
 
1315
            ftc(self.request_queue, FT_REQ_QU),
 
1316
            ftc(self.request_mlib, FT_REQ_MLIB),
 
1317
 
 
1318
            ftc(config.get_system_shutdown_command(), FT_SHUTDOWN),
 
1319
        
 
1320
        )
 
1321
        
 
1322
        flags = 0
 
1323
        
 
1324
        for feature in features:
 
1325
            flags |= feature
 
1326
             
 
1327
        log.debug("flags: %X" % flags)
 
1328
        
 
1329
        return flags
 
1330
 
 
1331
    # =========================================================================
 
1332
    # properties 
 
1333
    # =========================================================================
 
1334
    
 
1335
    # === property: clients ===
 
1336
    
 
1337
    def __pget_clients(self):
 
1338
        """A descriptive list of connected clients.
 
1339
        
 
1340
        May be useful to integrate connected clients in a media player UI.
 
1341
 
 
1342
        """ 
 
1343
        l = []
 
1344
        for c in self.__clients:
 
1345
            l.append(str(c))
 
1346
        return l
 
1347
    
 
1348
    clients = property(__pget_clients, None, None, __pget_clients.__doc__)
 
1349
 
 
1350
    # === property: config ===
 
1351
    
 
1352
    def __pget_config(self):
 
1353
        """Player adapter specific configuration (instance of Config).
 
1354
        
 
1355
        This mirrors the configuration in ~/.config/remuco/PLAYER/conf. Any
 
1356
        change to 'config' is saved immediately into the configuration file.
 
1357
        
 
1358
        """
 
1359
        return self.__config
 
1360
    
 
1361
    config = property(__pget_config, None, None, __pget_config.__doc__)
 
1362
 
 
1363
    # === property: manager ===
 
1364
    
 
1365
    def __pget_manager(self):
 
1366
        """The Manager controlling this adapter.
 
1367
        
 
1368
        This property may be used to call the method stop() on to stop and
 
1369
        completely shutdown the adapter from within an adapter. Calling
 
1370
        Manager.stop() has the same effect as if the Manager process
 
1371
        received a SIGINT or SIGTERM. 
 
1372
        
 
1373
        If this adapter is not controlled by or has not yet assigned a Manager
 
1374
        then this property refers to a dummy manager - so it is allways safe
 
1375
        to call stop() on this manager.
 
1376
        
 
1377
        @see: Manager
 
1378
        
 
1379
        """
 
1380
        return self.__manager
 
1381
    
 
1382
    def __pset_manager(self, value):
 
1383
        self.__manager = value
 
1384
    
 
1385
    manager = property(__pget_manager, __pset_manager, None,
 
1386
                       __pget_manager.__doc__)
 
1387