aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-09-30 20:34:41 +0000
committerMike Crute <mike@crute.us>2017-09-30 23:09:18 +0000
commit095351609ff0bbf9abdbf7a86b09cb6afc8f83b0 (patch)
tree686e683f40944da9f179d0d55edca40fe9215fb6
parent1e9bc9d34c210925004eeeea4533a14185078db6 (diff)
downloadpydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.tar.bz2
pydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.tar.xz
pydora-095351609ff0bbf9abdbf7a86b09cb6afc8f83b0.zip
Add Windows echo driver
This makes the previously stateless Screen class stateful because it now needs to construct and hold a reference to the platform specific echo driver.
-rw-r--r--pydora/configure.py9
-rw-r--r--pydora/player.py62
-rw-r--r--pydora/utils.py104
3 files changed, 128 insertions, 47 deletions
diff --git a/pydora/configure.py b/pydora/configure.py
index 01de8df..7d3c754 100644
--- a/pydora/configure.py
+++ b/pydora/configure.py
@@ -108,6 +108,7 @@ class Configurator(object):
108 self.builder = PydoraConfigFileBuilder() 108 self.builder = PydoraConfigFileBuilder()
109 109
110 self.cfg = ConfigParser() 110 self.cfg = ConfigParser()
111 self.screen = Screen()
111 112
112 if self.builder.file_exists: 113 if self.builder.file_exists:
113 self.read_config() 114 self.read_config()
@@ -116,11 +117,11 @@ class Configurator(object):
116 self.cfg.add_section("api") 117 self.cfg.add_section("api")
117 118
118 def fail(self, message): 119 def fail(self, message):
119 print(Screen.print_error(message)) 120 print(self.screen.print_error(message))
120 sys.exit(1) 121 sys.exit(1)
121 122
122 def finished(self, message): 123 def finished(self, message):
123 Screen.print_success(message) 124 self.screen.print_success(message)
124 sys.exit(0) 125 sys.exit(0)
125 126
126 def print_message(self, message): 127 def print_message(self, message):
@@ -133,10 +134,10 @@ class Configurator(object):
133 self.fail("Error loading config file. Unable to continue.") 134 self.fail("Error loading config file. Unable to continue.")
134 135
135 def get_value(self, section, key, prompt): 136 def get_value(self, section, key, prompt):
136 self.cfg.set(section, key, Screen.get_string(prompt)) 137 self.cfg.set(section, key, self.screen.get_string(prompt))
137 138
138 def get_password(self, section, key, prompt): 139 def get_password(self, section, key, prompt):
139 self.cfg.set(section, key, Screen.get_password(prompt)) 140 self.cfg.set(section, key, self.screen.get_password(prompt))
140 141
141 def set_static_value(self, section, key, value): 142 def set_static_value(self, section, key, value):
142 self.cfg.set(section, key, value) 143 self.cfg.set(section, key, value)
diff --git a/pydora/player.py b/pydora/player.py
index 653fb35..991468a 100644
--- a/pydora/player.py
+++ b/pydora/player.py
@@ -64,23 +64,24 @@ class PlayerApp(object):
64 64
65 def __init__(self): 65 def __init__(self):
66 self.client = None 66 self.client = None
67 self.screen = Screen()
67 68
68 def get_player(self): 69 def get_player(self):
69 try: 70 try:
70 player = VLCPlayer(self, sys.stdin) 71 player = VLCPlayer(self, sys.stdin)
71 Screen.print_success("Using VLC") 72 self.screen.print_success("Using VLC")
72 return player 73 return player
73 except PlayerUnusable: 74 except PlayerUnusable:
74 pass 75 pass
75 76
76 try: 77 try:
77 player = MPG123Player(self, sys.stdin) 78 player = MPG123Player(self, sys.stdin)
78 Screen.print_success("Using mpg123") 79 self.screen.print_success("Using mpg123")
79 return player 80 return player
80 except PlayerUnusable: 81 except PlayerUnusable:
81 pass 82 pass
82 83
83 Screen.print_error("Unable to find a player") 84 self.screen.print_error("Unable to find a player")
84 sys.exit(1) 85 sys.exit(1)
85 86
86 def get_client(self): 87 def get_client(self):
@@ -94,22 +95,22 @@ class PlayerApp(object):
94 return builder.build() 95 return builder.build()
95 96
96 if not self.client: 97 if not self.client:
97 Screen.print_error("No valid config found") 98 self.screen.print_error("No valid config found")
98 sys.exit(1) 99 sys.exit(1)
99 100
100 def station_selection_menu(self, error=None): 101 def station_selection_menu(self, error=None):
101 """Format a station menu and make the user select a station 102 """Format a station menu and make the user select a station
102 """ 103 """
103 Screen.clear() 104 self.screen.clear()
104 105
105 if error: 106 if error:
106 Screen.print_error("{}\n".format(error)) 107 self.screen.print_error("{}\n".format(error))
107 108
108 for i, station in enumerate(self.stations): 109 for i, station in enumerate(self.stations):
109 i = "{:>3}".format(i) 110 i = "{:>3}".format(i)
110 print(u"{}: {}".format(Colors.yellow(i), station.name)) 111 print(u"{}: {}".format(Colors.yellow(i), station.name))
111 112
112 return self.stations[Screen.get_integer("Station: ")] 113 return self.stations[self.screen.get_integer("Station: ")]
113 114
114 def play(self, song): 115 def play(self, song):
115 """Play callback 116 """Play callback
@@ -122,7 +123,7 @@ class PlayerApp(object):
122 123
123 def skip_song(self, song): 124 def skip_song(self, song):
124 if song.is_ad: 125 if song.is_ad:
125 Screen.print_error("Cannot skip advertisements") 126 self.screen.print_error("Cannot skip advertisements")
126 else: 127 else:
127 self.player.stop() 128 self.player.stop()
128 129
@@ -135,61 +136,63 @@ class PlayerApp(object):
135 def dislike_song(self, song): 136 def dislike_song(self, song):
136 try: 137 try:
137 if song.thumbs_down(): 138 if song.thumbs_down():
138 Screen.print_success("Track disliked") 139 self.screen.print_success("Track disliked")
139 self.player.stop() 140 self.player.stop()
140 else: 141 else:
141 Screen.print_error("Failed to dislike track") 142 self.screen.print_error("Failed to dislike track")
142 except NotImplementedError: 143 except NotImplementedError:
143 Screen.print_error("Cannot dislike this type of track") 144 self.screen.print_error("Cannot dislike this type of track")
144 145
145 def like_song(self, song): 146 def like_song(self, song):
146 try: 147 try:
147 if song.thumbs_up(): 148 if song.thumbs_up():
148 Screen.print_success("Track liked") 149 self.screen.print_success("Track liked")
149 else: 150 else:
150 Screen.print_error("Failed to like track") 151 self.screen.print_error("Failed to like track")
151 except NotImplementedError: 152 except NotImplementedError:
152 Screen.print_error("Cannot like this type of track") 153 self.screen.print_error("Cannot like this type of track")
153 154
154 def bookmark_song(self, song): 155 def bookmark_song(self, song):
155 try: 156 try:
156 if song.bookmark_song(): 157 if song.bookmark_song():
157 Screen.print_success("Bookmarked song") 158 self.screen.print_success("Bookmarked song")
158 else: 159 else:
159 Screen.print_error("Failed to bookmark song") 160 self.screen.print_error("Failed to bookmark song")
160 except NotImplementedError: 161 except NotImplementedError:
161 Screen.print_error("Cannot bookmark this type of track") 162 self.screen.print_error("Cannot bookmark this type of track")
162 163
163 def bookmark_artist(self, song): 164 def bookmark_artist(self, song):
164 try: 165 try:
165 if song.bookmark_artist(): 166 if song.bookmark_artist():
166 Screen.print_success("Bookmarked artist") 167 self.screen.print_success("Bookmarked artist")
167 else: 168 else:
168 Screen.print_error("Failed to bookmark artis") 169 self.screen.print_error("Failed to bookmark artis")
169 except NotImplementedError: 170 except NotImplementedError:
170 Screen.print_error("Cannot bookmark artist for this type of track") 171 self.screen.print_error(
172 "Cannot bookmark artist for this type of track")
171 173
172 def sleep_song(self, song): 174 def sleep_song(self, song):
173 try: 175 try:
174 if song.sleep(): 176 if song.sleep():
175 Screen.print_success("Song will not be played for 30 days") 177 self.screen.print_success(
178 "Song will not be played for 30 days")
176 self.player.stop() 179 self.player.stop()
177 else: 180 else:
178 Screen.print_error("Failed to sleep song") 181 self.screen.print_error("Failed to sleep song")
179 except NotImplementedError: 182 except NotImplementedError:
180 Screen.print_error("Cannot sleep this type of track") 183 self.screen.print_error("Cannot sleep this type of track")
181 184
182 def raise_volume(self, song): 185 def raise_volume(self, song):
183 try: 186 try:
184 self.player.raise_volume() 187 self.player.raise_volume()
185 except NotImplementedError: 188 except NotImplementedError:
186 Screen.print_error("Cannot sleep this type of track") 189 self.screen.print_error("Cannot sleep this type of track")
187 190
188 def lower_volume(self, song): 191 def lower_volume(self, song):
189 try: 192 try:
190 self.player.lower_volume() 193 self.player.lower_volume()
191 except NotImplementedError: 194 except NotImplementedError:
192 Screen.print_error("Cannot sleep this type of track") 195 self.screen.print_error("Cannot sleep this type of track")
193 196
194 def quit(self, song): 197 def quit(self, song):
195 self.player.end_station() 198 self.player.end_station()
@@ -209,20 +212,21 @@ class PlayerApp(object):
209 try: 212 try:
210 cmd = getattr(self, self.CMD_MAP[input][1]) 213 cmd = getattr(self, self.CMD_MAP[input][1])
211 except (IndexError, KeyError): 214 except (IndexError, KeyError):
212 return Screen.print_error("Invalid command {!r}!".format(input)) 215 return self.screen.print_error(
216 "Invalid command {!r}!".format(input))
213 217
214 cmd(song) 218 cmd(song)
215 219
216 def pre_poll(self): 220 def pre_poll(self):
217 Screen.set_echo(False) 221 self.screen.set_echo(False)
218 222
219 def post_poll(self): 223 def post_poll(self):
220 Screen.set_echo(True) 224 self.screen.set_echo(True)
221 225
222 def pre_flight_checks(self): 226 def pre_flight_checks(self):
223 # See #52, this key no longer passes some server-side check 227 # See #52, this key no longer passes some server-side check
224 if self.client.partner_user == "iphone": 228 if self.client.partner_user == "iphone":
225 Screen.print_error(( 229 self.screen.print_error((
226 "The `iphone` partner key set is no longer compatible with " 230 "The `iphone` partner key set is no longer compatible with "
227 "pydora. Please re-run pydora-configure to re-generate " 231 "pydora. Please re-run pydora-configure to re-generate "
228 "your config file before continuing.")) 232 "your config file before continuing."))
diff --git a/pydora/utils.py b/pydora/utils.py
index faa18e5..6c608a7 100644
--- a/pydora/utils.py
+++ b/pydora/utils.py
@@ -6,13 +6,14 @@ import getpass
6import subprocess 6import subprocess
7from pandora.py2compat import input 7from pandora.py2compat import input
8 8
9try:
10 import termios
11except ImportError:
12 # Windows does not have a termios module
13 termios = None
14 9
10class TerminalPlatformUnsupported(Exception):
11 """Platform-specific functionality is not supported
15 12
13 Raised by code that can not be used to interact with the terminal on this
14 platform.
15 """
16 pass
16 17
17 18
18class Colors(object): 19class Colors(object):
@@ -35,25 +36,100 @@ class Colors(object):
35 white = __wrap_with("37") 36 white = __wrap_with("37")
36 37
37 38
38class Screen(object): 39class PosixEchoControl(object):
40 """Posix Console Echo Control Driver
39 41
40 @staticmethod 42 Uses termios on POSIX compliant platforms to control console echo. Is not
41 def set_echo(enabled): 43 supported on Windows as termios is not available and will throw a
42 if not termios: 44 TerminalPlatformUnsupported exception if contructed on Windows.
43 return 45 """
46
47 def __init__(self):
48 try:
49 import termios
50 self.termios = termios
51 except ImportError:
52 raise TerminalPlatformUnsupported("POSIX not supported")
44 53
54 def set_echo(self, enabled):
45 handle = sys.stdin.fileno() 55 handle = sys.stdin.fileno()
46 if not os.isatty(handle): 56 if not os.isatty(handle):
47 return 57 return
48 58
49 attrs = termios.tcgetattr(handle) 59 attrs = self.termios.tcgetattr(handle)
60
61 if enabled:
62 attrs[3] |= self.termios.ECHO
63 else:
64 attrs[3] &= ~self.termios.ECHO
65
66 self.termios.tcsetattr(handle, self.termios.TCSANOW, attrs)
67
68
69class Win32EchoControl(object):
70 """Windows Console Echo Control Driver
71
72 This uses the console API from WinCon.h and ctypes to control console echo
73 on Windows clients. It is not possible to construct this class on
74 non-Windows systems, on those systems it will throw a
75 TerminalPlatformUnsupported exception.
76 """
77
78 STD_INPUT_HANDLE = -10
79 ENABLE_ECHO_INPUT = 0x4
80 DISABLE_ECHO_INPUT = ~ENABLE_ECHO_INPUT
81
82 def __init__(self):
83 import ctypes
84
85 if not hasattr(ctypes, "windll"):
86 raise TerminalPlatformUnsupported("Windows not supported")
87
88 from ctypes import wintypes
89
90 self.ctypes = ctypes
91 self.wintypes = wintypes
92 self.kernel32 = ctypes.windll.kernel32
93
94 def _GetStdHandle(self, handle):
95 return self.kernel32.GetStdHandle(handle)
96
97 def _GetConsoleMode(self, handle):
98 mode = self.wintypes.DWORD()
99 self.kernel32.GetConsoleMode(handle, self.ctypes.byref(mode))
100 return mode.value
101
102 def _SetConsoleMode(self, handle, value):
103 self.kernel32.SetConsoleMode(handle, value)
104
105 def set_echo(self, enabled):
106 stdin = self._GetStdHandle(self.STD_INPUT_HANDLE)
107 mode = self._GetConsoleMode(stdin)
50 108
51 if enabled: 109 if enabled:
52 attrs[3] |= termios.ECHO 110 self._SetConsoleMode(stdin, mode | self.ENABLE_ECHO_INPUT)
53 else: 111 else:
54 attrs[3] &= ~termios.ECHO 112 self._SetConsoleMode(stdin, mode & self.DISABLE_ECHO_INPUT)
113
114
115class Screen(object):
116
117 def __init__(self):
118 try:
119 self._echo_driver = PosixEchoControl()
120 except TerminalPlatformUnsupported:
121 pass
122
123 try:
124 self._echo_driver = Win32EchoControl()
125 except TerminalPlatformUnsupported:
126 pass
127
128 if not self._echo_driver:
129 raise TerminalPlatformUnsupported("No supported terminal driver")
55 130
56 termios.tcsetattr(handle, termios.TCSANOW, attrs) 131 def set_echo(self, enabled):
132 self._echo_driver.set_echo(enabled)
57 133
58 @staticmethod 134 @staticmethod
59 def clear(): 135 def clear():