From 9dae35a5373d5bac497c3ac281e420a16bb104ef Mon Sep 17 00:00:00 2001 From: BarsTiger Date: Mon, 12 Dec 2022 23:04:17 +0200 Subject: [PATCH] Added downloading from spotify. TODO: Two playing options - old stream and new download --- gui/gui.py | 47 ++++++++++++ gui/gui.ui | 97 +++++++++++++++++++++++++ gui/modules/handlers/register.py | 2 +- gui/modules/initialize/fill_settings.py | 23 +++++- gui/modules/initialize/setup_ui.py | 12 +-- gui/modules/menu/handlers.py | 5 -- gui/modules/menu/menu.py | 10 --- gui/modules/player/handlers.py | 13 +--- gui/modules/settings/handlers.py | 21 ++++-- modules/player/add_second.py | 30 -------- modules/player/convert.py | 42 +++++++++++ modules/player/player.py | 19 +++-- modules/spotify/__init__.py | 0 modules/spotify/config.py | 51 +++++++++++++ modules/spotify/spotify_dl.py | 57 +++++++++++++++ 15 files changed, 351 insertions(+), 78 deletions(-) delete mode 100644 modules/player/add_second.py create mode 100644 modules/player/convert.py create mode 100644 modules/spotify/__init__.py create mode 100644 modules/spotify/config.py create mode 100644 modules/spotify/spotify_dl.py diff --git a/gui/gui.py b/gui/gui.py index 83717e6..f7c924b 100644 --- a/gui/gui.py +++ b/gui/gui.py @@ -270,6 +270,10 @@ class Ui_MainWindow(object): self.output_device_restream_box.setObjectName("output_device_restream_box") self.verticalLayout.addWidget(self.output_device_restream_box) self.audio_devices_settings_tab_lay.addWidget(self.restream_options_group, 0, QtCore.Qt.AlignTop) + self.use_original_streaming_method_check = QtWidgets.QCheckBox(self.audio_devices_settings_tab) + self.use_original_streaming_method_check.setChecked(True) + self.use_original_streaming_method_check.setObjectName("use_original_streaming_method_check") + self.audio_devices_settings_tab_lay.addWidget(self.use_original_streaming_method_check) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.audio_devices_settings_tab_lay.addItem(spacerItem) self.settings_tabs_widget.addTab(self.audio_devices_settings_tab, "") @@ -288,6 +292,41 @@ class Ui_MainWindow(object): self.theme_box.addItem("") self.theme_box.addItem("") self.general_settings_tab_lay.addWidget(self.theme_box) + self.api_keys_settings_label = QtWidgets.QLabel(self.general_settings_tab) + self.api_keys_settings_label.setObjectName("api_keys_settings_label") + self.general_settings_tab_lay.addWidget(self.api_keys_settings_label) + self.api_keys_settings_tabs_widget = QtWidgets.QTabWidget(self.general_settings_tab) + self.api_keys_settings_tabs_widget.setObjectName("api_keys_settings_tabs_widget") + self.spotify_api_settings_tab = QtWidgets.QWidget() + self.spotify_api_settings_tab.setObjectName("spotify_api_settings_tab") + self.spotify_api_settings_tab_lay = QtWidgets.QVBoxLayout(self.spotify_api_settings_tab) + self.spotify_api_settings_tab_lay.setContentsMargins(0, 0, 0, 0) + self.spotify_api_settings_tab_lay.setObjectName("spotify_api_settings_tab_lay") + self.spotify_client_id_label = QtWidgets.QLabel(self.spotify_api_settings_tab) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spotify_client_id_label.sizePolicy().hasHeightForWidth()) + self.spotify_client_id_label.setSizePolicy(sizePolicy) + self.spotify_client_id_label.setMaximumSize(QtCore.QSize(16777215, 50)) + self.spotify_client_id_label.setObjectName("spotify_client_id_label") + self.spotify_api_settings_tab_lay.addWidget(self.spotify_client_id_label) + self.spotify_client_id_box = QtWidgets.QLineEdit(self.spotify_api_settings_tab) + self.spotify_client_id_box.setMinimumSize(QtCore.QSize(0, 30)) + self.spotify_client_id_box.setObjectName("spotify_client_id_box") + self.spotify_api_settings_tab_lay.addWidget(self.spotify_client_id_box) + self.spotify_client_secret_label = QtWidgets.QLabel(self.spotify_api_settings_tab) + self.spotify_client_secret_label.setObjectName("spotify_client_secret_label") + self.spotify_api_settings_tab_lay.addWidget(self.spotify_client_secret_label) + self.spotify_client_secret_box = QtWidgets.QLineEdit(self.spotify_api_settings_tab) + self.spotify_client_secret_box.setMinimumSize(QtCore.QSize(0, 30)) + self.spotify_client_secret_box.setObjectName("spotify_client_secret_box") + self.spotify_api_settings_tab_lay.addWidget(self.spotify_client_secret_box) + self.api_keys_settings_tabs_widget.addTab(self.spotify_api_settings_tab, "") + self.pusher_settings_tab = QtWidgets.QWidget() + self.pusher_settings_tab.setObjectName("pusher_settings_tab") + self.api_keys_settings_tabs_widget.addTab(self.pusher_settings_tab, "") + self.general_settings_tab_lay.addWidget(self.api_keys_settings_tabs_widget, 0, QtCore.Qt.AlignTop) spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.general_settings_tab_lay.addItem(spacerItem1) self.clear_temp_button = QtWidgets.QPushButton(self.general_settings_tab) @@ -391,11 +430,19 @@ class Ui_MainWindow(object): self.restream_micro_checkbox.setText(_translate("MainWindow", "Restream microphone")) self.input_device_restream_label.setText(_translate("MainWindow", "Input microphone")) self.output_device_restream_label.setText(_translate("MainWindow", "Output device (virtual mic input)")) + self.use_original_streaming_method_check.setText(_translate("MainWindow", "Use direct stream method (cuts last second of audio, not recommended if playing short files)")) self.settings_tabs_widget.setTabText(self.settings_tabs_widget.indexOf(self.audio_devices_settings_tab), _translate("MainWindow", "Audio")) self.theme_label.setText(_translate("MainWindow", " App theme (restart needed)")) self.theme_box.setItemText(0, _translate("MainWindow", "Dark gray")) self.theme_box.setItemText(1, _translate("MainWindow", "Black")) self.theme_box.setItemText(2, _translate("MainWindow", "Black acrylic")) + self.api_keys_settings_label.setText(_translate("MainWindow", "API keys settings")) + self.spotify_client_id_label.setText(_translate("MainWindow", "Client id")) + self.spotify_client_id_box.setPlaceholderText(_translate("MainWindow", "5f573c9620494bae87890c0f08a60293")) + self.spotify_client_secret_label.setText(_translate("MainWindow", "Client secret")) + self.spotify_client_secret_box.setPlaceholderText(_translate("MainWindow", "212476d9b0f3472eaa762d90b19b0ba8")) + self.api_keys_settings_tabs_widget.setTabText(self.api_keys_settings_tabs_widget.indexOf(self.spotify_api_settings_tab), _translate("MainWindow", "Spotify")) + self.api_keys_settings_tabs_widget.setTabText(self.api_keys_settings_tabs_widget.indexOf(self.pusher_settings_tab), _translate("MainWindow", "Pusher")) self.clear_temp_button.setText(_translate("MainWindow", "Clear KotoPad temporary files (use if sound doesn\'t play correctly)")) self.settings_tabs_widget.setTabText(self.settings_tabs_widget.indexOf(self.general_settings_tab), _translate("MainWindow", "General")) import gui.images_rc diff --git a/gui/gui.ui b/gui/gui.ui index 5b15d4a..68951ae 100644 --- a/gui/gui.ui +++ b/gui/gui.ui @@ -850,6 +850,16 @@ QListWidget:item:selected { + + + + Use direct stream method (cuts last second of audio, not recommended if playing short files) + + + true + + + @@ -914,6 +924,93 @@ QListWidget:item:selected { + + + + API keys settings + + + + + + + + Spotify + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 50 + + + + Client id + + + + + + + + 0 + 30 + + + + 5f573c9620494bae87890c0f08a60293 + + + + + + + Client secret + + + + + + + + 0 + 30 + + + + 212476d9b0f3472eaa762d90b19b0ba8 + + + + + + + + Pusher + + + + diff --git a/gui/modules/handlers/register.py b/gui/modules/handlers/register.py index a1c332d..9adb05a 100644 --- a/gui/modules/handlers/register.py +++ b/gui/modules/handlers/register.py @@ -12,6 +12,6 @@ from modules.restream.restream import Restreamer def register_handlers(ui: Ui_MainWindow, MainWindow: QMainWindow, p: Player, rs: Restreamer): menu.register_handlers(ui) pads.register_handlers(ui, MainWindow, p) - player.register_handlers(ui, MainWindow, p) + player.register_handlers(ui, p) settings.register_handlers(ui) restreammic.register_handlers(ui, MainWindow, rs) diff --git a/gui/modules/initialize/fill_settings.py b/gui/modules/initialize/fill_settings.py index 8fca869..4dc6406 100644 --- a/gui/modules/initialize/fill_settings.py +++ b/gui/modules/initialize/fill_settings.py @@ -1,6 +1,8 @@ from gui.gui import Ui_MainWindow from modules.config import Config +from modules.spotify.config import SpotifyConfig from modules.restream import get_streaming_devices +from modules.player.player import get_devices, get_instance, get_player def fill_settings(ui: Ui_MainWindow): @@ -8,7 +10,23 @@ def fill_settings(ui: Ui_MainWindow): ui.theme_box.setCurrentText(Config.get().theme) - # ui.output_device_play_box.addItems() + play_devices = get_devices(get_player(get_instance())) + + ui.output_device_play_box.addItems(play_devices) + ui.preview_device_play_box.addItems(play_devices) + + if Config.get().out_device in play_devices.keys(): + ui.output_device_play_box.setCurrentText(Config.get().out_device) + else: + for item in play_devices.keys(): + if '(VB-Audio Virtual Cable)' in item: + ui.output_device_play_box.setCurrentText(item) + break + + if Config.get().preview_device in play_devices.keys(): + ui.preview_device_play_box.setCurrentText(Config.get().preview_device) + elif 'Default' in play_devices.keys(): + ui.preview_device_play_box.setCurrentText('Default') ui.restream_micro_checkbox.setChecked(Config.get().restream) ui.input_device_restream_box.addItems(get_streaming_devices().in_l) @@ -19,3 +37,6 @@ def fill_settings(ui: Ui_MainWindow): if Config.get().out_micro in get_streaming_devices().out_l: ui.output_device_restream_box.setCurrentText(Config.get().out_micro) + + ui.spotify_client_id_box.setText(SpotifyConfig.get().client_id) + ui.spotify_client_secret_box.setText(SpotifyConfig.get().client_secret) diff --git a/gui/modules/initialize/setup_ui.py b/gui/modules/initialize/setup_ui.py index 4ca76ed..32fd976 100644 --- a/gui/modules/initialize/setup_ui.py +++ b/gui/modules/initialize/setup_ui.py @@ -11,12 +11,6 @@ from modules.restream.restream import Restreamer def on_load(ui: Ui_MainWindow, MainWindow: QMainWindow): - """ - Setup all UI elements - :param ui: - :param MainWindow: - :return: - """ ui.content.setCurrentIndex(0) MainWindow.setStyleSheet(styles.centralwidget()) @@ -27,11 +21,11 @@ def on_load(ui: Ui_MainWindow, MainWindow: QMainWindow): ui.timer = QtCore.QTimer(MainWindow) ui.timer.start(100) - p = Player() - rs = Restreamer() - fill_settings.fill_settings(ui) + p = Player(ui) + rs = Restreamer() + (lambda: rs.restart(ui) if ui.restream_micro_checkbox.isChecked() else rs.stop())() register.register_handlers(ui, MainWindow, p, rs) diff --git a/gui/modules/menu/handlers.py b/gui/modules/menu/handlers.py index dd517ab..076681a 100644 --- a/gui/modules/menu/handlers.py +++ b/gui/modules/menu/handlers.py @@ -3,9 +3,4 @@ from .menu import handle_menu_click def register_handlers(ui: Ui_MainWindow): - """ - Register this module handlers - :param ui: - :return: - """ ui.menu.itemClicked.connect(lambda: handle_menu_click(ui.menu.currentItem().text(), ui)) diff --git a/gui/modules/menu/menu.py b/gui/modules/menu/menu.py index 0dad797..ca2b59f 100644 --- a/gui/modules/menu/menu.py +++ b/gui/modules/menu/menu.py @@ -3,10 +3,6 @@ from gui.gui import Ui_MainWindow def open_menu(ui: Ui_MainWindow) -> None: - """ - Animates the menu to open and close, using animation from config - :return: - """ width = ui.menu.geometry().width() Ui_MainWindow.animation = QtCore.QPropertyAnimation(ui.menu, b"minimumWidth") Ui_MainWindow.animation.setDuration(300) @@ -22,12 +18,6 @@ def open_menu(ui: Ui_MainWindow) -> None: def handle_menu_click(text: str, ui: Ui_MainWindow) -> None: - """ - Handles the click on the menu and changes the page - :param text: - :param ui: - :return: - """ index = { "Close menu": [lambda _: open_menu(ui), None], "Pads": [ui.content.setCurrentWidget, ui.pads_page], diff --git a/gui/modules/player/handlers.py b/gui/modules/player/handlers.py index af0e9af..c08f0c1 100644 --- a/gui/modules/player/handlers.py +++ b/gui/modules/player/handlers.py @@ -1,19 +1,10 @@ from gui.gui import Ui_MainWindow from PyQt5 import QtGui -from PyQt5.QtWidgets import QMainWindow from modules.player.player import Player from modules.config import Config -def register_handlers(ui: Ui_MainWindow, MainWindow: QMainWindow, p: Player): - """ - Register this module handlers - :param p: - :param ui: - :param MainWindow: - :return: - """ - +def register_handlers(ui: Ui_MainWindow, p: Player): play_icon = QtGui.QIcon() play_icon.addPixmap(QtGui.QPixmap(":/img/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) @@ -27,3 +18,5 @@ def register_handlers(ui: Ui_MainWindow, MainWindow: QMainWindow, p: Player): ui.play_pause_button.setIcon(play_icon) if not p.mediaplayer_out.is_playing() else ui.play_pause_button.setIcon(pause_icon))) ui.player_time_slider.sliderPressed.connect(lambda: p.set_position(ui.player_time_slider.value() / 100.0)) + ui.output_device_play_box.currentTextChanged.connect(lambda: p.update_devices(ui)) + ui.preview_device_play_box.currentTextChanged.connect(lambda: p.update_devices(ui)) diff --git a/gui/modules/settings/handlers.py b/gui/modules/settings/handlers.py index e5cbaf4..4a59dc9 100644 --- a/gui/modules/settings/handlers.py +++ b/gui/modules/settings/handlers.py @@ -1,15 +1,10 @@ from gui.gui import Ui_MainWindow from modules.config import Config +from modules.spotify.config import SpotifyConfig import shutil def register_handlers(ui: Ui_MainWindow): - """ - Register this module handlers - :param ui: - :return: - """ - ui.theme_box.currentTextChanged.connect(lambda: Config.update("theme", ui.theme_box.currentText())) ui.restream_micro_checkbox.clicked.connect(lambda: Config.update("restream", ui.restream_micro_checkbox.isChecked())) @@ -21,6 +16,20 @@ def register_handlers(ui: Ui_MainWindow): lambda: Config.update("in_micro", ui.input_device_restream_box.currentText()) ) + ui.output_device_play_box.currentTextChanged.connect( + lambda: Config.update("out_device", ui.output_device_play_box.currentText()) + ) + ui.preview_device_play_box.currentTextChanged.connect( + lambda: Config.update("preview_device", ui.preview_device_play_box.currentText()) + ) + + ui.spotify_client_id_box.textChanged.connect( + lambda: SpotifyConfig.update("client_id", ui.spotify_client_id_box.text()) + ) + ui.spotify_client_secret_box.textChanged.connect( + lambda: SpotifyConfig.update("client_secret", ui.spotify_client_secret_box.text()) + ) + ui.clear_temp_button.clicked.connect( lambda: shutil.rmtree('temp', ignore_errors=True) ) diff --git a/modules/player/add_second.py b/modules/player/add_second.py deleted file mode 100644 index a02cc0d..0000000 --- a/modules/player/add_second.py +++ /dev/null @@ -1,30 +0,0 @@ -import pydub -import validators -from urllib.request import urlopen -from io import BytesIO -from rich import print -import pafy -import hashlib -import os - - -def get_silenced_media(original: str) -> str | None: - if not os.path.isdir('temp'): - os.mkdir('temp') - - try: - name = original - namehash = 'temp\\' + hashlib.md5(name.encode('utf-8')).hexdigest() - if not os.path.isfile(namehash): - if validators.url(original): - if 'youtu' in original: - original = pafy.new(original).getbestaudio().url - original = BytesIO(urlopen(original).read()) - - (pydub.AudioSegment.from_file(original) + pydub.AudioSegment.silent(1500))\ - .export(namehash, format='mp3') - return namehash - - except Exception as e: - print(e) - return None diff --git a/modules/player/convert.py b/modules/player/convert.py new file mode 100644 index 0000000..ee7a2d1 --- /dev/null +++ b/modules/player/convert.py @@ -0,0 +1,42 @@ +import pydub +import validators +from io import BytesIO +from rich import print +import pafy +import hashlib +import os +from modules.spotify.spotify_dl import Spotify +from gui.modules.core.popup import popup +import requests + + +def get_raw_link(url): + if validators.url(url): + if 'spotify' in url: + url = Spotify().get_youtube_url(url) + if 'youtu' in url: + url = pafy.new(url).audiostreams[0].url + else: + url = None + + return url + + +def get_silenced_media(original: str) -> str | None: + if not os.path.isdir('temp'): + os.mkdir('temp') + + try: + namehash = 'temp\\' + hashlib.md5(original.encode('utf-8')).hexdigest() + if not os.path.isfile(original): + if validators.url(original): + original = BytesIO(requests.get(get_raw_link(original)).content) + + (pydub.AudioSegment.from_file(original) + pydub.AudioSegment.silent(1500))\ + .export(namehash, format='mp3') + return namehash + + except Exception as e: + raise e + print(e) + return None diff --git a/modules/player/player.py b/modules/player/player.py index d5b1a6d..ae025d2 100644 --- a/modules/player/player.py +++ b/modules/player/player.py @@ -1,7 +1,7 @@ import vlc from gui.gui import Ui_MainWindow from gui.modules.core import popup -from modules.player.add_second import get_silenced_media +from modules.player.convert import get_silenced_media def get_instance() -> vlc.Instance: @@ -24,14 +24,16 @@ def get_devices(mediaplayer: vlc.MediaPlayer) -> dict: class Player(object): - def __init__(self): + def __init__(self, ui: Ui_MainWindow): self.instance_preview = get_instance() self.instance_out = get_instance() self.mediaplayer_preview = get_player(self.instance_preview) - self.mediaplayer_preview.audio_output_device_set(None, get_devices(self.mediaplayer_preview)['Default']) + self.mediaplayer_preview.audio_output_device_set(None, get_devices(self.mediaplayer_preview)[ + ui.preview_device_play_box.currentText() + ]) self.mediaplayer_out = get_player(self.instance_out) self.mediaplayer_out.audio_output_device_set(None, get_devices( - self.mediaplayer_out)['CABLE Input (VB-Audio Virtual Cable)']) + self.mediaplayer_out)[ui.output_device_play_box.currentText()]) def set_media(self, media: str) -> None: if get_silenced_media(media): @@ -77,5 +79,10 @@ class Player(object): self.mediaplayer_preview.set_position(pos) self.mediaplayer_out.set_position(pos) - def update_devices(self): - pass + def update_devices(self, ui: Ui_MainWindow): + self.mediaplayer_preview.audio_output_device_set(None, get_devices(self.mediaplayer_preview)[ + ui.preview_device_play_box.currentText() + ]) + self.mediaplayer_out.audio_output_device_set(None, get_devices(self.mediaplayer_out)[ + ui.output_device_play_box.currentText() + ]) diff --git a/modules/spotify/__init__.py b/modules/spotify/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/spotify/config.py b/modules/spotify/config.py new file mode 100644 index 0000000..447018b --- /dev/null +++ b/modules/spotify/config.py @@ -0,0 +1,51 @@ +import json +import os +from dataclasses import dataclass +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass(frozen=True) +class SpotConfig: + client_id: str + client_secret: str + + +class SpotifyConfig: + @staticmethod + def default(): + return { + "client_id": "5f573c9620494bae87890c0f08a60293", + "client_secret": "212476d9b0f3472eaa762d90b19b0ba8" + } + + @staticmethod + def fix() -> None: + try: + with open("data/config.spotify", "w") as file: + json.dump(SpotifyConfig.default(), file) + except FileNotFoundError: + if not os.path.exists('data'): + os.mkdir('data') + SpotifyConfig.fix() + + @staticmethod + def get() -> SpotConfig: + try: + with open("data/config.spotify", "r") as file: + return SpotConfig.from_dict(json.load(file)) + except: + SpotifyConfig.fix() + return SpotifyConfig.get() + + @staticmethod + def update(key: str, value: str | None) -> dict: + with open("data/config.spotify", "r") as file: + settings = json.load(file) + + settings[key] = value + + with open("data/config.spotify", "w") as file: + json.dump(settings, file) + + return settings diff --git a/modules/spotify/spotify_dl.py b/modules/spotify/spotify_dl.py new file mode 100644 index 0000000..23e14f0 --- /dev/null +++ b/modules/spotify/spotify_dl.py @@ -0,0 +1,57 @@ +import requests +from urllib.parse import urlencode +from urllib.request import urlopen +from urllib.error import HTTPError +import re +from base64 import b64encode +from modules.spotify.config import SpotifyConfig + + +def reencode(text: str): + return b64encode(text.encode()).decode() + + +class Spotify(object): + def __init__(self): + try: + headers = { + 'Authorization': f'Basic ' + f'{reencode(f"{SpotifyConfig.get().client_id}:{SpotifyConfig.get().client_secret}")}', + } + + data = { + 'grant_type': 'client_credentials' + } + + r = requests.post('https://accounts.spotify.com/api/token', headers=headers, data=data) + + self.token = r.json()['access_token'] + except Exception as e: + print(e) + + def get_youtube_url(self, url) -> str | None: + track_id = url.split('/')[-1].split('?')[0] + r = requests.get(f"https://api.spotify.com/v1/tracks/{track_id}", + headers={'Authorization': f'Bearer {self.token}'}) + + if r.status_code == 400 or r.status_code == 401: + return None + + track = r.json() + + song_name = track['name'] + artists = [] + + for artist in track['artists']: + artists.append(artist['name']) + artist_name = ' '.join(artists) + + try: + query_string = urlencode({'search_query': artist_name + ' ' + song_name}) + htm_content = urlopen('http://www.youtube.com/results?' + query_string) + search_results = re.findall(r'/watch\?v=(.{11})', htm_content.read().decode()) + + return f'http://www.youtube.com/watch?v={search_results[0]}' + + except HTTPError: + return None