WIP: preview via pygame working

This commit is contained in:
Keith Edmunds 2024-07-03 15:41:14 +01:00
parent a971298982
commit 9656bac49f
5 changed files with 170 additions and 92 deletions

2
.envrc
View File

@ -4,6 +4,8 @@ export MAIL_PORT=587
export MAIL_SERVER="smtp.fastmail.com" export MAIL_SERVER="smtp.fastmail.com"
export MAIL_USERNAME="kae@midnighthax.com" export MAIL_USERNAME="kae@midnighthax.com"
export MAIL_USE_TLS=True export MAIL_USE_TLS=True
export PYGAME_HIDE_SUPPORT_PROMPT=1
branch=$(git branch --show-current) branch=$(git branch --show-current)
# Always treat running from /home/kae/mm as production # Always treat running from /home/kae/mm as production

View File

@ -39,6 +39,7 @@ from PyQt6.QtWidgets import (
# Third party imports # Third party imports
import pipeclient import pipeclient
from pygame import mixer
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
import stackprinter # type: ignore import stackprinter # type: ignore
@ -142,6 +143,46 @@ class ImportTrack(QObject):
self.import_finished.emit() self.import_finished.emit()
class PreviewManager:
"""
Manage track preview player
"""
def __init__(self) -> None:
mixer.init()
self.path: str = ""
self.start_time: Optional[dt.datetime] = None
def get_playtime(self) -> int:
"""
Return time since track started in milliseconds, 0 if not playing
"""
if not mixer.music.get_busy():
return 0
if not self.start_time:
return 0
return int((dt.datetime.now() - self.start_time).total_seconds() * 1000)
def is_playing(self) -> bool:
return mixer.music.get_busy()
def play(self) -> None:
mixer.music.play()
self.start_time = dt.datetime.now()
def set_path(self, path) -> None:
self.path = path
mixer.music.load(path)
def stop(self) -> None:
mixer.music.stop()
self.path = ""
self.start_time = None
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None, *args, **kwargs) -> None: def __init__(self, parent=None, *args, **kwargs) -> None:
super().__init__(parent) super().__init__(parent)
@ -159,6 +200,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.txtSearch.setHidden(True) self.txtSearch.setHidden(True)
self.statusbar.addWidget(self.txtSearch) self.statusbar.addWidget(self.txtSearch)
self.hide_played_tracks = False self.hide_played_tracks = False
self.preview_manager = PreviewManager()
self.widgetFadeVolume.hideAxis("bottom") self.widgetFadeVolume.hideAxis("bottom")
self.widgetFadeVolume.hideAxis("left") self.widgetFadeVolume.hideAxis("left")
@ -288,7 +330,9 @@ class Window(QMainWindow, Ui_MainWindow):
current_track_playlist_id = track_sequence.current.playlist_id current_track_playlist_id = track_sequence.current.playlist_id
if current_track_playlist_id: if current_track_playlist_id:
if closing_tab_playlist_id == current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id:
helpers.show_OK(self, "Current track", "Can't close current track playlist") helpers.show_OK(
self, "Current track", "Can't close current track playlist"
)
return False return False
# Don't close next track playlist # Don't close next track playlist
@ -296,7 +340,9 @@ class Window(QMainWindow, Ui_MainWindow):
next_track_playlist_id = track_sequence.next.playlist_id next_track_playlist_id = track_sequence.next.playlist_id
if next_track_playlist_id: if next_track_playlist_id:
if closing_tab_playlist_id == next_track_playlist_id: if closing_tab_playlist_id == next_track_playlist_id:
helpers.show_OK(self, "Next track", "Can't close next track playlist") helpers.show_OK(
self, "Next track", "Can't close next track playlist"
)
return False return False
# Record playlist as closed and update remaining playlist tabs # Record playlist as closed and update remaining playlist tabs
@ -1043,42 +1089,23 @@ class Window(QMainWindow, Ui_MainWindow):
def preview(self) -> None: def preview(self) -> None:
""" """
Preview selected or next track. Preview selected or next track. We use a different mechanism to
normal track playing so that the user can route the output audio
differently (eg, to headphones).
""" """
if self.btnPreview.isChecked(): if self.btnPreview.isChecked():
# Get track_id for first selected track if there is one # Get track path for first selected track if there is one
track_id = None track_path = self.active_tab().get_selected_row_track_path()
row_number_and_track_id = self.active_tab().get_selected_row_and_track_id() if not track_path:
if row_number_and_track_id:
row_number, track_id = row_number_and_track_id
else:
# Otherwise get track_id to next track to play # Otherwise get track_id to next track to play
if track_sequence.next: if track_sequence.next:
track_id = track_sequence.next.track_id track_path = track_sequence.next.path
row_number = track_sequence.next.row_number if track_path:
if not track_id or row_number is None: self.preview_manager.set_path(track_path)
self.btnPreview.setChecked(False) self.preview_manager.play()
return
try:
with db.Session() as session:
self.preview_track_manager = PreviewTrackManager(
session=session, track_id=track_id, row_number=row_number
)
self.preview_track_manager.play()
except ValueError as e:
log.error(f"Error creating PreviewTrackManager({str(e)})")
return
else: else:
if self.preview_track_manager: self.preview_manager.stop()
self.preview_track_manager.stop()
self.preview_track_manager = None
self.label_intro_timer.setText("0.0")
self.label_intro_timer.setStyleSheet("")
self.btnPreviewMark.setEnabled(False)
self.btnPreviewArm.setChecked(False)
def preview_arm(self): def preview_arm(self):
"""Manager arm button for setting intro length""" """Manager arm button for setting intro length"""
@ -1088,45 +1115,47 @@ class Window(QMainWindow, Ui_MainWindow):
def preview_back(self) -> None: def preview_back(self) -> None:
"""Wind back preview file""" """Wind back preview file"""
if self.preview_track_manager: # TODO
self.preview_track_manager.move_back() pass
def preview_end(self) -> None: def preview_end(self) -> None:
"""Advance preview file to Config.PREVIEW_END_BUFFER_MS before end of intro""" """Advance preview file to Config.PREVIEW_END_BUFFER_MS before end of intro"""
if self.preview_track_manager: # TODO
self.preview_track_manager.move_to_intro_end() pass
def preview_fwd(self) -> None: def preview_fwd(self) -> None:
"""Advance preview file""" """Advance preview file"""
if self.preview_track_manager: # TODO
self.preview_track_manager.move_forward() pass
def preview_mark(self) -> None: def preview_mark(self) -> None:
"""Set intro time""" """Set intro time"""
if self.preview_track_manager: # TODO
track_id = self.preview_track_manager.track_id pass
row_number = self.preview_track_manager.row_number # if self.preview_track_manager:
with db.Session() as session: # track_id = self.preview_track_manager.track_id
track = session.get(Tracks, track_id) # row_number = self.preview_track_manager.row_number
if track: # with db.Session() as session:
# Save intro as millisends rounded to nearest 0.1 # track = session.get(Tracks, track_id)
# second because editor spinbox only resolves to 0.1 # if track:
# seconds # # Save intro as millisends rounded to nearest 0.1
intro = round(self.preview_track_manager.time_playing() / 100) * 100 # # second because editor spinbox only resolves to 0.1
track.intro = intro # # seconds
session.commit() # intro = round(self.preview_track_manager.time_playing() / 100) * 100
self.preview_track_manager.intro = intro # track.intro = intro
self.active_tab().source_model.refresh_row(session, row_number) # session.commit()
self.active_tab().source_model.invalidate_row(row_number) # self.preview_track_manager.intro = intro
# self.active_tab().source_model.refresh_row(session, row_number)
# self.active_tab().source_model.invalidate_row(row_number)
def preview_start(self) -> None: def preview_start(self) -> None:
"""Restart preview""" """Restart preview"""
if self.preview_track_manager: # TODO
self.preview_track_manager.restart() pass
def rename_playlist(self) -> None: def rename_playlist(self) -> None:
""" """
@ -1477,23 +1506,23 @@ class Window(QMainWindow, Ui_MainWindow):
pass pass
# Ensure preview button is reset if preview finishes playing # Ensure preview button is reset if preview finishes playing
if self.preview_track_manager: # Update preview timer
self.btnPreview.setChecked(self.preview_track_manager.is_playing()) if self.btnPreview.isChecked():
if self.preview_manager.is_playing():
# Update preview timer self.btnPreview.setChecked(True)
if self.preview_track_manager.is_playing(): playtime = self.preview_manager.get_playtime()
playtime = self.preview_track_manager.time_playing()
self.label_intro_timer.setText(f"{playtime / 1000:.1f}") self.label_intro_timer.setText(f"{playtime / 1000:.1f}")
if self.preview_track_manager.time_remaining_intro() <= 50: # if self.preview_track_manager.time_remaining_intro() <= 50:
self.label_intro_timer.setStyleSheet( # self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}" # f"background: {Config.COLOUR_WARNING_TIMER}"
) # )
else: # else:
self.label_intro_timer.setStyleSheet("") # self.label_intro_timer.setStyleSheet("")
else: else:
self.label_intro_timer.setText("0.0") self.btnPreview.setChecked(False)
self.label_intro_timer.setStyleSheet("") self.label_intro_timer.setText("0.0")
self.btnPreview.setChecked(False) self.label_intro_timer.setStyleSheet("")
self.btnPreview.setChecked(False)
def tick_1000ms(self) -> None: def tick_1000ms(self) -> None:
""" """

