diff options
author | Mike Crute <mcrute@gmail.com> | 2014-01-04 22:29:14 -0500 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2014-01-04 22:38:58 -0500 |
commit | 598c9987e62c757ee297ff11869dbb841f7278f9 (patch) | |
tree | 15c3f3303c11eb1e62603fcf5810c3ce2213c031 /pydora | |
parent | 2b21d231774a831a28c66998b46352fce6c48036 (diff) | |
download | pydora-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.py | 154 | ||||
-rwxr-xr-x | pydora/player.py | 4 | ||||
-rw-r--r-- | pydora/utils.py | 79 |
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 @@ | |||
1 | import os | ||
2 | import select | ||
3 | import subprocess | ||
4 | |||
5 | |||
6 | def 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 | |||
21 | class 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 | |||
37 | class 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 | |||
60 | class 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 | |||
10 | import sys | 10 | import sys |
11 | 11 | ||
12 | from pandora import APIClient | 12 | from pandora import APIClient |
13 | from pandora.player import Player | 13 | from pydora.mpg123 import Player |
14 | from pandora.utils import Colors, Screen | 14 | from .utils import Colors, Screen |
15 | 15 | ||
16 | 16 | ||
17 | class PlayerApp: | 17 | class 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 @@ | |||
1 | from __future__ import print_function | ||
2 | |||
3 | import sys | ||
4 | import termios | ||
5 | |||
6 | |||
7 | class 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 | |||
27 | class 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 | |||
75 | def clear_screen(): | ||
76 | """Clear the terminal | ||
77 | """ | ||
78 | sys.stdout.write('\x1b[2J\x1b[H') | ||
79 | sys.stdout.flush() | ||