97 lines
6.1 KiB
Python
97 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Install a VoicePult folder/profile without replacing existing OpenDeck keys."""
|
|
import argparse
|
|
import copy
|
|
from datetime import datetime
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
import urllib.request
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
PLUGIN='de.voicepult.control.sdPlugin'
|
|
STARTER='com.amansprojects.starterpack.sdPlugin'
|
|
|
|
def build(default, audio, device, parent, profile):
|
|
keys=default['keys']
|
|
free=next((i for i,key in enumerate(keys) if key is None),None)
|
|
if free is None:
|
|
raise ValueError('Im Ausgangsprofil ist keine freie Taste für den VoicePult-Ordner.')
|
|
items=[dict(c,kind='channel') for c in audio['channels']]+[dict(t,kind='target') for t in audio['targets']]
|
|
if len(items)>len(keys)-1:
|
|
raise ValueError('Zu viele Audiokanäle für eine einzelne Stream-Deck-Seite.')
|
|
base={'alignment':'middle','background_colour':'#1d281f','colour':'#ffffff','family':'Liberation Sans','image_scale':100,'name':'','show':True,'size':13,'stroke_colour':'#000000','stroke_size':1,'style':'Bold','text':'','underline':False}
|
|
def key(index,name,uuid,plugin,settings,images,text,automatic=False,inspector=None):
|
|
states=[dict(base,image=f'plugins/{plugin}/{image}',text=text) for image in images]
|
|
action={'controllers':['Keypad'],'disable_automatic_states':not automatic,'encoder':None,'icon':states[0]['image'],'name':name,'plugin':plugin,'property_inspector':inspector,'states':copy.deepcopy(states),'supported_in_multi_actions':True,'tooltip':name,'uuid':uuid,'visible_in_action_list':True}
|
|
return {'action':action,'children':None,'context':f'Keypad.{index}.0','current_state':0,'settings':settings,'states':states}
|
|
def switch(index,destination,title):
|
|
result=key(index,'Switch Profile','com.amansprojects.starterpack.switchprofile',STARTER,{'device':device,'profile':destination},['icons/switchProfile.png'],title,True,f'plugins/{STARTER}/propertyInspector/switchProfile.html')
|
|
result['states'][0]['image']=f'plugins/{PLUGIN}/folder.svg'
|
|
return result
|
|
updated=copy.deepcopy(default)
|
|
updated['keys'][free]=switch(free,profile,'VoicePult')
|
|
page=copy.deepcopy(default)
|
|
page['keys']=[None]*len(keys)
|
|
# A profile page must not inherit encoder or touchscreen actions.
|
|
for field in ('sliders','encoders','touchscreens','infobars'):
|
|
if field in page:page[field]=[None]*len(page[field]) if field!='infobars' else []
|
|
for index,item in enumerate(items):
|
|
label=('Mic: ' if item['kind']=='target' else '')+item['name']
|
|
page['keys'][index]=key(index,'Kanal ein/aus','de.voicepult.control.toggle',PLUGIN,{'kind':item['kind'],'id':item['id'],'name':label},['on.svg','off.svg'],label+'\n'+('AUS' if item['muted'] else 'AN'),inspector=f'plugins/{PLUGIN}/inspector.html')
|
|
page['keys'][index]['current_state']=int(item['muted'])
|
|
page['keys'][-1]=switch(len(keys)-1,parent,'Zurück')
|
|
return updated,page,free
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--device',required=True)
|
|
parser.add_argument('--parent',default='Default')
|
|
parser.add_argument('--profile',default='VoicePult')
|
|
parser.add_argument('--apply',action='store_true')
|
|
args=parser.parse_args()
|
|
for value in (args.device,args.parent,args.profile):
|
|
if not value or '/' in value or '\\' in value or value in ('.','..'):
|
|
parser.error('Ungültiger Profil- oder Gerätename')
|
|
config=Path(os.environ.get('XDG_CONFIG_HOME',Path.home()/'.config'))/'opendeck'
|
|
directory=config/'profiles'/args.device
|
|
parent=directory/(args.parent+'.json');profile=directory/(args.profile+'.json')
|
|
if profile.exists() or (config/'plugins'/PLUGIN).exists():
|
|
raise SystemExit('VoicePult-Profil oder Plugin bereits vorhanden; bestehende Einrichtung wird nicht überschrieben.')
|
|
if not (config/'plugins'/STARTER/'manifest.json').exists():
|
|
raise SystemExit('OpenDeck Starter Pack wird für die Ordnernavigation benötigt.')
|
|
version=subprocess.check_output(['node','--version'],text=True).strip()
|
|
if int(version.lstrip('v').split('.')[0])<22:raise SystemExit('Node.js 22 oder neuer benötigt.')
|
|
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
audio=json.load(opener.open('http://127.0.0.1:8765/api/state',timeout=10))
|
|
updated,page,slot=build(json.loads(parent.read_text()),audio,args.device,args.parent,args.profile)
|
|
print(f'Ordner {args.profile}: Taste {slot+1}, {len(audio["channels"])} Kanäle und {len(audio["targets"])} Mikrofonziele; Zurück auf Taste {len(page["keys"])}.')
|
|
if not args.apply:return
|
|
pids=subprocess.run(['pgrep','-u',str(os.getuid()),'-x','opendeck'],capture_output=True,text=True).stdout.split()
|
|
for pid in pids:os.kill(int(pid),signal.SIGTERM)
|
|
for _ in range(80):
|
|
running=subprocess.run(['pgrep','-u',str(os.getuid()),'-x','opendeck'],capture_output=True).returncode==0
|
|
if not running:break
|
|
time.sleep(.1)
|
|
else:raise SystemExit('OpenDeck hat sich noch nicht beendet; keine Dateien verändert.')
|
|
try:
|
|
# Read again after shutdown, so the last user changes are preserved.
|
|
updated,page,slot=build(json.loads(parent.read_text()),audio,args.device,args.parent,args.profile)
|
|
backup=config/'voicepult-backups'/datetime.now().strftime('%Y%m%d-%H%M%S')
|
|
backup.mkdir(parents=True)
|
|
shutil.copy2(parent,backup/parent.name)
|
|
shutil.copy2(config/'profiles'/(args.device+'.json'),backup/'device.json')
|
|
shutil.copytree(ROOT/'integrations/opendeck'/PLUGIN,config/'plugins'/PLUGIN)
|
|
for path,data in ((profile,page),(parent,updated)):
|
|
temp=path.with_suffix('.voicepult.tmp');temp.write_text(json.dumps(data,ensure_ascii=False,indent=2));temp.replace(path)
|
|
print('Sicherung:',backup)
|
|
finally:
|
|
subprocess.Popen(['/usr/bin/opendeck','--hide'],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)
|
|
print('OpenDeck mit VoicePult-Ordner gestartet.')
|
|
|
|
if __name__=='__main__':main()
|