Compare commits
6 Commits
92eb3fc953
...
553376a99e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553376a99e | ||
|
|
e3d7ae8e0f | ||
|
|
9656bac49f | ||
|
|
a971298982 | ||
|
|
4fe6e9186c | ||
|
|
8bc41f2fcd |
2
.envrc
2
.envrc
@ -4,6 +4,8 @@ export MAIL_PORT=587
|
||||
export MAIL_SERVER="smtp.fastmail.com"
|
||||
export MAIL_USERNAME="kae@midnighthax.com"
|
||||
export MAIL_USE_TLS=True
|
||||
export PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||
|
||||
branch=$(git branch --show-current)
|
||||
|
||||
# Always treat running from /home/kae/mm as production
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Standard library imports
|
||||
from dataclasses import dataclass, field
|
||||
from enum import auto, Enum
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, NamedTuple
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import pyqtSignal, QObject
|
||||
@ -65,3 +65,8 @@ class TrackFileData:
|
||||
obsolete_path: Optional[str] = None
|
||||
tags: dict[str, Any] = field(default_factory=dict)
|
||||
audio_metadata: dict[str, str | int | float] = field(default_factory=dict)
|
||||
|
||||
|
||||
class TrackInfo(NamedTuple):
|
||||
track_id: int
|
||||
row_number: int
|
||||
|
||||
@ -39,6 +39,7 @@ from PyQt6.QtWidgets import (
|
||||
|
||||
# Third party imports
|
||||
import pipeclient
|
||||
from pygame import mixer
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm.session import Session
|
||||
import stackprinter # type: ignore
|
||||
@ -47,16 +48,17 @@ import stackprinter # type: ignore
|
||||
from classes import (
|
||||
MusicMusterSignals,
|
||||
TrackFileData,
|
||||
TrackInfo,
|
||||
)
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog, ReplaceFilesDialog
|
||||
from helpers import file_is_unreadable
|
||||
from log import log
|
||||
from models import db, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
from playlists import PlaylistTab
|
||||
from trackmanager import (
|
||||
MainTrackManager,
|
||||
PreviewTrackManager,
|
||||
track_sequence,
|
||||
)
|
||||
from ui import icons_rc # noqa F401
|
||||
@ -143,6 +145,114 @@ class ImportTrack(QObject):
|
||||
self.import_finished.emit()
|
||||
|
||||
|
||||
class PreviewManager:
|
||||
"""
|
||||
Manage track preview player
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
mixer.init()
|
||||
self.intro: Optional[int] = None
|
||||
self.path: str = ""
|
||||
self.row_number: Optional[int] = None
|
||||
self.track_id: int = 0
|
||||
self.start_time: Optional[dt.datetime] = None
|
||||
|
||||
def back(self, ms: int) -> None:
|
||||
"""
|
||||
Move play position back by 'ms' milliseconds
|
||||
"""
|
||||
|
||||
position = max(0, (self.get_playtime() - ms)) / 1000
|
||||
mixer.music.set_pos(position)
|
||||
self.start_time = dt.datetime.now() - dt.timedelta(seconds=position)
|
||||
|
||||
def forward(self, ms: int) -> None:
|
||||
"""
|
||||
Move play position forward by 'ms' milliseconds
|
||||
"""
|
||||
|
||||
position = (self.get_playtime() + ms) / 1000
|
||||
mixer.music.set_pos(position)
|
||||
self.start_time = dt.datetime.now() - dt.timedelta(seconds=position)
|
||||
|
||||
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 move_to_intro_end(self) -> None:
|
||||
"""
|
||||
Move play position to 'buffer' milliseconds before end of intro.
|
||||
|
||||
If no intro defined, do nothing.
|
||||
"""
|
||||
|
||||
if self.intro is None:
|
||||
return
|
||||
|
||||
position = max(0, self.intro - Config.PREVIEW_END_BUFFER_MS) / 1000
|
||||
mixer.music.set_pos(position)
|
||||
self.start_time = dt.datetime.now() - dt.timedelta(seconds=position)
|
||||
|
||||
def play(self) -> None:
|
||||
mixer.music.play()
|
||||
self.start_time = dt.datetime.now()
|
||||
|
||||
def restart(self) -> None:
|
||||
"""
|
||||
Restart player from beginning
|
||||
"""
|
||||
|
||||
if not mixer.music.get_busy():
|
||||
return
|
||||
|
||||
mixer.music.set_pos(0)
|
||||
self.start_time = dt.datetime.now()
|
||||
|
||||
def set_intro(self, ms: int) -> None:
|
||||
"""
|
||||
Set intro time
|
||||
"""
|
||||
|
||||
self.intro = ms
|
||||
|
||||
def set_track_info(self, info: TrackInfo) -> None:
|
||||
self.track_id = info.track_id
|
||||
self.row_number = info.row_number
|
||||
|
||||
with db.Session() as session:
|
||||
track = session.get(Tracks, self.track_id)
|
||||
if not track:
|
||||
raise ValueError(f"PreviewManager: unable to retreive track {self.track_id=}")
|
||||
self.intro = track.intro
|
||||
self.path = track.path
|
||||
|
||||
# Check file readable
|
||||
if file_is_unreadable(self.path):
|
||||
raise ValueError(f"PreviewManager.__init__: {track.path=} unreadable")
|
||||
|
||||
mixer.music.load(self.path)
|
||||
|
||||
def stop(self) -> None:
|
||||
mixer.music.stop()
|
||||
self.path = ""
|
||||
self.row_number = None
|
||||
self.track_id = 0
|
||||
self.start_time = None
|
||||
|
||||
|
||||
class Window(QMainWindow, Ui_MainWindow):
|
||||
def __init__(self, parent=None, *args, **kwargs) -> None:
|
||||
super().__init__(parent)
|
||||
@ -153,8 +263,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.timer500: QTimer = QTimer()
|
||||
self.timer1000: QTimer = QTimer()
|
||||
|
||||
self.preview_track_manager: Optional[PreviewTrackManager] = None
|
||||
|
||||
self.set_main_window_size()
|
||||
self.lblSumPlaytime = QLabel("")
|
||||
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
|
||||
@ -162,6 +270,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.txtSearch.setHidden(True)
|
||||
self.statusbar.addWidget(self.txtSearch)
|
||||
self.hide_played_tracks = False
|
||||
self.preview_manager = PreviewManager()
|
||||
|
||||
self.widgetFadeVolume.hideAxis("bottom")
|
||||
self.widgetFadeVolume.hideAxis("left")
|
||||
@ -291,7 +400,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
current_track_playlist_id = track_sequence.current.playlist_id
|
||||
if 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
|
||||
|
||||
# Don't close next track playlist
|
||||
@ -299,7 +410,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
next_track_playlist_id = track_sequence.next.playlist_id
|
||||
if 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
|
||||
|
||||
# Record playlist as closed and update remaining playlist tabs
|
||||
@ -883,12 +996,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# Need to ensure that the new playlist is committed to
|
||||
# the database before it is opened by the model.
|
||||
|
||||
session.commit()
|
||||
if playlist:
|
||||
log.error("Playlist failed to create")
|
||||
playlist.mark_open()
|
||||
self.create_playlist_tab(playlist)
|
||||
else:
|
||||
log.error("Playlist failed to create")
|
||||
|
||||
def open_playlist(self) -> None:
|
||||
"""Open existing playlist"""
|
||||
@ -1046,42 +1159,27 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
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():
|
||||
# Get track_id for first selected track if there is one
|
||||
track_id = None
|
||||
row_number_and_track_id = self.active_tab().get_selected_row_and_track_id()
|
||||
if row_number_and_track_id:
|
||||
row_number, track_id = row_number_and_track_id
|
||||
else:
|
||||
# Get track path for first selected track if there is one
|
||||
track_info = self.active_tab().get_selected_row_track_info()
|
||||
if not track_info:
|
||||
# Otherwise get track_id to next track to play
|
||||
if track_sequence.next:
|
||||
track_id = track_sequence.next.track_id
|
||||
row_number = track_sequence.next.row_number
|
||||
if not track_id or row_number is None:
|
||||
self.btnPreview.setChecked(False)
|
||||
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
|
||||
|
||||
if track_sequence.next.path:
|
||||
track_info = TrackInfo(track_sequence.next.track_id,
|
||||
track_sequence.next.row_number
|
||||
)
|
||||
else:
|
||||
return
|
||||
self.preview_manager.set_track_info(track_info)
|
||||
self.preview_manager.play()
|
||||
else:
|
||||
if self.preview_track_manager:
|
||||
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)
|
||||
self.preview_manager.stop()
|
||||
|
||||
def preview_arm(self):
|
||||
"""Manager arm button for setting intro length"""
|
||||
@ -1091,45 +1189,42 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def preview_back(self) -> None:
|
||||
"""Wind back preview file"""
|
||||
|
||||
if self.preview_track_manager:
|
||||
self.preview_track_manager.move_back()
|
||||
self.preview_manager.back(5000)
|
||||
|
||||
def preview_end(self) -> None:
|
||||
"""Advance preview file to Config.PREVIEW_END_BUFFER_MS before end of intro"""
|
||||
|
||||
if self.preview_track_manager:
|
||||
self.preview_track_manager.move_to_intro_end()
|
||||
if self.preview_manager:
|
||||
self.preview_manager.move_to_intro_end()
|
||||
|
||||
def preview_fwd(self) -> None:
|
||||
"""Advance preview file"""
|
||||
|
||||
if self.preview_track_manager:
|
||||
self.preview_track_manager.move_forward()
|
||||
self.preview_manager.forward(5000)
|
||||
|
||||
def preview_mark(self) -> None:
|
||||
"""Set intro time"""
|
||||
|
||||
if self.preview_track_manager:
|
||||
track_id = self.preview_track_manager.track_id
|
||||
row_number = self.preview_track_manager.row_number
|
||||
if self.preview_manager.is_playing():
|
||||
track_id = self.preview_manager.track_id
|
||||
row_number = self.preview_manager.row_number
|
||||
with db.Session() as session:
|
||||
track = session.get(Tracks, track_id)
|
||||
if track:
|
||||
# Save intro as millisends rounded to nearest 0.1
|
||||
# second because editor spinbox only resolves to 0.1
|
||||
# seconds
|
||||
intro = round(self.preview_track_manager.time_playing() / 100) * 100
|
||||
intro = round(self.preview_manager.get_playtime() / 100) * 100
|
||||
track.intro = intro
|
||||
session.commit()
|
||||
self.preview_track_manager.intro = intro
|
||||
self.preview_manager.set_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:
|
||||
"""Restart preview"""
|
||||
|
||||
if self.preview_track_manager:
|
||||
self.preview_track_manager.restart()
|
||||
self.preview_manager.restart()
|
||||
|
||||
def rename_playlist(self) -> None:
|
||||
"""
|
||||
@ -1480,23 +1575,23 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
pass
|
||||
|
||||
# Ensure preview button is reset if preview finishes playing
|
||||
if self.preview_track_manager:
|
||||
self.btnPreview.setChecked(self.preview_track_manager.is_playing())
|
||||
|
||||
# Update preview timer
|
||||
if self.preview_track_manager.is_playing():
|
||||
playtime = self.preview_track_manager.time_playing()
|
||||
# Update preview timer
|
||||
if self.btnPreview.isChecked():
|
||||
if self.preview_manager.is_playing():
|
||||
self.btnPreview.setChecked(True)
|
||||
playtime = self.preview_manager.get_playtime()
|
||||
self.label_intro_timer.setText(f"{playtime / 1000:.1f}")
|
||||
if self.preview_track_manager.time_remaining_intro() <= 50:
|
||||
self.label_intro_timer.setStyleSheet(
|
||||
f"background: {Config.COLOUR_WARNING_TIMER}"
|
||||
)
|
||||
else:
|
||||
self.label_intro_timer.setStyleSheet("")
|
||||
else:
|
||||
self.label_intro_timer.setText("0.0")
|
||||
self.label_intro_timer.setStyleSheet("")
|
||||
self.btnPreview.setChecked(False)
|
||||
# if self.preview_track_manager.time_remaining_intro() <= 50:
|
||||
# self.label_intro_timer.setStyleSheet(
|
||||
# f"background: {Config.COLOUR_WARNING_TIMER}"
|
||||
# )
|
||||
# else:
|
||||
# self.label_intro_timer.setStyleSheet("")
|
||||
else:
|
||||
self.btnPreview.setChecked(False)
|
||||
self.label_intro_timer.setText("0.0")
|
||||
self.label_intro_timer.setStyleSheet("")
|
||||
self.btnPreview.setChecked(False)
|
||||
|
||||
def tick_1000ms(self) -> None:
|
||||
"""
|
||||
|
||||
@ -35,7 +35,7 @@ from PyQt6.QtWidgets import (
|
||||
# Third party imports
|
||||
|
||||
# App imports
|
||||
from classes import Col, MusicMusterSignals
|
||||
from classes import Col, MusicMusterSignals, TrackInfo
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog
|
||||
from helpers import (
|
||||
@ -644,42 +644,26 @@ class PlaylistTab(QTableView):
|
||||
self.source_model.delete_rows(self.selected_model_row_numbers())
|
||||
self.clear_selection()
|
||||
|
||||
def get_selected_row_and_track_id(self) -> Optional[tuple[int, int]]:
|
||||
def get_selected_row_track_info(self) -> Optional[TrackInfo]:
|
||||
"""
|
||||
Return the (row_number, track_id) of the selected row. If no row selected or selected
|
||||
row does not have a track, return None.
|
||||
Return the track_path, track_id and row number 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:
|
||||
"""
|
||||
Return the path of the selected row. If no row selected or selected
|
||||
row does not have a track, return empty string.
|
||||
"""
|
||||
|
||||
log.debug("get_selected_row_track_path() called")
|
||||
selected_row = self.get_selected_row()
|
||||
if selected_row is None:
|
||||
return None
|
||||
|
||||
model_row_number = self.source_model_selected_row_number()
|
||||
if model_row_number is None:
|
||||
result = ""
|
||||
return None
|
||||
else:
|
||||
result = self.source_model.get_row_track_path(model_row_number)
|
||||
|
||||
log.debug(f"get_selected_row_track_path() returned: {result=}")
|
||||
return result
|
||||
track_id = self.source_model.get_row_track_id(model_row_number)
|
||||
if not track_id:
|
||||
return None
|
||||
else:
|
||||
return TrackInfo(track_id, selected_row)
|
||||
|
||||
def get_selected_row(self) -> Optional[int]:
|
||||
"""
|
||||
|
||||
@ -328,7 +328,9 @@ class _Music:
|
||||
if self.player:
|
||||
self.player.set_position(position)
|
||||
|
||||
def set_volume(self, volume: Optional[int] = None, set_default: bool = True) -> None:
|
||||
def set_volume(
|
||||
self, volume: Optional[int] = None, set_default: bool = True
|
||||
) -> None:
|
||||
"""Set maximum volume used for player"""
|
||||
|
||||
if not self.player:
|
||||
@ -366,7 +368,6 @@ class _TrackManager:
|
||||
player_name: str,
|
||||
track_id: int,
|
||||
row_number: int,
|
||||
preview_player: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Initialises data structure.
|
||||
@ -379,7 +380,6 @@ class _TrackManager:
|
||||
raise ValueError(f"_TrackPlayer: unable to retreived {track_id=}")
|
||||
self.player_name = player_name
|
||||
self.row_number = row_number
|
||||
self.preview_player = preview_player
|
||||
|
||||
# Check file readable
|
||||
if file_is_unreadable(track.path):
|
||||
@ -409,29 +409,25 @@ class _TrackManager:
|
||||
self.player = _Music(name=player_name)
|
||||
|
||||
# Initialise and add FadeCurve in a thread as it's slow
|
||||
if not self.preview_player:
|
||||
self.fadecurve_thread = QThread()
|
||||
self.worker = _AddFadeCurve(
|
||||
self,
|
||||
track_path=track.path,
|
||||
track_fade_at=track.fade_at,
|
||||
track_silence_at=track.silence_at,
|
||||
)
|
||||
self.worker.moveToThread(self.fadecurve_thread)
|
||||
self.fadecurve_thread.started.connect(self.worker.run)
|
||||
self.worker.finished.connect(self.fadecurve_thread.quit)
|
||||
self.worker.finished.connect(self.worker.deleteLater)
|
||||
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
|
||||
self.fadecurve_thread.start()
|
||||
self.fadecurve_thread = QThread()
|
||||
self.worker = _AddFadeCurve(
|
||||
self,
|
||||
track_path=track.path,
|
||||
track_fade_at=track.fade_at,
|
||||
track_silence_at=track.silence_at,
|
||||
)
|
||||
self.worker.moveToThread(self.fadecurve_thread)
|
||||
self.fadecurve_thread.started.connect(self.worker.run)
|
||||
self.worker.finished.connect(self.fadecurve_thread.quit)
|
||||
self.worker.finished.connect(self.worker.deleteLater)
|
||||
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
|
||||
self.fadecurve_thread.start()
|
||||
|
||||
def check_for_end_of_track(self) -> None:
|
||||
"""
|
||||
Check whether track has ended. If so, emit track_ended_signal
|
||||
"""
|
||||
|
||||
if self.preview_player:
|
||||
return
|
||||
|
||||
if self.start_time is None:
|
||||
return
|
||||
|
||||
@ -530,9 +526,6 @@ class _TrackManager:
|
||||
Send end of track signal unless we are a preview player
|
||||
"""
|
||||
|
||||
if not self.preview_player:
|
||||
self.signals.track_ended_signal.emit()
|
||||
|
||||
def stop(self, fade_seconds: int = 0) -> None:
|
||||
"""
|
||||
Stop this track playing
|
||||
@ -641,24 +634,6 @@ class MainTrackManager(_TrackManager):
|
||||
)
|
||||
|
||||
|
||||
class PreviewTrackManager(_TrackManager):
|
||||
"""
|
||||
Manage previewing tracks
|
||||
"""
|
||||
|
||||
def __init__(self, session: db.Session, track_id: int, row_number: int) -> None:
|
||||
super().__init__(
|
||||
session=session,
|
||||
player_name=Config.VLC_PREVIEW_PLAYER_NAME,
|
||||
track_id=track_id,
|
||||
row_number=row_number,
|
||||
preview_player=True,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PreviewTrackManager(track_id={self.track_id}>"
|
||||
|
||||
|
||||
class TrackSequence:
|
||||
next: Optional[MainTrackManager] = None
|
||||
current: Optional[MainTrackManager] = None
|
||||
|
||||
68
poetry.lock
generated
68
poetry.lock
generated
@ -1253,6 +1253,72 @@ files = [
|
||||
{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]]
|
||||
name = "pygments"
|
||||
version = "2.17.2"
|
||||
@ -1970,4 +2036,4 @@ test = ["websockets"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "3b2e747f93972b78a9a35454810c99c4ec81e14fc9780e65a6a4434a97d1a713"
|
||||
content-hash = "4400e265162fa56d70d4ef5501896dec6f22b743414364ef99bdfef0be979785"
|
||||
|
||||
@ -25,6 +25,7 @@ pyqtgraph = "^0.13.3"
|
||||
colorlog = "^6.8.2"
|
||||
alchemical = "^1.0.2"
|
||||
obs-websocket-py = "^1.0"
|
||||
pygame = "^2.6.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
ipdb = "^0.13.9"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user