View File

@ -644,26 +644,6 @@ class PlaylistTab(QTableView):
self.source_model.delete_rows(self.selected_model_row_numbers()) self.source_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection() self.clear_selection()
def get_selected_row_and_track_id(self) -> Optional[tuple[int, int]]:
"""
Return the (row_number, track_id) of the selected row. If no row selected or selected
row does not have a track, return None.
"""
row_number = self.source_model_selected_row_number()
if row_number is None:
result = None
else:
track_id = self.source_model.get_row_track_id(row_number)
if not track_id:
result = None
else:
result = (row_number, track_id)
log.debug(f"get_selected_row_and_track_id() returned: {result=}")
return result
def get_selected_row_track_path(self) -> str: def get_selected_row_track_path(self) -> str:
""" """
Return the path of the selected row. If no row selected or selected Return the path of the selected row. If no row selected or selected

68
poetry.lock generated
View File

@ -1253,6 +1253,72 @@ files = [
{file = "pyfzf-0.3.1.tar.gz", hash = "sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2"}, {file = "pyfzf-0.3.1.tar.gz", hash = "sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2"},
] ]
[[package]]
name = "pygame"
version = "2.6.0"
description = "Python Game Development"
optional = false
python-versions = ">=3.6"
files = [
{file = "pygame-2.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5707aa9d029752495b3eddc1edff62e0e390a02f699b0f1ce77fe0b8c70ea4f"},
{file = "pygame-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3ed0547368733b854c0d9981c982a3cdfabfa01b477d095c57bf47f2199da44"},
{file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6050f3e95f1f16602153d616b52619c6a2041cee7040eb529f65689e9633fc3e"},
{file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89be55b7e9e22e0eea08af9d6cfb97aed5da780f0b3a035803437d481a16d972"},
{file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d65fb222eea1294cfc8206d9e5754d476a1673eb2783c03c4f70e0455320274"},
{file = "pygame-2.6.0-cp310-cp310-win32.whl", hash = "sha256:71eebb9803cb350298de188fb7cdd3ebf13299f78d59a71c7e81efc649aae348"},
{file = "pygame-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1551852a2cd5b4139a752888f6cbeeb4a96fc0fe6e6f3f8b9d9784eb8fceab13"},
{file = "pygame-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6e5e6c010b1bf429388acf4d41d7ab2f7ad8fbf241d0db822102d35c9a2eb84"},
{file = "pygame-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99902f4a2f6a338057200d99b5120a600c27a9f629ca012a9b0087c045508d08"},
{file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a284664978a1989c1e31a0888b2f70cfbcbafdfa3bb310e750b0d3366416225"},
{file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:829623cee298b3dbaa1dd9f52c3051ae82f04cad7708c8c67cb9a1a4b8fd3c0b"},
{file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acf7949ed764487d51123f4f3606e8f76b0df167fef12ef73ef423c35fdea39"},
{file = "pygame-2.6.0-cp311-cp311-win32.whl", hash = "sha256:3f809560c99bd1fb4716610eca0cd36412528f03da1a63841a347b71d0c604ee"},
{file = "pygame-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6897ab87f9193510a774a3483e00debfe166f340ca159f544ef99807e2a44ec4"},
{file = "pygame-2.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b834711ebc8b9d0c2a5f9bfae4403dd277b2c61bcb689e1aa630d01a1ebcf40a"},
{file = "pygame-2.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5ac288655e8a31a303cc286e79cc57979ed2ba19c3a14042d4b6391c1d3bed2"},
{file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d666667b7826b0a7921b8ce0a282ba5281dfa106976c1a3b24e32a0af65ad3b1"},
{file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd8848a37a7cee37854c7efb8d451334477c9f8ce7ac339c079e724dc1334a76"},
{file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:315e7b3c1c573984f549ac5da9778ac4709b3b4e3a4061050d94eab63fa4fe31"},
{file = "pygame-2.6.0-cp312-cp312-win32.whl", hash = "sha256:e44bde0840cc21a91c9d368846ac538d106cf0668be1a6030f48df139609d1e8"},
{file = "pygame-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c429824b1f881a7a5ce3b5c2014d3d182aa45a22cea33c8347a3971a5446907"},
{file = "pygame-2.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b832200bd8b6fc485e087bf3ef7ec1a21437258536413a5386088f5dcd3a9870"},
{file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098029d01a46ea4e30620dfb7c28a577070b456c8fc96350dde05f85c0bf51b5"},
{file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a858bbdeac5ec473ec9e726c55fb8fbdc2f4aad7c55110e899883738071c7c9b"},
{file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f908762941fd99e1f66d1211d26383184f6045c45673443138b214bf48a89aa"},
{file = "pygame-2.6.0-cp36-cp36m-win32.whl", hash = "sha256:4a63daee99d050f47d6ec7fa7dbd1c6597b8f082cdd58b6918d382d2bc31262d"},
{file = "pygame-2.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ace471b3849d68968e5427fc01166ef5afaf552a5c442fc2c28d3b7226786f55"},
{file = "pygame-2.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fea019713d0c89dfd5909225aa933010100035d1cd30e6c936e8b6f00529fb80"},
{file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:249dbf2d51d9f0266009a380ccf0532e1a57614a1528bb2f89a802b01d61f93e"},
{file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb51533ee3204e8160600b0de34eaad70eb913a182c94a7777b6051e8fc52f1"},
{file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f637636a44712e94e5601ec69160a080214626471983dfb0b5b68aa0c61563d"},
{file = "pygame-2.6.0-cp37-cp37m-win32.whl", hash = "sha256:e432156b6f346f4cc6cab03ce9657600093390f4c9b10bf458716b25beebfe33"},
{file = "pygame-2.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0194652db7874bdde7dfc69d659ca954544c012e04ae527151325bfb970f423"},
{file = "pygame-2.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eae3ee62cc172e268121d5bd9dc406a67094d33517de3a91de3323d6ae23eb02"},
{file = "pygame-2.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f6a58b0a5a8740a3c2cf6fc5366888bd4514561253437f093c12a9ab4fb3ecae"},
{file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71da36997dc7b9b4ee973fa3a5d4a6cfb2149161b5b1c08b712d2f13a63ccfe"},
{file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b86771801a7fc10d9a62218f27f1d5c13341c3a27394aa25578443a9cd199830"},
{file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4928f3acf5a9ce5fbab384c21f1245304535ffd5fb167ae92a6b4d3cdb55a3b6"},
{file = "pygame-2.6.0-cp38-cp38-win32.whl", hash = "sha256:4faab2df9926c4d31215986536b112f0d76f711cf02f395805f1ff5df8fd55fc"},
{file = "pygame-2.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:afbb8d97aed93dfb116fe105603dacb68f8dab05b978a40a9e4ab1b6c1f683fd"},
{file = "pygame-2.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d11f3646b53819892f4a731e80b8589a9140343d0d4b86b826802191b241228c"},
{file = "pygame-2.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ef92ed93c354eabff4b85e457d4d6980115004ec7ff52a19fd38b929c3b80fb"},
{file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc1795f2e36302882546faacd5a0191463c4f4ae2b90e7c334a7733aa4190d2"},
{file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e92294fcc85c4955fe5bc6a0404e4cc870808005dc8f359e881544e3cc214108"},
{file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0cb7bdf3ee0233a3ac02ef777c01dfe315e6d4670f1312c83b91c1ef124359a"},
{file = "pygame-2.6.0-cp39-cp39-win32.whl", hash = "sha256:ac906478ae489bb837bf6d2ae1eb9261d658aa2c34fa5b283027a04149bda81a"},
{file = "pygame-2.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:92cf12a9722f6f0bdc5520d8925a8f085cff9c054a2ea462fc409cba3781be27"},
{file = "pygame-2.6.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:a6636f452fdaddf604a060849feb84c056930b6a3c036214f607741f16aac942"},
{file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dc242dc15d067d10f25c5b12a1da48ca9436d8e2d72353eaf757e83612fba2f"},
{file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f82df23598a281c8c342d3c90be213c8fe762a26c15815511f60d0aac6e03a70"},
{file = "pygame-2.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ed2539bb6bd211fc570b1169dc4a64a74ec5cd95741e62a0ab46bd18fe08e0d"},
{file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:904aaf29710c6b03a7e1a65b198f5467ed6525e8e60bdcc5e90ff8584c1d54ea"},
{file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd28f96f0fffd28e71a98773843074597e10d7f55a098e2e5bcb2bef1bdcbf5"},
{file = "pygame-2.6.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4fad1ab33443ecd4f958dbbb67fc09fcdc7a37e26c34054e3296fb7e26ad641e"},
{file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e909186d4d512add39b662904f0f79b73028fbfc4fbfdaf6f9412aed4e500e9c"},
{file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79abcbf6d12fce51a955a0652ccd50b6d0a355baa27799535eaf21efb43433dd"},
{file = "pygame-2.6.0.tar.gz", hash = "sha256:722d33ae676aa8533c1f955eded966411298831346b8d51a77dad22e46ba3e35"},
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.17.2" version = "2.17.2"
@ -1970,4 +2036,4 @@ test = ["websockets"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "3b2e747f93972b78a9a35454810c99c4ec81e14fc9780e65a6a4434a97d1a713" content-hash = "4400e265162fa56d70d4ef5501896dec6f22b743414364ef99bdfef0be979785"

View File

@ -25,6 +25,7 @@ pyqtgraph = "^0.13.3"
colorlog = "^6.8.2" colorlog = "^6.8.2"
alchemical = "^1.0.2" alchemical = "^1.0.2"
obs-websocket-py = "^1.0" obs-websocket-py = "^1.0"
pygame = "^2.6.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"