1
# =============================================================================
3
# Remuco - A remote control system for media players.
4
# Copyright (C) 2006-2009 Oben Sonne <obensonne@googlemail.com>
6
# This file is part of Remuco.
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.
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.
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/>.
21
# =============================================================================
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
39
from remuco.defs import *
40
from remuco.features import *
42
from remuco.data import PlayerInfo, PlayerState, Progress, ItemList, Item
43
from remuco.data import Control, Action, Tagging, Request
45
from remuco.manager import DummyManager
47
# =============================================================================
48
# media browser actions
49
# =============================================================================
51
class ListAction(object):
52
"""List related action for a client's media browser.
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().
60
@see: PlayerAdapter.action_mlib_list()
65
def __init__(self, label):
66
"""Create a new action for lists from a player's media library.
69
label of the action (keep short, ideally this is just a single word
73
ListAction.__id_counter -= 1
74
self.__id = ListAction.__id_counter
81
return "(%d, %s)" % (self.__id, self.__label)
83
# === property: id ===
86
"""ID of the action (auto-generated, read only)"""
89
id = property(__pget_id, None, None, __pget_id.__doc__)
91
class ItemAction(object):
92
"""Item related action for a client's media browser.
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.
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().
107
@see: PlayerAdapter.action_files()
108
@see: PlayerAdapter.action_playlist()
109
@see: PlayerAdapter.action_queue()
110
@see: PlayerAdapter.action_mlib_item()
115
def __init__(self, label, multiple=False):
116
"""Create a new action for items or files.
119
label of the action (keep short, ideally this is just a single word
120
like 'Enqueue', 'Play', ..)
122
if the action may be applied to multiple items/files or only to a
126
ItemAction.__id_counter += 1
127
self.__id = ItemAction.__id_counter
132
self.multiple = multiple
136
return "(%d, %s, %s)" % (self.id, self.label, self.__multiple)
138
# === property: id ===
141
"""ID of the action (auto-generated, read only)"""
144
id = property(__pget_id, None, None, __pget_id.__doc__)
146
# =============================================================================
148
# =============================================================================
150
class PlayerAdapter(object):
151
'''Base class for Remuco player adapters.
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.
158
===========================================================================
159
Methods to extend to manage life cycle
160
===========================================================================
165
A PlayerAdapter can be started and stopped with start() and stop().
166
The same instance of a PlayerAdapter should be startable and stoppable
169
Subclasses of PlayerAdapter may override these methods as needed but
170
must always call the super class implementations too!
172
===========================================================================
173
Methods to override to control the media player:
174
===========================================================================
176
* ctrl_toggle_playing()
177
* ctrl_toggle_repeat()
178
* ctrl_toggle_shuffle()
179
* ctrl_toggle_fullscreen()
188
* action_playlist_item()
189
* action_queue_item()
193
Player adapters only need to implement only a *subset* of these
194
methods - depending on what is possible and what makes sense.
196
Remuco checks which methods have been overridden and uses this
197
information to notify Remuco clients about capabilities of player
200
===========================================================================
201
Methods to override to provide information from the media player:
202
===========================================================================
208
As above, only override the methods which make sense for the
209
corresponding media player.
211
===========================================================================
212
Methods to call to respond to the requests above:
213
===========================================================================
215
* reply_playlist_request()
216
* reply_queue_request()
217
* reply_mlib_request()
219
===========================================================================
220
Methods to call to synchronize media player state information with clients:
221
===========================================================================
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).
235
Subclasses of PlayerAdapter may override the method poll() to
236
periodically check a player's state.
238
===========================================================================
239
Finally some utility methods:
240
===========================================================================
246
# =========================================================================
248
# =========================================================================
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.
255
Just does some early initializations. Real job starts with start().
258
name of the media player
259
@keyword playback_known:
260
indicates if the player's playback state can be provided (see
262
@keyword volume_known:
263
indicates if the player's volume can be provided (see
265
@keyword repeat_known:
266
indicates if the player's repeat mode can be provided (see
268
@keyword shuffle_known:
269
indicates if the player's shuffle mode can be provided (see
271
@keyword progress_known:
272
indicates if the player's playback progress can be provided (see
275
maximum possible rating value for items
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
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
291
@attention: When overriding, call super class implementation first!
297
# init config (config inits logging)
299
self.__config = config.Config(self.__name)
303
serial.Bin.HOST_ENCODING = self.__config.encoding
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)
312
flags = self.__util_calc_flags(playback_known, volume_known,
313
repeat_known, shuffle_known, progress_known)
315
self.__info = PlayerInfo(name, flags, max_rating, file_actions)
317
self.__sync_triggers = {}
319
self.__poll_ival = max(500, int(poll * 1000))
322
self.__stopped = True
324
self.__server_bluetooth = None
325
self.__server_wifi = None
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)
333
self.__manager = DummyManager()
335
log.debug("init done")
338
"""Start the player adapter.
340
@attention: When overriding, call super class implementation first!
344
if not self.__stopped:
345
log.debug("ignore start, already running")
348
self.__stopped = False
352
if self.__config.bluetooth:
353
self.__server_bluetooth = net.BluetoothServer(self.__clients,
354
self.__info, self.__handle_message, self.__config)
356
self.__server_bluetooth = None
358
if self.__config.wifi:
359
self.__server_wifi = net.WifiServer(self.__clients,
360
self.__info, self.__handle_message, self.__config)
362
self.__server_wifi = None
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)
371
log.debug("start done")
374
"""Shutdown the player adapter.
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(), ...).
380
@note: The same player adapter instance can be started again with
383
@attention: When overriding, call super class implementation first!
387
if self.__stopped: return
389
self.__stopped = True
391
for c in self.__clients:
392
c.disconnect(remove_from_list=False, send_bye_msg=True)
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
403
for sid in self.__sync_triggers.values():
405
gobject.source_remove(sid)
407
self.__sync_triggers = {}
409
if self.__poll_sid > 0:
410
gobject.source_remove(self.__poll_sid)
412
log.debug("stop done")
415
"""Does nothing by default.
417
If player adapters override this method, it gets called periodically
418
in the interval specified by the keyword 'poll' in __init__().
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.
426
raise NotImplementedError
432
except NotImplementedError:
437
# =========================================================================
438
# utility methods which may be useful for player adapters
439
# =========================================================================
441
def find_image(self, resource, prefer_thumbnail=False):
442
"""Find a local art image file related to a resource.
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).
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
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
459
file = art.get_art(resource, prefer_thumbnail=prefer_thumbnail)
460
log.debug("image for '%s': %s" % (resource, file))
463
# =========================================================================
465
# =========================================================================
467
def ctrl_toggle_playing(self):
468
"""Toggle play and pause.
470
@note: Override if it is possible and makes sense.
473
log.error("** BUG ** in feature handling")
475
def ctrl_toggle_repeat(self):
476
"""Toggle repeat mode.
478
@note: Override if it is possible and makes sense.
480
@see: update_repeat()
483
log.error("** BUG ** in feature handling")
485
def ctrl_toggle_shuffle(self):
486
"""Toggle shuffle mode.
488
@note: Override if it is possible and makes sense.
490
@see: update_shuffle()
493
log.error("** BUG ** in feature handling")
495
def ctrl_toggle_fullscreen(self):
496
"""Toggle full screen mode.
498
@note: Override if it is possible and makes sense.
501
log.error("** BUG ** in feature handling")
504
"""Play the next item.
506
@note: Override if it is possible and makes sense.
509
log.error("** BUG ** in feature handling")
511
def ctrl_previous(self):
512
"""Play the previous item.
514
@note: Override if it is possible and makes sense.
517
log.error("** BUG ** in feature handling")
519
def ctrl_seek(self, direction):
520
"""Seek forward or backward some seconds.
522
The number of seconds to seek should be reasonable for the current
523
item's length (if known).
525
If the progress of the current item is known, it should get
526
synchronized immediately with clients by calling update_progress().
532
@note: Override if it is possible and makes sense.
535
log.error("** BUG ** in feature handling")
537
def ctrl_rate(self, rating):
538
"""Rate the currently played item.
543
@note: Override if it is possible and makes sense.
546
log.error("** BUG ** in feature handling")
548
def ctrl_tag(self, id, tags):
549
"""Attach some tags to an item.
552
ID of the item to attach the tags to
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).
559
@note: Override if it is possible and makes sense.
562
log.error("** BUG ** in feature handling")
564
def ctrl_volume(self, direction):
568
* -1: decrease by some percent (5 is a good value)
570
* +1: increase by some percent (5 is a good value)
572
@note: Override if it is possible and makes sense.
575
log.error("** BUG ** in feature handling")
577
def __ctrl_shutdown_system(self):
579
shutdown_cmd = config.get_system_shutdown_command()
581
log.debug("run shutdown command")
583
subprocess.Popen(shutdown_cmd, shell=True)
585
log.warning("failed to run shutdown command (%s)", e)
589
# =========================================================================
591
# =========================================================================
593
def action_files(self, action_id, files, uris):
594
"""Do an action on one or more files.
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.
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__().
605
ID of the action to do - this specifies one of the actions passed
606
previously to __init__() by the keyword 'file_actions'
608
list of files to apply the action to (regular path names)
610
list of files to apply the action to (URI notation)
612
@note: Override if file item actions gets passed to __init__().
615
log.error("** BUG ** action_files() not implemented")
617
def action_playlist_item(self, action_id, positions, ids):
618
"""Do an action on one or more items from the playlist.
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.
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'
628
list of positions to apply the action to
630
list of IDs to apply the action to
632
@note: Override if item actions gets passed to reply_playlist_request().
635
log.error("** BUG ** action_item() not implemented")
637
def action_queue_item(self, action_id, positions, ids):
638
"""Do an action on one or more items from the play queue.
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.
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'
648
list of positions to apply the action to
650
list of IDs to apply the action to
652
@note: Override if item actions gets passed to reply_queue_request().
655
log.error("** BUG ** action_item() not implemented")
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.
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.
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'
668
the library path that contains the items
670
list of positions to apply the action to
672
list of IDs to apply the action to
674
@note: Override if item actions gets passed to reply_mlib_request().
677
log.error("** BUG ** action_item() not implemented")
679
def action_mlib_list(self, action_id, path):
680
"""Do an action on a list from the player's media library.
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'
686
path specifying the list to apply the action to
688
@note: Override if list actions gets passed to reply_mlib_request().
691
log.error("** BUG ** action_mlib_list() not implemented")
693
# =========================================================================
695
# =========================================================================
697
def __request_item(self, client, id):
699
info[INFO_TITLE] = "Sorry, item request is disabled."
700
self.__reply_item_request(client, "NoID", info)
702
def request_playlist(self, client):
703
"""Request the content of the currently active playlist.
706
the requesting client (needed for reply)
708
@note: Override if it is possible and makes sense.
710
@see: reply_playlist_request() for sending back the result
713
log.error("** BUG ** in feature handling")
715
def request_queue(self, client):
716
"""Request the content of the play queue.
719
the requesting client (needed for reply)
721
@note: Override if it is possible and makes sense.
723
@see: reply_queue_request() for sending back the result
726
log.error("** BUG ** in feature handling")
728
def request_mlib(self, client, path):
729
"""Request contents of a specific level from the player's media library.
731
@param client: the requesting client (needed for reply)
732
@param path: path of the requested level (string list)
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.
738
A player may have a media library structure like this:
754
Here possibles values for path are [ "Radio" ] or
755
[ "Playlists", "Party", "Sue's b-day" ] or ...
757
@note: Override if it is possible and makes sense.
759
@see: reply_list_request() for sending back the result
762
log.error("** BUG ** in feature handling")
764
# =========================================================================
765
# player side synchronization
766
# =========================================================================
768
def update_position(self, position, queue=False):
769
"""Set the current item's position in the playlist or queue.
772
position of the currently played item (starting at 0)
774
True if currently played item is from the queue, False if it is
775
from the currently active playlist
777
@note: Call to synchronize player state with remote clients.
780
change = self.__state.queue != queue
781
change |= self.__state.position != position
784
self.__state.queue = queue
785
self.__state.position = position
786
self.__sync_trigger(self.__sync_state)
788
def update_playback(self, playback):
789
"""Set the current playback state.
794
@see: remuco.PLAYBACK_...
796
@note: Call to synchronize player state with remote clients.
799
change = self.__state.playback != playback
802
self.__state.playback = playback
803
self.__sync_trigger(self.__sync_state)
805
def update_repeat(self, repeat):
806
"""Set the current repeat mode.
808
@param repeat: True means play indefinitely, False means stop after the
811
@note: Call to synchronize player state with remote clients.
814
change = self.__state.repeat != repeat
817
self.__state.repeat = repeat
818
self.__sync_trigger(self.__sync_state)
820
def update_shuffle(self, shuffle):
821
"""Set the current shuffle mode.
823
@param shuffle: True means play in non-linear order, False means play
826
@note: Call to synchronize player state with remote clients.
829
change = self.__state.shuffle != shuffle
832
self.__state.shuffle = shuffle
833
self.__sync_trigger(self.__sync_state)
835
def update_volume(self, volume):
836
"""Set the current volume.
838
@param volume: the volume in percent
840
@note: Call to synchronize player state with remote clients.
844
change = self.__state.volume != volume
847
self.__state.volume = volume
848
self.__sync_trigger(self.__sync_state)
850
def update_progress(self, progress, length):
851
"""Set the current playback progress.
854
number of currently elapsed seconds
856
item length in seconds (maximum possible progress value)
858
@note: Call to synchronize player state with remote clients.
861
# sanitize progress (to a multiple of 5)
862
length = max(0, int(length))
863
progress = max(0, int(progress))
868
progress += (5 - off)
869
progress = min(length, progress)
871
#diff = abs(self.__progress.progress - progress)
873
change = self.__progress.length != length
875
change |= self.__progress.progress != progress
878
self.__progress.progress = progress
879
self.__progress.length = length
880
self.__sync_trigger(self.__sync_progress)
882
def update_item(self, id, info, img):
883
"""Set currently played item.
888
meta information (dict)
890
image / cover art (either a file name or URI or an instance of
893
@note: Call to synchronize player state with remote clients.
895
@see: find_image() for finding image files for an item.
897
@see: remuco.INFO_... for keys to use for 'info'
901
log.debug("new item: (%s, %s %s)" % (id, info, img))
903
change = self.__item.id != id
904
change |= self.__item.info != info
905
change |= self.__item.img != img
909
self.__item.info = info
910
self.__item.img = img
911
self.__sync_trigger(self.__sync_item)
913
# =========================================================================
915
# =========================================================================
917
def __reply_item_request(self, client, id, info):
918
"""Currently unused."""
927
msg = net.build_message(message.REQ_ITEM, item)
929
gobject.idle_add(self.__reply, client, msg, "item")
931
def reply_playlist_request(self, client, ids, names, item_actions=None):
932
"""Send the reply to a playlist request back to the client.
935
the client to reply to
937
IDs of the items contained in the playlist
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
943
@see: request_playlist()
949
playlist = ItemList(None, None, ids, names, item_actions, None)
951
msg = net.build_message(message.REQ_PLAYLIST, playlist)
953
gobject.idle_add(self.__reply, client, msg, "playlist")
955
def reply_queue_request(self, client, ids, names, item_actions=None):
956
"""Send the reply to a queue request back to the client.
959
the client to reply to
961
IDs of the items contained in the queue
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
967
@see: request_queue()
973
queue = ItemList(None, None, ids, names, item_actions, None)
975
msg = net.build_message(message.REQ_QUEUE, queue)
977
gobject.idle_add(self.__reply, client, msg, "queue")
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.
984
the client to reply to
986
path of the requested library level
988
names of nested lists at the requested path
990
IDs of the items at the requested path
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
996
@keyword list_actions:
997
a list of ListAction which can be applied to nested lists at the
1000
@see: request_mlib()
1006
lib = ItemList(path, nested, ids, names, item_actions, list_actions)
1008
msg = net.build_message(message.REQ_MLIB, lib)
1010
gobject.idle_add(self.__reply, client, msg, "mlib")
1012
def __reply_files_request(self, client, path, nested, ids, names):
1014
files = ItemList(path, nested, ids, names, None, None)
1016
msg = net.build_message(message.REQ_FILES, files)
1018
gobject.idle_add(self.__reply, client, msg, "files")
1020
def __reply(self, client, msg, name):
1022
log.debug("send %s reply to %s" % (name, client))
1026
# =========================================================================
1027
# synchronization (outbound communication)
1028
# =========================================================================
1030
def __sync_trigger(self, sync_fn):
1035
if sync_fn in self.__sync_triggers:
1036
log.debug("trigger for %s already active" % sync_fn.func_name)
1039
self.__sync_triggers[sync_fn] = \
1040
gobject.idle_add(sync_fn, priority=gobject.PRIORITY_LOW)
1042
def __sync_state(self):
1044
msg = net.build_message(message.SYNC_STATE, self.__state)
1046
self.__sync(msg, self.__sync_state, "state", self.__state)
1050
def __sync_progress(self):
1052
msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
1054
self.__sync(msg, self.__sync_progress, "progress", self.__progress)
1058
def __sync_item(self):
1060
msg = net.build_message(message.SYNC_ITEM, self.__item)
1062
self.__sync(msg, self.__sync_item, "item", self.__item)
1066
def __sync(self, msg, sync_fn, name, data):
1068
del self.__sync_triggers[sync_fn]
1073
log.debug("broadcast new %s to clients: %s" % (name, data))
1075
for c in self.__clients: c.send(msg)
1077
# =========================================================================
1078
# handling client message (inbound communication)
1079
# =========================================================================
1081
def __handle_message(self, client, id, bindata):
1083
if message.is_control(id):
1085
log.debug("control from client %s" % client)
1087
self.__handle_message_control(id, bindata)
1089
elif message.is_action(id):
1091
log.debug("action from client %s" % client)
1093
self.__handle_message_action(id, bindata)
1095
elif message.is_request(id):
1097
log.debug("request from client %s" % client)
1099
self.__handle_message_request(client, id, bindata)
1101
elif id == message.PRIV_INITIAL_SYNC:
1103
msg = net.build_message(message.SYNC_STATE, self.__state)
1106
msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
1109
msg = net.build_message(message.SYNC_ITEM, self.__item)
1113
log.error("** BUG ** unexpected message: %d" % id)
1115
def __handle_message_control(self, id, bindata):
1117
if id == message.CTRL_PLAYPAUSE:
1119
self.ctrl_toggle_playing()
1121
elif id == message.CTRL_NEXT:
1125
elif id == message.CTRL_PREV:
1127
self.ctrl_previous()
1129
elif id == message.CTRL_SEEK:
1131
control = serial.unpack(Control, bindata)
1135
self.ctrl_seek(control.param)
1137
elif id == message.CTRL_VOLUME:
1139
control = serial.unpack(Control, bindata)
1143
self.ctrl_volume(control.param)
1145
elif id == message.CTRL_REPEAT:
1147
self.ctrl_toggle_repeat()
1149
elif id == message.CTRL_SHUFFLE:
1151
self.ctrl_toggle_shuffle()
1153
elif id == message.CTRL_RATE:
1155
control = serial.unpack(Control, bindata)
1159
self.ctrl_rate(control.param)
1161
elif id == message.CTRL_TAG:
1163
tag = serial.unpack(Tagging, bindata)
1167
self.ctrl_tag(tag.id, tag.tags)
1169
elif id == message.CTRL_FULLSCREEN:
1171
self.ctrl_toggle_fullscreen()
1173
elif id == message.CTRL_SHUTDOWN:
1175
self.__ctrl_shutdown_system()
1178
log.error("** BUG ** unexpected control message: %d" % id)
1180
def __handle_message_action(self, id, bindata):
1182
a = serial.unpack(Action, bindata)
1186
if id == message.ACT_PLAYLIST:
1188
self.action_playlist_item(a.id, a.positions, a.items)
1190
elif id == message.ACT_QUEUE:
1192
self.action_queue_item(a.id, a.positions, a.items)
1194
elif id == message.ACT_MLIB and a.id < 0:
1196
self.action_mlib_list(a.id, a.path)
1198
elif id == message.ACT_MLIB and a.id > 0:
1200
self.action_mlib_item(a.id, a.path, a.positions, a.items)
1202
elif id == message.ACT_FILES:
1204
uris = self.__util_files_to_uris(a.items)
1206
self.action_files(a.id, a.items, uris)
1209
log.error("** BUG ** unexpected action message: %d" % id)
1211
def __handle_message_request(self, client, id, bindata):
1213
if id == message.REQ_ITEM:
1215
request = serial.unpack(Request, bindata)
1219
self.__request_item(client, request.id)
1221
elif id == message.REQ_PLAYLIST:
1223
self.request_playlist(client)
1225
elif id == message.REQ_QUEUE:
1227
self.request_queue(client)
1229
elif id == message.REQ_MLIB:
1231
request = serial.unpack(Request, bindata)
1235
self.request_mlib(client, request.path)
1237
elif id == message.REQ_FILES:
1239
request = serial.unpack(Request, bindata)
1243
nested, ids, names = self.__filelib.get_level(request.path)
1245
self.__reply_files_request(client, request.path, nested, ids, names)
1248
log.error("** BUG ** unexpected request message: %d" % id)
1250
# =========================================================================
1252
# =========================================================================
1254
def __util_files_to_uris(self, files):
1256
def file_to_uri(file):
1257
url = urllib.pathname2url(file)
1258
return urlparse.urlunparse(("file", None, url, None, None, None))
1265
uris.append(file_to_uri(file))
1269
def __util_calc_flags(self, playback_known, volume_known, repeat_known,
1270
shuffle_known, progress_known):
1271
""" Check player adapter capabilities.
1273
Most capabilities get detected by testing which methods have been
1274
overridden by a subclassing player adapter.
1278
def ftc(cond, feature):
1279
if inspect.ismethod(cond): # check if overridden
1280
enabled = cond.__module__ != __name__
1290
# --- 'is known' features ---
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),
1298
# --- misc control features ---
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),
1311
# --- request features ---
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),
1318
ftc(config.get_system_shutdown_command(), FT_SHUTDOWN),
1324
for feature in features:
1327
log.debug("flags: %X" % flags)
1331
# =========================================================================
1333
# =========================================================================
1335
# === property: clients ===
1337
def __pget_clients(self):
1338
"""A descriptive list of connected clients.
1340
May be useful to integrate connected clients in a media player UI.
1344
for c in self.__clients:
1348
clients = property(__pget_clients, None, None, __pget_clients.__doc__)
1350
# === property: config ===
1352
def __pget_config(self):
1353
"""Player adapter specific configuration (instance of Config).
1355
This mirrors the configuration in ~/.config/remuco/PLAYER/conf. Any
1356
change to 'config' is saved immediately into the configuration file.
1359
return self.__config
1361
config = property(__pget_config, None, None, __pget_config.__doc__)
1363
# === property: manager ===
1365
def __pget_manager(self):
1366
"""The Manager controlling this adapter.
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.
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.
1380
return self.__manager
1382
def __pset_manager(self, value):
1383
self.__manager = value
1385
manager = property(__pget_manager, __pset_manager, None,
1386
__pget_manager.__doc__)