536 lines
No EOL
19 KiB
Python
536 lines
No EOL
19 KiB
Python
#\ SundPood version 0201 /#
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from time import time
|
|
import pygame as pg
|
|
import sounddevice as sd
|
|
from pynput.keyboard import Listener
|
|
from cryptography.fernet import Fernet
|
|
from PyQt5 import QtWidgets, QtGui, QtCore
|
|
from PyQt5.QtCore import QFile, QTextStream
|
|
from PyQt5.QtWidgets import QApplication
|
|
from data import ui_preferences
|
|
from data import ui_hotkeys
|
|
from data import ui_sundpood
|
|
from data import ui_overlay
|
|
from data import keys
|
|
import themes
|
|
import key
|
|
|
|
|
|
###! UI !###
|
|
class OverlayUi(QtWidgets.QMainWindow, ui_overlay.Ui_MainWindow):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
|
self.setGeometry(QtCore.QRect(0, 0, 250, 20))
|
|
self.setupUi(self)
|
|
|
|
def keyPressEvent(self, e):
|
|
if e.key() == QtCore.Qt.Key_F1:
|
|
self.hide()
|
|
win.show()
|
|
|
|
class PreferencesUi(QtWidgets.QMainWindow, ui_preferences.Ui_MainWindow):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
self.setupUi(self)
|
|
|
|
def mousePressEvent(self, event):
|
|
self.offset = event.pos()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
x = event.globalX()
|
|
y = event.globalY()
|
|
x_w = self.offset.x()
|
|
y_w = self.offset.y()
|
|
self.move(x-x_w, y-y_w)
|
|
|
|
def closeEvent(self, event):
|
|
save()
|
|
|
|
class HotkeysUi(QtWidgets.QMainWindow, ui_hotkeys.Ui_MainWindow):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
self.setupUi(self)
|
|
|
|
def mousePressEvent(self, event):
|
|
self.offset = event.pos()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
x = event.globalX()
|
|
y = event.globalY()
|
|
x_w = self.offset.x()
|
|
y_w = self.offset.y()
|
|
self.move(x-x_w, y-y_w)
|
|
|
|
class MainUi(QtWidgets.QMainWindow, ui_sundpood.Ui_MainWindow):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
self.setupUi(self)
|
|
self.volume_slider.valueChanged[int].connect(change_volume)
|
|
|
|
def mousePressEvent(self, event):
|
|
self.offset = event.pos()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
try:
|
|
x = event.globalX()
|
|
y = event.globalY()
|
|
x_w = self.offset.x()
|
|
y_w = self.offset.y()
|
|
self.move(x-x_w, y-y_w)
|
|
except AttributeError:
|
|
pass
|
|
|
|
def keyPressEvent(self, e):
|
|
if e.key() == QtCore.Qt.Key_F1:
|
|
pref.close()
|
|
self.hide()
|
|
over.show()
|
|
|
|
def closeEvent(self, event):
|
|
save()
|
|
if os.path.exists('.play'):
|
|
os.remove('.play')
|
|
hotk.close()
|
|
pref.close()
|
|
|
|
|
|
###! JSON !###
|
|
def jsonread(file):
|
|
'''
|
|
Чтение JSON файла file
|
|
file - имя JSON файла, может содержать полный или относительный путь
|
|
'''
|
|
with open(file, "r", encoding='utf-8') as read_file:
|
|
data = json.load(read_file)
|
|
return data
|
|
|
|
def jsonwrite(file, data):
|
|
'''
|
|
Запись в JSON файл file данные data
|
|
file - имя JSON файла, может содержать полный или относительный путь
|
|
data - данные, может быть словарем/списком/кортежем/строкой/числом
|
|
'''
|
|
with open(file, 'w', encoding='utf-8') as write_file:
|
|
write_file.write(json.dumps(data))
|
|
|
|
|
|
###! FUNCTIONS !###
|
|
def toggle_stylesheet(path):
|
|
'''
|
|
Toggle the stylesheet to use the desired path in the Qt resource
|
|
system (prefixed by `:/`) or generically (a path to a file on
|
|
system).
|
|
|
|
:path: A full path to a resource or file on system
|
|
'''
|
|
|
|
# get the QApplication instance, or crash if not set
|
|
app = QApplication.instance()
|
|
if app is None:
|
|
raise RuntimeError("No Qt Application found.")
|
|
|
|
path = os.path.join('themes', path)
|
|
file = QFile(path)
|
|
file.open(QFile.ReadOnly | QFile.Text)
|
|
stream = QTextStream(file)
|
|
theme = stream.readAll()
|
|
|
|
pref.setStyleSheet(theme)
|
|
win.setStyleSheet(theme)
|
|
hotk.setStyleSheet(theme)
|
|
|
|
def find_device():
|
|
'''
|
|
Ищем устройство device и возвращаем его индекс
|
|
'''
|
|
|
|
list_ = list(sd.query_devices()) # Список устройств вывода звука
|
|
device = ["CABLE Input (VB-Audio Virtual Cable)",
|
|
"VoiceMeeter Input (VB-Audio VoiceMeeter VAIO)"]# Имя искомого устройства
|
|
|
|
msg = QtWidgets.QMessageBox() # Окно ошибки (Устройство не найдено)
|
|
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
|
msg.setText("You don't install VoiceMeeter")
|
|
msg.setInformativeText('install VoiceMetter from "redist" folder or download it from \nvb-audio.com/Voicemeeter')
|
|
msg.setWindowTitle('Error')
|
|
|
|
for i in list_:
|
|
for d in device:
|
|
if d in i['name']:
|
|
return i['name']
|
|
|
|
msg.exec_()
|
|
exit()
|
|
|
|
def get_files(dir_, config):
|
|
'''
|
|
Сбор всех аудифайлов
|
|
dir_ - Путь к папке со звуками
|
|
config - Имя файла настроек
|
|
'''
|
|
msg = QtWidgets.QMessageBox() # Окно ошибки (Не найдены файлы в папке 'sound')
|
|
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
|
msg.setText("You don't have any sounds in 'sound' folder")
|
|
msg.setInformativeText('download sound in .wav / .mp3 / .m4a format')
|
|
msg.setWindowTitle('Error')
|
|
|
|
if os.path.exists(dir_):
|
|
if len(os.listdir(dir_)) == 0:
|
|
msg.exec_()
|
|
|
|
sounds = [] # Все аудио файлы
|
|
sounds_list = [f'{dir_}\\'] # Начальная категория
|
|
|
|
for i in os.listdir(dir_):
|
|
if os.path.isfile(os.path.join(dir_, i)):
|
|
name = os.path.join(dir_, i)
|
|
if name == None:
|
|
sounds_list.append(i)
|
|
elif os.path.splitext(i)[1] in ['.mp3', '.m4a', '.wav']:
|
|
sounds_list.append(i)
|
|
else:
|
|
sounds_list_cat = [os.path.join(dir_, i)]
|
|
for x in os.listdir(os.path.join(dir_, i)):
|
|
if os.path.isfile(os.path.join(dir_, i, x)):
|
|
if name == None:
|
|
sounds_list_cat.append(x)
|
|
elif os.path.splitext(x)[1] in ['.mp3', '.m4a', '.wav']:
|
|
sounds_list_cat.append(x)
|
|
sounds.append(sounds_list_cat)
|
|
sounds.append(sounds_list)
|
|
|
|
if os.path.exists(config):
|
|
hotkeys = jsonread(config)['hotkeys']
|
|
theme = jsonread(config)['Theme']
|
|
KEYS_CMD = jsonread(config)['KEYS_CMD']
|
|
sounds_list = { 'hotkeys':hotkeys,
|
|
'sounds':sounds,
|
|
'Theme':theme,
|
|
'KEYS_CMD':KEYS_CMD}
|
|
else:
|
|
sounds_list = { 'hotkeys':{},
|
|
'sounds':sounds,
|
|
'Theme':'None',
|
|
'KEYS_CMD':{
|
|
'select_move_up' :' ',# вверх
|
|
'select_move_down' :' ',# вниз
|
|
'select_move_left' :' ',# влево
|
|
'select_move_right' :' ',# вправо
|
|
'play_sound' :' ',# Играть
|
|
'stop_sound' :' ',# Остановить
|
|
}}
|
|
|
|
jsonwrite(config, sounds_list)
|
|
else:
|
|
sounds_list = { 'hotkeys':{},
|
|
'sounds':'',
|
|
'Theme':'None',
|
|
'KEYS_CMD':{
|
|
'select_move_up' :' ',# вверх
|
|
'select_move_down' :' ',# вниз
|
|
'select_move_left' :' ',# влево
|
|
'select_move_right' :' ',# вправо
|
|
'play_sound' :' ',# Играть
|
|
'stop_sound' :' ',# Остановить
|
|
}}
|
|
jsonwrite(config, sounds_list)
|
|
os.mkdir(dir_)
|
|
msg.exec_()
|
|
|
|
def change_volume(value):
|
|
pg.mixer.music.set_volume(value/100)
|
|
|
|
def play_sound(*argv):
|
|
if False in argv:
|
|
try:
|
|
sound = os.path.join(win.soundList.item(0).text(),
|
|
win.soundList.currentItem().text())
|
|
except AttributeError:
|
|
return False
|
|
else:
|
|
sound = argv[0]
|
|
try:
|
|
pg.mixer.music.load(sound)
|
|
pg.mixer.music.play()
|
|
pg.event.wait()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
def cat_select(cat):
|
|
win.soundList.clear()
|
|
for i in menu:
|
|
if 'sound' + cat == i[0]:
|
|
win.soundList.addItems(i)
|
|
|
|
def hotkey_remap():
|
|
'''
|
|
Переназначение хоткея
|
|
btn - индекс кнопки хоткея в списке HOTKEYS
|
|
'''
|
|
def check(key):
|
|
key = str(key).replace("'",'')
|
|
try:
|
|
sound = os.path.join(win.soundList.item(0).text(),
|
|
win.soundList.currentItem().text())
|
|
except AttributeError:
|
|
win.hkset.setEnabled(True)
|
|
return False
|
|
|
|
if key not in keys.forbidden:
|
|
hotkeys.update({key:sound})
|
|
elif key == 'Key.backspace':
|
|
del hotkeys[find_key(hotkeys, sound)]
|
|
|
|
save()
|
|
hotk.hotkeyList.clear()
|
|
for i in sound_get_dict['sounds']:
|
|
for x in i:
|
|
x = os.path.join(i[0], x)
|
|
if x in hotkeys.values():
|
|
hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t:{x}')
|
|
|
|
win.hkset.setEnabled(True)
|
|
return False
|
|
|
|
win.hkset.setEnabled(False)
|
|
|
|
hotkey_remap_Listener = Listener(
|
|
on_release=check)
|
|
hotkey_remap_Listener.start()
|
|
|
|
def hotkey_delete():
|
|
key = hotk.hotkeyList.currentItem().text().split(':')[0].replace('\t', '')
|
|
hotkeys.pop(key)
|
|
|
|
save()
|
|
hotk.hotkeyList.clear()
|
|
for i in sound_get_dict['sounds']:
|
|
for x in i:
|
|
x = os.path.join(i[0], x)
|
|
if x in hotkeys.values():
|
|
hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t:{x}')
|
|
|
|
def pref_remap(btn, func_):
|
|
'''
|
|
Переназначение клавиши в окно Preference
|
|
btn - PyQt5 кнопка
|
|
func_ - строковое значени функции из словаря KEYS_CMD
|
|
'''
|
|
def check(key):
|
|
'''
|
|
Проверка кнопки btn на не участие
|
|
в списке запрещенных клавиш keys.forbidden
|
|
если это клавиша 'Key.backspace' то стираем значение
|
|
key - pynput код клавиши
|
|
'''
|
|
key = str(key).replace("'",'')
|
|
if key not in keys.forbidden:
|
|
KEYS_CMD.update({find_key(COMMAND_DICT, func_) : key})
|
|
btn.setText(keys.dict_[key])
|
|
elif key == 'Key.backspace':
|
|
KEYS_CMD.update({find_key(COMMAND_DICT, func_) : ' '})
|
|
btn.setText(' ')
|
|
for i in PREF_BTN:
|
|
i.setEnabled(True)
|
|
save()
|
|
return False
|
|
|
|
for i in PREF_BTN:
|
|
i.setEnabled(False)
|
|
|
|
hotkey_remap_Listener = Listener(
|
|
on_release=check)
|
|
hotkey_remap_Listener.start()
|
|
|
|
def find_key(dict, val):
|
|
'''
|
|
Поиск ключа в словаре dict по значению val
|
|
dict - словарь
|
|
val - значение
|
|
'''
|
|
return next((key for key, value in dict.items() if value == val), None)
|
|
|
|
def check_update():
|
|
def decrypt(filename, key):
|
|
f = Fernet(key)
|
|
with open(filename, 'rb') as file:
|
|
encrypted_data = file.read()
|
|
|
|
decrypted_data = f.decrypt(encrypted_data)
|
|
|
|
return decrypted_data.decode('utf-8')
|
|
|
|
file_ = decrypt(os.path.join('data', 'sundpood-runtime.sr') , key.KEY)
|
|
version = ''
|
|
for i in file_:
|
|
if i != '/':
|
|
version += i
|
|
else:
|
|
break
|
|
print(VERSION)
|
|
return version.split(' ')[3]
|
|
|
|
def save():
|
|
'''
|
|
Сохранение настроек оверлея и глобальных хоткеев
|
|
'''
|
|
sounds = jsonread(config)['sounds']# Все аудиофайлы
|
|
|
|
KEYS_JSON = {} # Настроенные клавиши
|
|
for i in KEYS_CMD.keys():
|
|
KEYS_JSON.setdefault(COMMAND_DICT[i], KEYS_CMD[i])
|
|
|
|
try:
|
|
theme = pref.themesList.currentItem().text()
|
|
except AttributeError:
|
|
theme = jsonread(config)['Theme']
|
|
|
|
sounds_list = {'sounds':sounds, 'hotkeys':hotkeys, 'Theme':theme, 'KEYS_CMD':KEYS_JSON}
|
|
jsonwrite(config, sounds_list)
|
|
|
|
###! CONTROL !###
|
|
def select_move(mode):
|
|
'''
|
|
Перемешение по оверлейному меню
|
|
mode - кортеж из двух цифр
|
|
(смещение по категории, смещение по списку)
|
|
'''
|
|
select[0] += mode[0] # Категории
|
|
select[1] += mode[1] # Файлы в категории
|
|
if select[0] > len(menu)-1 or select[0] < -len(menu)+1:
|
|
select[0] = 0
|
|
if select[1] > len(menu[select[0]])-1 or select[1] < -len(menu[select[0]])+1:
|
|
select[1] = 0
|
|
if mode[0] != 0:
|
|
select[1] = 0
|
|
over.label.setText(menu[select[0]][select[1]])
|
|
win.select_label.setText(menu[select[0]][select[1]])
|
|
|
|
def key_check(key):
|
|
key = str(key).replace("'",'') # Преобразование кода в строку
|
|
key_n = '' # Переведенное значение клавиши
|
|
try:
|
|
key_n = keys.dict_[key]
|
|
except KeyError:
|
|
pass
|
|
if key in hotkeys.keys():
|
|
play_sound(hotkeys[key])
|
|
elif key in KEYS_CMD.values():
|
|
find_key(KEYS_CMD, key)()
|
|
|
|
def main():
|
|
key_check_Listener = Listener(
|
|
on_release=key_check)
|
|
key_check_Listener.start()
|
|
|
|
win.exit_button.clicked.connect(win.close)
|
|
win.min_button.clicked.connect(win.showMinimized)
|
|
pref.exit_button.clicked.connect(pref.close)
|
|
pref.min_button.clicked.connect(pref.showMinimized)
|
|
hotk.exit_button.clicked.connect(hotk.close)
|
|
hotk.min_button.clicked.connect(hotk.showMinimized)
|
|
|
|
win.hkset.clicked.connect(hotkey_remap)
|
|
win.pref_button.clicked.connect(pref.show)
|
|
win.hotkeys_button.clicked.connect(hotk.show)
|
|
win.catList.currentTextChanged.connect(cat_select)
|
|
win.stop_button.clicked.connect(lambda: sd.stop())
|
|
win.play_button.clicked.connect(play_sound)
|
|
|
|
pref.play_sound.clicked.connect(
|
|
lambda: pref_remap(pref.play_sound, 'play_sound'))
|
|
pref.stop_sound.clicked.connect(
|
|
lambda: pref_remap(pref.stop_sound, 'stop_sound'))
|
|
pref.select_move_up.clicked.connect(
|
|
lambda: pref_remap(pref.select_move_up, 'select_move_up'))
|
|
pref.select_move_down.clicked.connect(
|
|
lambda: pref_remap(pref.select_move_down, 'select_move_down'))
|
|
pref.select_move_left.clicked.connect(
|
|
lambda: pref_remap(pref.select_move_left, 'select_move_left'))
|
|
pref.select_move_right.clicked.connect(
|
|
lambda: pref_remap(pref.select_move_right, 'select_move_right'))
|
|
pref.themesList.currentTextChanged.connect(toggle_stylesheet)
|
|
pref.update_button.clicked.connect(check_update)
|
|
|
|
hotk.delete_button.clicked.connect(hotkey_delete)
|
|
|
|
win.show()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
### Создание окна ###
|
|
app = QApplication([]) # Приложение
|
|
over = OverlayUi() # Окно оверлея
|
|
pref = PreferencesUi() # Окно настроек
|
|
hotk = HotkeysUi() # Окно хоткеев
|
|
win = MainUi() # Основное окно
|
|
|
|
PREF_BTN = [ # Список кнопок настроек клавиш в pref
|
|
pref.select_move_up,
|
|
pref.select_move_down,
|
|
pref.select_move_left,
|
|
pref.select_move_right,
|
|
pref.play_sound,
|
|
pref.stop_sound,]
|
|
|
|
### Поиск устроства ввода ###
|
|
pg.mixer.pre_init(44100, -16, 2, 2048) # setup mixer to avoid sound lag
|
|
pg.mixer.init(devicename=find_device()) # Установка устройства вывода по умолчанию
|
|
|
|
### Глобальные переменные ###
|
|
VERSION = 102
|
|
dir_ = 'sound'
|
|
config = 'settings.json'
|
|
get_files(dir_, config) # Сбор всех аудиофайлов
|
|
sound_get_dict = jsonread(config) # Загрузка данных
|
|
hotkeys = sound_get_dict['hotkeys'] # Загрузка словаря хоткеев
|
|
theme = sound_get_dict['Theme'] # Загрузка темы
|
|
menu = sound_get_dict['sounds'] # Загрузка оверлейного меню
|
|
select = [0, 0] # Установка курсора оверлея в нулевую позицию
|
|
|
|
if theme != 'None':
|
|
toggle_stylesheet(theme)
|
|
pref.themesList.addItems(os.listdir('themes'))
|
|
|
|
for i in sound_get_dict['sounds']:
|
|
win.catList.addItem(i[0].replace('sound', ''))
|
|
|
|
for x in i:
|
|
x = os.path.join(i[0], x)
|
|
if x in hotkeys.values():
|
|
hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t: {x}')
|
|
|
|
|
|
COMMAND_DICT = { # Словарь функций к строковому значению
|
|
lambda: select_move((0, -1)):'select_move_up', # вверх
|
|
lambda: select_move((0, 1)) :'select_move_down', # вниз
|
|
lambda: select_move((-1, 0)):'select_move_left', # влево
|
|
lambda: select_move((1, 0)) :'select_move_right', # вправо
|
|
lambda: play_sound(os.path.join(menu[select[0]][0],
|
|
menu[select[0]][select[1]])):'play_sound', # Играть
|
|
lambda: sd.stop() :'stop_sound', # Остановить
|
|
}
|
|
KEYS_JSON = sound_get_dict['KEYS_CMD']# Загрузка настроенных клавиш
|
|
KEYS_CMD = COMMAND_DICT.copy() # Настроенные клавиши
|
|
|
|
### Установка настроенных клавиш ###
|
|
for i in KEYS_CMD.keys():
|
|
KEYS_CMD.update({i:KEYS_JSON[COMMAND_DICT[i]]})
|
|
KEYS_JSON = None
|
|
|
|
combo = 0
|
|
for i in KEYS_CMD.values():
|
|
PREF_BTN[combo].setText(keys.dict_[i])
|
|
combo += 1
|
|
|
|
main()
|
|
|
|
sys.exit(app.exec()) |