aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-09-18 20:59:09 -0700
committerMike Crute <mike@crute.us>2017-09-30 23:09:18 +0000
commit251eac4e987e241798b12f271f0cd4af24173d3e (patch)
tree9fed127b0aba8a35b62c4411831eebc35617c72f
parent095351609ff0bbf9abdbf7a86b09cb6afc8f83b0 (diff)
downloadpydora-251eac4e987e241798b12f271f0cd4af24173d3e.tar.bz2
pydora-251eac4e987e241798b12f271f0cd4af24173d3e.tar.xz
pydora-251eac4e987e241798b12f271f0cd4af24173d3e.zip
Add remote VLC player backend
-rw-r--r--README.rst25
-rw-r--r--pydora/audio_backend.py42
-rw-r--r--pydora/player.py28
3 files changed, 92 insertions, 3 deletions
diff --git a/README.rst b/README.rst
index 8eab71d..f317900 100644
--- a/README.rst
+++ b/README.rst
@@ -55,6 +55,31 @@ The ``pydora`` player will try to auto-detect whatever player exists on your
55system, prefering VLC, and will use that audio output backend. If you notice a 55system, prefering VLC, and will use that audio output backend. If you notice a
56lot of skipping in a playlist consider installing VLC. 56lot of skipping in a playlist consider installing VLC.
57 57
58Remote VLC Backend
59------------------
60It is also possible to remotely control a copy of VLC running on another
61machine if you're unable or unwilling to install Pydora on your playback
62machine. To do this start VLC on the remote machine with the ``rc-host`` option
63set. For example::
64
65 vlc -I rc --advanced --rc-host=0.0.0.0:1234
66
67Once VLC is running start Pydora with the ``vlc-net`` option and specify the
68remote host and port that VLC is listening on. For example::
69
70 pydora --vlc-net 192.168.0.12:1234
71
72Pydora will now send all audio playback requests to the remote VLC. It does
73this using a text control protocol; all audio data is streamed directly from
74the internet to VLC and is not passed over the pydora control channel. Because
75of this it is possible for the control channel to run over a very low bandwidth
76connection.
77
78**Note**: VLC doesn't provide any security so anyone on the network will be
79able to control VLC. It is generally safer to bind VLC to ``127.0.0.1`` and use
80something like SSH forwarding to securely forward the port to a remote host but
81that's outside of the scope of this README.
82
58Simple Player 83Simple Player
59============= 84=============
60Included is ``pydora``, a simple Pandora stream player that runs at the command 85Included is ``pydora``, a simple Pandora stream player that runs at the command
diff --git a/pydora/audio_backend.py b/pydora/audio_backend.py
index 9599771..f92998a 100644
--- a/pydora/audio_backend.py
+++ b/pydora/audio_backend.py
@@ -2,6 +2,7 @@ import os
2import time 2import time
3import fcntl 3import fcntl
4import select 4import select
5import socket
5 6
6from pandora.py2compat import which 7from pandora.py2compat import which
7from .utils import iterate_forever, SilentPopen 8from .utils import iterate_forever, SilentPopen
@@ -141,6 +142,13 @@ class BasePlayer(object):
141 self._process = SilentPopen(self._cmd) 142 self._process = SilentPopen(self._cmd)
142 self._post_start() 143 self._post_start()
143 144
145 def _get_select_readers(self):
146 """Return a list of file-like objects for reading
147
148 Will be passed to select() to poll for readers.
149 """
150 return [self._control_channel, self._process.stdout]
151
144 def play(self, song): 152 def play(self, song):
145 """Play a new song from a Pandora model 153 """Play a new song from a Pandora model
146 154
@@ -159,7 +167,7 @@ class BasePlayer(object):
159 self._loop_hook() 167 self._loop_hook()
160 168
161 readers, _, _ = select.select( 169 readers, _, _ = select.select(
162 [self._control_channel, self._process.stdout], [], [], 1) 170 self._get_select_readers(), [], [], 1)
163 171
164 for handle in readers: 172 for handle in readers:
165 if handle.fileno() == self._control_fd: 173 if handle.fileno() == self._control_fd:
@@ -272,3 +280,35 @@ class VLCPlayer(BasePlayer):
272 if (time.time() - self._last_poll) >= self.POLL_INTERVAL: 280 if (time.time() - self._last_poll) >= self.POLL_INTERVAL:
273 self._send_cmd("status") 281 self._send_cmd("status")
274 self._last_poll = time.time() 282 self._last_poll = time.time()
283
284
285class RemoteVLC(VLCPlayer):
286
287 def __init__(self, host, port, callbacks, control_channel):
288 self._connect_to = (host, int(port))
289 self._control_sock = None
290 super(RemoteVLC, self).__init__(callbacks, control_channel)
291
292 def _get_select_readers(self):
293 return [self._control_channel, self._control_sock]
294
295 def _send_cmd(self, cmd):
296 self._control_sock.sendall("{}\n".format(cmd).encode("utf-8"))
297
298 def _read_from_process(self, handle):
299 return handle.recv(self.CHUNK_SIZE).strip()
300
301 def _ensure_started(self):
302 if not self._control_sock:
303 self._control_sock = socket.create_connection(self._connect_to)
304 self._control_sock.setblocking(False)
305
306 def _find_path(self):
307 try:
308 self._ensure_started()
309 except socket.error:
310 raise PlayerUnusable("Unable to connect to VLC")
311
312 def _post_start(self):
313 # This is a NOOP for network VLC
314 pass
diff --git a/pydora/player.py b/pydora/player.py
index 991468a..128674b 100644
--- a/pydora/player.py
+++ b/pydora/player.py
@@ -10,9 +10,11 @@ from __future__ import print_function
10 10
11import os 11import os
12import sys 12import sys
13import argparse
13from pandora import clientbuilder 14from pandora import clientbuilder
14 15
15from .utils import Colors, Screen 16from .utils import Colors, Screen
17from .audio_backend import RemoteVLC
16from .audio_backend import MPG123Player, VLCPlayer 18from .audio_backend import MPG123Player, VLCPlayer
17from .audio_backend import UnsupportedEncoding, PlayerUnusable 19from .audio_backend import UnsupportedEncoding, PlayerUnusable
18 20
@@ -66,7 +68,20 @@ class PlayerApp(object):
66 self.client = None 68 self.client = None
67 self.screen = Screen() 69 self.screen = Screen()
68 70
69 def get_player(self): 71 def get_player(self, vlc_net=None):
72 # The user must explicitly request network VLC so we should always
73 # honor that request, to this end we try network first and fail hard
74 # if that isn't available.
75 if vlc_net:
76 try:
77 host, port = vlc_net.split(":")
78 player = RemoteVLC(host, port, self, sys.stdin)
79 Screen.print_success("Using Remote VLC")
80 return player
81 except PlayerUnusable:
82 Screen.print_error("Unable to connect to vlc")
83 raise
84
70 try: 85 try:
71 player = VLCPlayer(self, sys.stdin) 86 player = VLCPlayer(self, sys.stdin)
72 self.screen.print_success("Using VLC") 87 self.screen.print_success("Using VLC")
@@ -232,8 +247,16 @@ class PlayerApp(object):
232 "your config file before continuing.")) 247 "your config file before continuing."))
233 sys.exit(1) 248 sys.exit(1)
234 249
250 def _parse_args(self):
251 parser = argparse.ArgumentParser(
252 description="command line Pandora player")
253 parser.add_argument(
254 "--vlc-net", dest="vlc_net",
255 help="connect to VLC over the network (host:port)")
256 return parser.parse_args()
257
235 def run(self): 258 def run(self):
236 self.player = self.get_player() 259 self.player = self.get_player(self._parse_args().vlc_net)
237 self.player.start() 260 self.player.start()
238 261
239 self.client = self.get_client() 262 self.client = self.get_client()
@@ -258,6 +281,7 @@ class PlayerApp(object):
258 except UnsupportedEncoding as ex: 281 except UnsupportedEncoding as ex:
259 error = str(ex) 282 error = str(ex)
260 except KeyboardInterrupt: 283 except KeyboardInterrupt:
284 self.player.stop()
261 sys.exit(0) 285 sys.exit(0)
262 286
263 287