aboutsummaryrefslogtreecommitdiff
path: root/pydora
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2014-01-04 22:29:14 -0500
committerMike Crute <mcrute@gmail.com>2014-01-04 22:38:58 -0500
commit598c9987e62c757ee297ff11869dbb841f7278f9 (patch)
tree15c3f3303c11eb1e62603fcf5810c3ce2213c031 /pydora
parent2b21d231774a831a28c66998b46352fce6c48036 (diff)
downloadpydora-598c9987e62c757ee297ff11869dbb841f7278f9.tar.bz2
pydora-598c9987e62c757ee297ff11869dbb841f7278f9.tar.xz
pydora-598c9987e62c757ee297ff11869dbb841f7278f9.zip
Factor player code out of pandora package
Diffstat (limited to 'pydora')
-rw-r--r--pydora/mpg123.py154
-rwxr-xr-xpydora/player.py4
-rw-r--r--pydora/utils.py79
3 files changed, 235 insertions, 2 deletions
diff --git a/pydora/mpg123.py b/pydora/mpg123.py
new file mode 100644
index 0000000..bb2e76a
--- /dev/null
+++ b/pydora/mpg123.py
@@ -0,0 +1,154 @@
1import os
2import select
3import subprocess
4
5
6def iterate_forever(func, *args, **kwargs):
7 """Iterate over a finite iterator forever
8
9 When the iterator is exhausted will call the function again to generate a
10 new iterator and keep iterating.
11 """
12 output = func(*args, **kwargs)
13
14 while True:
15 try:
16 yield next(output)
17 except StopIteration:
18 output = func(*args, **kwargs)
19
20
21class SilentPopen(subprocess.Popen):
22 """A Popen varient that dumps it's output and error
23 """
24
25 def __init__(self, *args, **kwargs):
26 self._dev_null = open(os.devnull, 'w')
27 kwargs['stdin'] = subprocess.PIPE
28 kwargs['stdout'] = subprocess.PIPE
29 kwargs['stderr'] = self._dev_null
30 super(SilentPopen, self).__init__(*args, **kwargs)
31
32 def __del__(self):
33 self._dev_null.close()
34 super(SilentPopen, self).__del__()
35
36
37class PlayerCallbacks(object):
38
39 def play(self, song):
40 """Called once when a song starts playing
41 """
42 pass
43
44 def pre_poll(self):
45 """Called before polling for process status
46 """
47 pass
48
49 def post_poll(self):
50 """Called after polling for process status
51 """
52 pass
53
54 def input(self, value, song):
55 """Called after user input during song playback
56 """
57 pass
58
59
60class Player(object):
61 """Remote control for an mpg123 process
62
63 Starts and owns a handle to an mpg123 process then feeds commands to it to
64 play pandora audio
65 """
66
67 def __init__(self, callbacks, control_channel):
68 self._control_channel = control_channel
69 self._control_fd = control_channel.fileno()
70 self._callbacks = callbacks
71 self._process = None
72 self._ensure_started()
73
74 def __del__(self):
75 self._process.kill()
76
77 def _ensure_started(self):
78 """Ensure mpg123 is started
79 """
80 if self._process and self._process.poll() is None:
81 return
82
83 self._process = SilentPopen(
84 ['mpg123', '-q', '-R', '--preload', '0.1'])
85
86 # Only output play status in the player stdout
87 self._send_cmd('silence')
88
89 def _send_cmd(self, cmd):
90 """Write command to remote mpg123 process
91 """
92 self._process.stdin.write("{}\n".format(cmd))
93 self._process.stdin.flush()
94
95 def stop(self):
96 """Stop the currently playing song
97 """
98 self._send_cmd('stop')
99
100 def pause(self):
101 """Pause the player
102 """
103 self._send_cmd('pause')
104
105 def _player_stopped(self, value):
106 """Determine if player has stopped
107 """
108 return value.startswith("@P") and value[3] == "0"
109
110 def play(self, song):
111 """Play a new song from a Pandora model
112
113 Returns once the stream starts but does not shut down the remote mpg123
114 process. Calls the input callback when the user has input.
115 """
116 self._callbacks.play(song)
117 self._send_cmd('load {}'.format(song.audio_url))
118
119 while True:
120 try:
121 self._callbacks.pre_poll()
122 self._ensure_started()
123
124 readers, _, _ = select.select(
125 [self._control_channel, self._process.stdout], [], [], 1)
126
127 for fd in readers:
128 value = fd.readline().strip()
129
130 if fd.fileno() == self._control_fd:
131 self._callbacks.input(value, song)
132 else:
133 if self._player_stopped(value):
134 return
135 finally:
136 self._callbacks.post_poll()
137
138 def end_station(self):
139 """Stop playing the station
140 """
141 raise StopIteration
142
143 def play_station(self, station):
144 """Play the station until something ends it
145
146 This function will run forever until termintated by calling
147 end_station.
148 """
149 for song in iterate_forever(station.get_playlist):
150 try:
151 self.play(song)
152 except StopIteration:
153 self.stop()
154 return
diff --git a/pydora/player.py b/pydora/player.py
index 2b00ad3..6a9c345 100755
--- a/pydora/player.py
+++ b/pydora/player.py
@@ -10,8 +10,8 @@ import os
10import sys 10import sys
11 11
12from pandora import APIClient 12from pandora import APIClient
13from pandora.player import Player 13from pydora.mpg123 import Player
14from pandora.utils import Colors, Screen 14from .utils import Colors, Screen
15 15
16 16
17class PlayerApp: 17class PlayerApp:
diff --git a/pydora/utils.py b/pydora/utils.py
new file mode 100644
index 0000000..7a86d7a
--- /dev/null
+++ b/pydora/utils.py
@@ -0,0 +1,79 @@
1from __future__ import print_function
2
3import sys
4import termios
5
6
7class Colors:
8
9 def __wrap_with(code):
10 @staticmethod
11 def inner(text, bold=False):
12 c = code
13 if bold:
14 c = u"1;{}".format(c)
15 return u"\033[{}m{}\033[0m".format(c, text)
16 return inner
17
18 red = __wrap_with('31')
19 green = __wrap_with('32')
20 yellow = __wrap_with('33')
21 blue = __wrap_with('34')
22 magenta = __wrap_with('35')
23 cyan = __wrap_with('36')
24 white = __wrap_with('37')
25
26
27class Screen:
28
29 @staticmethod
30 def set_echo(enabled):
31 fd = sys.stdin.fileno()
32 (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = \
33 termios.tcgetattr(fd)
34
35 if enabled:
36 lflag |= termios.ECHO
37 else:
38 lflag &= ~termios.ECHO
39
40 termios.tcsetattr(fd, termios.TCSANOW,
41 [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
42
43 @staticmethod
44 def clear():
45 sys.stdout.write('\x1b[2J\x1b[H')
46 sys.stdout.flush()
47
48 @staticmethod
49 def print_error(msg):
50 print(Colors.red(msg))
51
52 @staticmethod
53 def print_success(msg):
54 print(Colors.green(msg))
55
56 @staticmethod
57 def get_integer(prompt):
58 """Gather user input and convert it to an integer
59
60 Will keep trying till the user enters an interger or until they ^C the
61 program.
62 """
63 try:
64 raw_input
65 except NameError:
66 raw_input = input
67
68 while True:
69 try:
70 return int(raw_input(prompt).strip())
71 except ValueError:
72 print(Colors.red('Invaid Input!'))
73
74
75def clear_screen():
76 """Clear the terminal
77 """
78 sys.stdout.write('\x1b[2J\x1b[H')
79 sys.stdout.flush()