-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpcvc.py
More file actions
executable file
·205 lines (172 loc) · 7.96 KB
/
pcvc.py
File metadata and controls
executable file
·205 lines (172 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python
"""This is a PC Volume Control Python client.
"""
__version__ = 6
import argparse
from pprint import pprint
import json
import readline
import socket
import sys
import time
class MyCompleter(object):
"""An autocomplete widget thingy because hey why not"""
def __init__(self, options):
self.options = sorted(options)
def complete(self, text, state):
if state == 0: # on first trigger, build possible matches
if text: # cache matches (entries that start with entered text)
self.matches = [s for s in self.options
if s and s.startswith(text)]
else: # no text entered, all matches possible
self.matches = self.options[:]
# return match indexed by state
try:
return self.matches[state]
except IndexError:
return None
class PcvgClient(object):
"""A TCP Client used to interact with PC Volume Control"""
state = None
def __init__(self, servername, port):
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.servername = servername
self.port = int(port)
self.connected = False
self.server_version = None
def connect(self):
"""Open a socket with the target. This will get an immediate response
with the full state. Populate this object with the initial state.
"""
self.client.connect((self.servername, self.port))
buff, bufsize = [], 1024
while True:
data = self.client.recv(bufsize).decode()
buff.append(data)
if data.endswith('\n'):
break
response = ''.join(buff)
self.state = json.loads(response)
self.server_version = self.state.get('version')
if self.server_version != __version__:
raise RuntimeError(f'Version mismatch: client({__version__}) and server{self.server_version}!')
self.connected = True
def push_update(self, data):
"""Push an update over to the server.
Take in a regular dict and convert/encode to JSON for sending.
Updates require the version and device id.
"""
jstring = json.dumps(data)
# data going out has to be bytes-encoded.
print("JSON string being sent to the server:\n")
pprint(jstring)
jstring = jstring + '\n'
self.client.send(jstring.encode())
def disconnect(self):
self.client.close()
self.connected = False
def toggle_master_mute(self):
"""For the master device, toggle its mute button."""
master_device_id = self.state['defaultDevice']['deviceId']
new_state = not self.state['defaultDevice']['masterMuted']
update = {'version': __version__,
'defaultDevice': {'deviceId': master_device_id,
'masterMuted': new_state}}
self.push_update(data=update)
def toggle_session_mute(self, session):
master_device_id = self.state['defaultDevice']['deviceId']
# 'session' can be a case-insensitive substring in the session name.
# There can be one or many sessions with this name.
targeted_sessions = [d for d in self.state['defaultDevice']['sessions'] if session.lower() in d.get('name').lower()]
for tgt in targeted_sessions:
tgt['muted'] = not tgt['muted']
update = {'version': __version__,
'defaultDevice': {'deviceId': master_device_id,
'sessions': targeted_sessions}}
for x in targeted_sessions:
print(f"Session ({x.get('name')}) changed muted value to ({x.get('muted')})")
self.push_update(data=update)
def __repr__(self):
return f'Connected: {self.connected}, Host: {self.servername}:{self.port}, Server Version: {self.server_version}'
def change_master_volume(self, newvolume):
master_device_id = self.state['defaultDevice']['deviceId']
update = {'version': __version__,
'defaultDevice': {'deviceId': master_device_id,
'masterVolume': float(newvolume)}}
self.push_update(data=update)
def change_session_volume(self, session, newvolume):
targeted_sessions = [d for d in self.state['defaultDevice']['sessions'] if session.lower() in d.get('name').lower()]
for tgt in targeted_sessions:
tgt['volume'] = float(newvolume)
master_device_id = self.state['defaultDevice']['deviceId']
update = {'version': __version__,
'defaultDevice': {'deviceId': master_device_id,
'sessions': targeted_sessions}}
for x in targeted_sessions:
print(f"Session ({x.get('name')}) changed volume value to ({x.get('volume')})")
self.push_update(data=update)
def main(arguments):
"""Run the volume control client."""
my_client = PcvgClient(servername=arguments.server_name, port=arguments.server_port)
my_client.connect()
if arguments.watch:
try:
while True:
print(chr(27) + "[2J") # clear screen
pprint(my_client.state)
my_client.disconnect()
time.sleep(0.5)
except KeyboardInterrupt:
print('Exiting...')
my_client.disconnect()
sys.exit()
if arguments.interactive:
# play around
commands = ['toggle', 'volume', 't', 'v', 'sessions', 'scary']
completer = MyCompleter(commands)
readline.set_completer(completer.complete)
readline.parse_and_bind('tab: complete')
while True:
sessions = my_client.state['defaultDevice']['sessions']
command = input('what do? > ')
params = command.split()
try:
if params[0] not in commands:
print(f'The commands available are: {commands}')
continue
if params[0] in ['toggle', 't']:
if params[1] == 'master':
# mute or unmute mster
my_client.toggle_master_mute()
else:
# It's a session to mute.
my_client.toggle_session_mute(params[1])
if params[0] in ['volume', 'v']:
# Change the volume to a certain amount.
if params[1] == 'master':
my_client.change_master_volume(params[2])
else:
my_client.change_session_volume(session=params[1], newvolume=params[2])
if params[0] == 'sessions':
# Just dump all the sessions.
pprint(my_client.state['defaultDevice']['sessions'])
if params[0] == 'scary':
# Just allow them to send whatever they paste into the terminal.
unsanitized_frightening_input = input('Enter some JSONt to send:')
my_client.push_update(unsanitized_frightening_input)
pprint(my_client.state)
except IndexError:
print('There was a problem with your command. Maybe add arguments?')
print(f'The commands available are: {commands}')
except KeyboardInterrupt:
my_client.disconnect()
sys.exit()
if __name__ == '__main__':
# parse arguments on the command line.
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('server_name', help='server FQDN or IP')
parser.add_argument('-p', action='store', dest='server_port', help='server listening TCP port, defaults to 3000', default=3000)
parser.add_argument('-w', action='store_true', dest='watch', help='Just watch the server state.')
parser.add_argument('-i', action='store_true', dest='interactive', help='Run in interactive mode for server testing.')
args = parser.parse_args()
main(args)