diff --git a/pyproject.toml b/pyproject.toml index 16d2fe4..8a1631c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,8 @@ pymediainfo = { version = "^6.1.0", optional = true } pyyaml = { version = "^6.0.1", optional = true } alt-profanity-check = {version = "^1.4.1", optional = true} pyqt6 = {version="^6.7.1", optional = true} -mpv = "^1.0.7" -qasync = "^0.27.1" +mpv = {version = "^1.0.7", optional = true} +qasync = {version = "^0.27.1", optional = true} [tool.poetry.group.dev.dependencies] types-pyyaml = "^6.0.12.12" diff --git a/resources/windows/download_and_build.sh b/resources/windows/download_and_build.sh index 273087b..183d353 100755 --- a/resources/windows/download_and_build.sh +++ b/resources/windows/download_and_build.sh @@ -5,10 +5,13 @@ mkdir -p requirements cd requirements # download mpv -wget https://nightly.link/mpv-player/mpv/workflows/build/master/mpv-x86_64-windows-msvc.zip -unzip mpv-x86_64-windows-msvc.zip -cp mpv.exe ../src -cp vulkan-1.dll ../src +# wget https://nightly.link/mpv-player/mpv/workflows/build/master/mpv-x86_64-windows-msvc.zip +# unzip mpv-x86_64-windows-msvc.zip +# cp mpv.exe ../src +# cp vulkan-1.dll ../src +wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20241118/mpv-dev-x86_64-20241118-git-e8fd7b8.7z +7z x mpv-dev-x86_64-20241118-git-e8fd7b8.7z +cp libmpv-2.dll ../src # download ffmpeg wget https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z @@ -27,7 +30,7 @@ cp ../icons/syng.ico src/ # rm -rf src/build # rm -rf src/dist # docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-windows:latest "pyinstaller --onefile -w -i'.\syng.ico' --add-data='.\syng\static\syng.png;.\static' --add-binary '.\mpv.exe;.' --add-binary '.\vulkan-1.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py" -docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-windows:latest "pyinstaller -F -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-binary '.\mpv.exe;.' --add-binary '.\vulkan-1.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py" +docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-windows:latest "pyinstaller -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-binary '.\libmpv-2.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py" # cd syng-2.0.1 # wine python -m poetry install -E client diff --git a/syng/client.py b/syng/client.py index 3e1a92a..f460274 100644 --- a/syng/client.py +++ b/syng/client.py @@ -21,7 +21,6 @@ from logging.handlers import QueueHandler from multiprocessing import Queue import secrets import string -import tempfile import signal from argparse import Namespace from dataclasses import dataclass @@ -35,7 +34,6 @@ from qrcode.main import QRCode import socketio from socketio.exceptions import ConnectionError import engineio -from PIL import Image from yaml import load, Loader from syng.player_libmpv import Player diff --git a/syng/gui.py b/syng/gui.py index 4507524..352a2d4 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -5,7 +5,6 @@ import logging from logging.handlers import QueueListener from logging.handlers import QueueHandler -# from multiprocessing import Process, Queue from queue import Queue from collections.abc import Callable from datetime import datetime @@ -13,7 +12,6 @@ import os from functools import partial import random from typing import TYPE_CHECKING, Any, Optional -import threading import secrets import string import signal @@ -32,7 +30,6 @@ from qasync import QEventLoop, QApplication from PyQt6.QtCore import QTimer, Qt from PyQt6.QtGui import QCloseEvent, QIcon, QPixmap from PyQt6.QtWidgets import ( - # QApplication, QCheckBox, QComboBox, QDateTimeEdit, @@ -58,7 +55,7 @@ from qrcode.main import QRCode import platformdirs from . import resources # noqa -from .client import Client, create_async_and_start_client, default_config +from .client import Client, default_config from .log import logger from .sources import available_sources @@ -73,16 +70,6 @@ from .config import ( StrOption, ) -# try: -# from .server import run_server -# -# SERVER_AVAILABLE = True -# except ImportError: -# if TYPE_CHECKING: -# from .server import run_server -# -# SERVER_AVAILABLE = False - # TODO: ScrollableFrame class OptionFrame(QWidget): @@ -482,14 +469,8 @@ class GeneralConfig(OptionFrame): class SyngGui(QMainWindow): def closeEvent(self, a0: Optional[QCloseEvent]) -> None: - # if self.syng_server is not None: - # self.syng_server.kill() - # self.syng_server.join() - - # if self.syng_client is not None: - # self.syng_client.terminate() - # self.syng_client.join(1.0) - # self.syng_client.kill() + if self.client is not None: + self.client.quit_callback() self.destroy() @@ -497,10 +478,6 @@ class SyngGui(QMainWindow): self.buttons_layout = QHBoxLayout() self.central_layout.addLayout(self.buttons_layout) - # self.startsyng_serverbutton = QPushButton("Start Local Server") - # self.startsyng_serverbutton.clicked.connect(self.start_syng_server) - # self.buttons_layout.addWidget(self.startsyng_serverbutton) - self.resetbutton = QPushButton("Set Config to Default") self.exportbutton = QPushButton("Export Config") self.importbutton = QPushButton("Import Config") @@ -521,11 +498,7 @@ class SyngGui(QMainWindow): self.buttons_layout.addWidget(self.show_advanced_toggle) spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - # self.notification_label = QLabel("", self) - # spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.buttons_layout.addItem(spacer_item) - # self.buttons_layout.addWidget(self.notification_label) - # self.buttons_layout.addItem(spacer_item2) self.savebutton = QPushButton("Save") self.savebutton.clicked.connect(self.save_config) @@ -632,8 +605,6 @@ class SyngGui(QMainWindow): self.setWindowIcon(QIcon(":/icons/syng.ico")) self.loop = asyncio.get_event_loop() - # self.syng_server: Optional[threading.Thread] = None - # self.syng_client: Optional[threading.Thread] = None self.client: Optional[Client] = None self.syng_client_logging_listener: Optional[QueueListener] = None @@ -658,7 +629,6 @@ class SyngGui(QMainWindow): self.setCentralWidget(self.central_widget) - # check every 500 ms if client is running self.timer = QTimer() self.timer.timeout.connect(self.check_if_client_is_running) @@ -781,48 +751,16 @@ class SyngGui(QMainWindow): ) self.syng_client_logging_listener.start() - # self.syng_client = multiprocessing.Process( - # target=create_async_and_start_client, args=[config, queue] - # ) logger.addHandler(QueueHandler(queue)) self.client = Client(config) asyncio.run_coroutine_threadsafe(self.client.start_client(config), self.loop) - # self.syng_client = threading.Thread( - # target=create_async_and_start_client, args=[config, queue, self.client] - # ) - # self.syng_client.start() self.notification_label.setText("") self.timer.start(500) self.set_client_button_stop() else: self.client.quit_callback() - # self.syng_client.join(1.0) self.set_client_button_start() - # def start_syng_server(self) -> None: - # if self.syng_server is None: - # root_path = os.path.join(os.path.dirname(__file__), "static") - # self.syng_server = multiprocessing.Process( - # target=run_server, - # args=[ - # Namespace( - # host="0.0.0.0", - # port=8080, - # registration_keyfile=None, - # root_folder=root_path, - # private=False, - # restricted=False, - # ) - # ], - # ) - # self.syng_server.start() - # self.startsyng_serverbutton.setText("Stop Local Server") - # else: - # self.syng_server.terminate() - # self.syng_server.join() - # self.syng_server = None - # self.startsyng_serverbutton.setText("Start Local Server") - def change_qr(self, data: str) -> None: qr = QRCode(box_size=10, border=2) qr.add_data(data) @@ -876,7 +814,6 @@ def run_gui() -> None: app.setDesktopFileName("rocks.syng.Syng") window = SyngGui() window.show() - # app.exec() with event_loop: event_loop.run_forever() diff --git a/syng/server.py b/syng/server.py index fbe18e9..70fb98a 100644 --- a/syng/server.py +++ b/syng/server.py @@ -477,8 +477,8 @@ class Server: "source": data["source"], "performer": data["performer"], "ident": data["ident"], - "artist": data["artist"], - "title": data["title"], + "artist": data.get("artist", None), + "title": data.get("title", None), }, "old_entry": { "artist": old_entry.artist, @@ -493,7 +493,10 @@ class Server: source_obj = state.client.sources[data["source"]] entry = await source_obj.get_entry( - data["performer"], data["ident"], artist=data["artist"], title=data["title"] + data["performer"], + data["ident"], + artist=data.get("artist", None), + title=data.get("title", None), ) if entry is None: diff --git a/syng/sources/source.py b/syng/sources/source.py index 8a4c039..fe73db0 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -127,40 +127,11 @@ class Source(ABC): """ self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry) self._masterlock: asyncio.Lock = asyncio.Lock() - # self.player: Optional[asyncio.subprocess.Process] = None self._index: list[str] = config["index"] if "index" in config else [] self.extra_mpv_arguments: list[str] = [] self.extra_mpv_options: dict[str, str] = {} self._skip_next = False - @staticmethod - async def play_mpv( - video: str, audio: Optional[str], /, *options: str - ) -> asyncio.subprocess.Process: - """ - Create a mpv process to play a song in full screen. - - :param video: Location of the video part. - :type video: str - :param audio: Location of the audio part, if it exists. - :type audio: Optional[str] - :param options: Extra arguments forwarded to the mpv player - :type options: str - :returns: An async reference to the process - :rtype: asyncio.subprocess.Process - """ - args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else []) - - # print(f"File is {video=} and {audio=}") - - mpv_process = asyncio.create_subprocess_exec( - "mpv", - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - return await mpv_process - async def get_entry( self, performer: str, @@ -299,22 +270,10 @@ class Source(ABC): entry.skip = True return - extra_options = ( - (self.extra_mpv_arguments + [mpv_options]) - if mpv_options - else self.extra_mpv_arguments - ) - - # self.player = await self.play_mpv( - # self.downloaded_files[entry.ident].video, - # self.downloaded_files[entry.ident].audio, - # *extra_options, - # ) await player.play( self.downloaded_files[entry.ident].video, self.downloaded_files[entry.ident].audio ) - # await self.player.wait() - # self.player = None + if self._skip_next: self._skip_next = False entry.skip = True @@ -339,9 +298,6 @@ class Source(ABC): buffer_task.cancel() self.downloaded_files[entry.ident].ready.set() - # if self.player is not None: - # self.player.kill() - async def ensure_playable(self, entry: Entry) -> tuple[str, Optional[str]]: """ Guaranties that the given entry can be played. diff --git a/syng/sources/youtube.py b/syng/sources/youtube.py index 5405a37..2159572 100644 --- a/syng/sources/youtube.py +++ b/syng/sources/youtube.py @@ -103,12 +103,6 @@ class YouTube: :type search_result: dict[str, Any] """ url = search_result["url"] - # cls.__cache__[url] = { - # "duration": int(search_result["duration"]), - # "title": search_result["title"], - # "channel": search_result["channel"], - # "url": url, - # } return cls(url, info=search_result) @@ -255,15 +249,6 @@ class YoutubeSource(Source): :rtype: None """ if self.start_streaming and not self.downloaded_files[entry.ident].complete: - # self.player = await self.play_mpv( - # entry.ident, - # None, - # "--script-opts=ytdl_hook-ytdl_path=yt-dlp,ytdl_hook-exclude='%.pls$'", - # f"--ytdl-format={self.formatstring}", - # "--fullscreen", - # mpv_options, - # ) - # await self.player.wait() await player.play(entry.ident) else: await super().play(entry, player, mpv_options) diff --git a/typings/qasync.pyi b/typings/qasync.pyi index ac97255..d5cfaa5 100644 --- a/typings/qasync.pyi +++ b/typings/qasync.pyi @@ -1,4 +1,17 @@ -from asyncio import AbstractEventLoop +from types import TracebackType +from typing import Optional +import PyQt6.QtWidgets +from asyncio import BaseEventLoop -class QApplication: +class QApplication(PyQt6.QtWidgets.QApplication): def __init__(self, argv: list[str]) -> None: ... + +class QEventLoop(BaseEventLoop): + def __init__(self, app: QApplication) -> None: ... + def __enter__(self) -> None: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: ...