From 6eb42e6d4468ad161281125c77a41063f93380e1 Mon Sep 17 00:00:00 2001 From: Eddy Pedroni Date: Tue, 21 Dec 2021 18:24:55 +0100 Subject: Added session manager, renamed solo-tool.py --- abcontroller.py | 3 + abcontroller_unittest.py | 4 + playlist.py | 24 +++--- playlist_unittest.py | 21 ++++-- session_manager.py | 39 ++++++++++ session_manager_unittest.py | 137 ++++++++++++++++++++++++++++++++++ solo-tool.py | 175 -------------------------------------------- solo_tool_qt.py | 175 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 384 insertions(+), 194 deletions(-) create mode 100644 session_manager.py create mode 100644 session_manager_unittest.py delete mode 100644 solo-tool.py create mode 100644 solo_tool_qt.py diff --git a/abcontroller.py b/abcontroller.py index 302bc9e..9d232c1 100644 --- a/abcontroller.py +++ b/abcontroller.py @@ -48,3 +48,6 @@ class ABController: def setEnable(self, enable): self._enabled = enable + + def getLimits(self, song): + return self._limits.get(song) diff --git a/abcontroller_unittest.py b/abcontroller_unittest.py index 839c88a..42eaabc 100644 --- a/abcontroller_unittest.py +++ b/abcontroller_unittest.py @@ -37,6 +37,7 @@ def test_oneSetOfLimits(): uut.setCurrentLimits(0) checkLimits(uut, abLimits.a, abLimits.b) + assert uut.getLimits(song) == [abLimits] def test_multipleSetsOfLimits(): song = "/path/to/song" @@ -55,6 +56,8 @@ def test_multipleSetsOfLimits(): uut.setCurrentLimits(i) checkLimits(uut, l.a, l.b) + assert uut.getLimits(song) == abLimits + def test_multipleSongs(): songs = [ "/path/to/song", @@ -73,6 +76,7 @@ def test_multipleSongs(): uut.setCurrentLimits(0) checkLimits(uut, abLimits[i].a, abLimits[i].b) + assert uut.getLimits(s) == [abLimits[i]] def test_disableAbRepeat(): song = "/path/to/song" diff --git a/playlist.py b/playlist.py index 6e96534..5c52774 100644 --- a/playlist.py +++ b/playlist.py @@ -2,21 +2,23 @@ import logging class Playlist: def __init__(self, callback): - self.songList = list() - self.currentSong = None - self.setSongCallback = callback + self._songList = list() + self._currentSong = None + self._setSongCallback = callback def addSong(self, path): - self.songList.append(path) + self._songList.append(path) logging.debug(f"Added song: {path}") - if self.currentSong is None: - self.setCurrentSong(0) def setCurrentSong(self, index): - if index >= 0 and index < len(self.songList): - self.currentSong = index - self.setSongCallback(self.songList[index]) - logging.debug(f"Selected song: {self.currentSong}") + if index >= 0 and index < len(self._songList): + self._currentSong = index + self._setSongCallback(self._songList[index]) + logging.debug(f"Selected song: {self._currentSong}") def getCurrentSong(self): - return self.currentSong + index = self._currentSong + return self._songList[index] if index is not None else None + + def getSongs(self): + return self._songList diff --git a/playlist_unittest.py b/playlist_unittest.py index b407e4d..ff5a79c 100644 --- a/playlist_unittest.py +++ b/playlist_unittest.py @@ -13,7 +13,8 @@ def test_addAndSelectOneSong(): uut.setCurrentSong(0) assert songAddedByUser == songSetByCallback - assert uut.getCurrentSong() == 0 + assert uut.getCurrentSong() == songAddedByUser + assert uut.getSongs() == [songAddedByUser] def test_addTwoSongsAndSelectBoth(): songAddedByUser = ["/path/to/song", "/path/to/second/song"] @@ -26,16 +27,17 @@ def test_addTwoSongsAndSelectBoth(): uut = Playlist(testCallback) uut.addSong(songAddedByUser[0]) uut.addSong(songAddedByUser[1]) + assert uut.getSongs() == songAddedByUser uut.setCurrentSong(0) assert songAddedByUser[0] == songSetByCallback - assert uut.getCurrentSong() == 0 + assert uut.getCurrentSong() == songAddedByUser[0] uut.setCurrentSong(1) assert songAddedByUser[1] == songSetByCallback - assert uut.getCurrentSong() == 1 + assert uut.getCurrentSong() == songAddedByUser[1] -def test_firstAddedSongIsSelected(): +def test_firstAddedSongIsNotSelected(): songAddedByUser = "/path/to/song" songSetByCallback = None @@ -46,8 +48,9 @@ def test_firstAddedSongIsSelected(): uut = Playlist(testCallback) uut.addSong(songAddedByUser) - assert songAddedByUser == songSetByCallback - assert uut.getCurrentSong() == 0 + assert songSetByCallback == None + assert uut.getCurrentSong() == None + assert uut.getSongs() == [songAddedByUser] def test_invalidSongSelection(): songAddedByUser = "/path/to/song" @@ -59,6 +62,7 @@ def test_invalidSongSelection(): uut = Playlist(testCallback) assert songSetByCallback == None + assert uut.getCurrentSong() == None uut.setCurrentSong(10) assert songSetByCallback == None @@ -66,6 +70,7 @@ def test_invalidSongSelection(): uut.addSong(songAddedByUser) uut.setCurrentSong(10) - assert songSetByCallback == songAddedByUser - assert uut.getCurrentSong() == 0 + assert songSetByCallback == None + assert uut.getCurrentSong() == None + assert uut.getSongs() == [songAddedByUser] diff --git a/session_manager.py b/session_manager.py new file mode 100644 index 0000000..057d952 --- /dev/null +++ b/session_manager.py @@ -0,0 +1,39 @@ +import json + +class SessionManager: + def __init__(self, playlist, abController): + self._playlist = playlist + self._abController = abController + + def addSong(self, path): + self._playlist.addSong(path) + + def addLimits(self, aLimit, bLimit): + self._abController.addLimits(aLimit, bLimit) + + def loadSession(self, file): + jsonStr = file.read() + session = json.loads(jsonStr) + + for entry in session: + songPath = entry["path"] + abLimits = entry["ab_limits"] + self._playlist.addSong(songPath) + + if abLimits is not None: + for l in abLimits: + self._abController.addLimits(l[0], l[1], songPath) + + def saveSession(self, file): + songs = self._playlist.getSongs() + abLimits = self._abController.getLimits() + session = list() + + for s in songs: + entry = { + "path": s, + "ab_limits" : abLimits.get(s) + } + session.append(entry) + + file.write(json.dumps(session)) diff --git a/session_manager_unittest.py b/session_manager_unittest.py new file mode 100644 index 0000000..0169132 --- /dev/null +++ b/session_manager_unittest.py @@ -0,0 +1,137 @@ +from session_manager import SessionManager +from json import loads, dumps + +testSession = [ + { + "path" : "/path/to/another/song", + "ab_limits" : None + }, + { + "path" : "/path/to/song", + "ab_limits" : [ + [0.1, 0.2], + [0.3, 0.4] + ] + }, + { + "path" : "/path/to/something", + "ab_limits" : [ + [0.1, 0.2] + ] + } +] + +class PlaylistMock: + def __init__(self): + self.lastAddedSong = None + self.songs = list() + + def addSong(self, s): + self.songs.append(s) + self.lastAddedSong = s + + def getSongs(self): + return self.songs + +class ABControllerMock: + def __init__(self): + self.limits = dict() + + def addLimits(self, aLimit, bLimit, song="current"): + if song not in self.limits: + self.limits[song] = list() + self.limits[song].append([aLimit, bLimit]) + + def getLimits(self): + return self.limits + +class MockFile: + def __init__(self, init=""): + self.contents = init + + def open(self, *args): + pass + + def write(self, s): + self.contents += s + + def read(self): + return self.contents + + +def test_addSongs(): + songs = [ + "/path/to/song", + "/path/to/another/song" + ] + + playlistMock = PlaylistMock() + uut = SessionManager(playlistMock, None) + + for s in songs: + uut.addSong(s) + assert playlistMock.lastAddedSong == s + +def test_addAbLimits(): + abLimits = [ + [0.1, 0.2], + [0.3, 0.4] + ] + + abControllerMock = ABControllerMock() + uut = SessionManager(None, abControllerMock) + + for i, ab in enumerate(abLimits): + uut.addLimits(ab[0], ab[1]) + assert abControllerMock.limits["current"][i] == ab + +def test_loadSession(): + playlistMock = PlaylistMock() + abControllerMock = ABControllerMock() + uut = SessionManager(playlistMock, abControllerMock) + + sessionFile = MockFile(dumps(testSession)) + uut.loadSession(sessionFile) + + for i, entry in enumerate(testSession): + expectedSong = entry["path"] + expectedLimits = entry["ab_limits"] + loadedSong = playlistMock.songs[i] + loadedLimits = abControllerMock.limits.get(expectedSong) + + assert loadedSong == expectedSong + assert loadedLimits == expectedLimits + +def test_saveSession(): + playlistMock = PlaylistMock() + abControllerMock = ABControllerMock() + uut = SessionManager(playlistMock, abControllerMock) + + for i, entry in enumerate(testSession): + song = entry["path"] + playlistMock.addSong(song) + + abLimits = entry["ab_limits"] + if abLimits is not None: + for l in abLimits: + abControllerMock.addLimits(l[0], l[1], song) + + sessionFile = MockFile() + uut.saveSession(sessionFile) + + savedSession = loads(sessionFile.read()) + assert savedSession == testSession + +def test_loadAndSaveEmptySession(): + playlistMock = PlaylistMock() + abControllerMock = ABControllerMock() + uut = SessionManager(playlistMock, abControllerMock) + + sessionFile = MockFile() + + uut.saveSession(sessionFile) + assert loads(sessionFile.read()) == list() + + uut.loadSession(sessionFile) + assert playlistMock.getSongs() == list() + assert abControllerMock.getLimits() == dict() diff --git a/solo-tool.py b/solo-tool.py deleted file mode 100644 index 5db6d3f..0000000 --- a/solo-tool.py +++ /dev/null @@ -1,175 +0,0 @@ -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * -from MainWindow import Ui_MainWindow - -import vlc - -import logging -LOGLEVEL = logging.DEBUG - -import midi -import playlist -import control - -class MainWindow(QMainWindow, Ui_MainWindow): - def __init__(self, *args, **kwargs): - super(MainWindow, self).__init__(*args, **kwargs) - self.setupUi(self) - - self.midiEnabled = False - self.player = vlc.MediaListPlayer() - self.playlist = playlist.Playlist(self) - self.control = control.Control(self) - - """ - self.player.durationChanged.connect(self.updateDuration) - self.player.positionChanged.connect(self.updatePosition) - - self.player.positionChanged.connect(self.positionChanged) - - self.abListModel = AbListModel() - self.abListView.setModel(self.abListModel) - self.abListView.selectionModel().selectionChanged.connect(self.abListSelectionChanged) - - self.saveAbButton.pressed.connect(self.addAb) - self.internalState = dict() - - self.saveSessionButton.pressed.connect(self.saveSession) - self.loadSessionButton.pressed.connect(self.loadSession) - - self.abRepeatButton.clicked.connect(self.abRepeatToggleClick) - - self.initMidiButton.pressed.connect(self.initMidi) - """ - - self.show() - - def playlistPositionChanged(self, i): - if i > -1: - ix = self.playlistModel.index(i) - self.songListView.setCurrentIndex(ix) - - def abListSelectionChanged(self, ix): - if len(ix.indexes()) > 0: - i = ix.indexes()[0].row() - ab = self.abListModel.abList[i] - self.aSlider.setValue(ab[0]) - self.bSlider.setValue(ab[1]) - - def addAb(self, song=None, a=None, b=None): - currentSong = song or self.playlist.currentMedia().canonicalUrl().path() - abState = [a or self.aSlider.value(), b or self.bSlider.value()] - self.abListModel.abList.append(abState) - self.abListModel.layoutChanged.emit() - - - if self.midiEnabled: - midi.button_on(midi.lp_key[0][1], (midi.GREEN if self.player.state() == QMediaPlayer.PlayingState else midi.RED)) - - def updateDuration(self, duration): - self.aSlider.setMaximum(duration) - self.bSlider.setMaximum(duration) - - def updatePosition(self, position): - # Disable the events to prevent updating triggering a setPosition event (can cause stuttering). - self.songSlider.blockSignals(True) - self.songSlider.setValue(position) - self.songSlider.blockSignals(False) - - def positionChanged(self, position): - if self.abRepeatButton.isChecked() and position > self.bSlider.value(): - self.player.setPosition(self.aSlider.value()) - - def loadSession(self): - path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "session (*.json)") - if path: - import json - with open(path, "r") as f: - session = json.load(f) - self.reset() - - for song in session: - self.addSong(song) - self.abListModel.abList = self.internalState[song] - - for ab in session[song]: - self.addAb(song=song, a=ab[0], b=ab[1]) - - def saveSession(self): - path, _ = QFileDialog.getSaveFileName(self, "Save file", "", "session (*.json)") - if path: - import json - with open(path, "w") as f: - json.dump(self.internalState, f) - - def abRepeatToggleClick(self): - midi.button_on(midi.lp_key[1][1], (midi.GREEN if self.abRepeatButton.isChecked() else midi.RED)) - - def initMidi(self): - if self.midiEnabled: - return - - try: - midi.midi_init() - except Exception as e: - print(e) - return - - self.midiEnabled = True - - # play pause - midi.button_on(midi.lp_key[0][1], (midi.GREEN if self.player.state() == QMediaPlayer.PlayingState else midi.RED)) - midi.on_press(midi.lp_key[0][1], self.playPauseButton.click) - - # next song - midi.button_on(midi.lp_key[0][2], midi.YELLOW) - midi.on_press(midi.lp_key[0][2], self.playlist.next) - - # previous song - midi.button_on(midi.lp_key[0][0], midi.YELLOW) - midi.on_press(midi.lp_key[0][0], self.playlist.previous) - - # a/b repeat - midi.button_on(midi.lp_key[1][1], (midi.GREEN if self.abRepeatButton.isChecked() else midi.RED)) - midi.on_press(midi.lp_key[1][1], self.abRepeatButton.click) - - # next ab - # TODO continue here - midi.button_on(midi.lp_key[1][2], midi.YELLOW) - #midi.on_press(midi.lp_key[1][2], nextAb) - - - def reset(self): - self.playlist.clear() - self.playlistModel.layoutChanged.emit() - self.abListModel.abList.clear() - self.abListModel.layoutChanged.emit() - -if __name__ == '__main__': - logging.basicConfig(level=LOGLEVEL) - - app = QApplication([]) - app.setApplicationName("Solo Tool") - app.setStyle("Fusion") - - # Fusion dark palette from https://gist.github.com/QuantumCD/6245215. - palette = QPalette() - palette.setColor(QPalette.Window, QColor(53, 53, 53)) - palette.setColor(QPalette.WindowText, Qt.white) - palette.setColor(QPalette.Base, QColor(25, 25, 25)) - palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) - palette.setColor(QPalette.ToolTipBase, Qt.white) - palette.setColor(QPalette.ToolTipText, Qt.white) - palette.setColor(QPalette.Text, Qt.white) - palette.setColor(QPalette.Button, QColor(53, 53, 53)) - palette.setColor(QPalette.ButtonText, Qt.white) - palette.setColor(QPalette.BrightText, Qt.red) - palette.setColor(QPalette.Link, QColor(42, 130, 218)) - palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) - palette.setColor(QPalette.HighlightedText, Qt.black) - app.setPalette(palette) - app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }") - - window = MainWindow() - app.exec_() diff --git a/solo_tool_qt.py b/solo_tool_qt.py new file mode 100644 index 0000000..5db6d3f --- /dev/null +++ b/solo_tool_qt.py @@ -0,0 +1,175 @@ +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from MainWindow import Ui_MainWindow + +import vlc + +import logging +LOGLEVEL = logging.DEBUG + +import midi +import playlist +import control + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + self.setupUi(self) + + self.midiEnabled = False + self.player = vlc.MediaListPlayer() + self.playlist = playlist.Playlist(self) + self.control = control.Control(self) + + """ + self.player.durationChanged.connect(self.updateDuration) + self.player.positionChanged.connect(self.updatePosition) + + self.player.positionChanged.connect(self.positionChanged) + + self.abListModel = AbListModel() + self.abListView.setModel(self.abListModel) + self.abListView.selectionModel().selectionChanged.connect(self.abListSelectionChanged) + + self.saveAbButton.pressed.connect(self.addAb) + self.internalState = dict() + + self.saveSessionButton.pressed.connect(self.saveSession) + self.loadSessionButton.pressed.connect(self.loadSession) + + self.abRepeatButton.clicked.connect(self.abRepeatToggleClick) + + self.initMidiButton.pressed.connect(self.initMidi) + """ + + self.show() + + def playlistPositionChanged(self, i): + if i > -1: + ix = self.playlistModel.index(i) + self.songListView.setCurrentIndex(ix) + + def abListSelectionChanged(self, ix): + if len(ix.indexes()) > 0: + i = ix.indexes()[0].row() + ab = self.abListModel.abList[i] + self.aSlider.setValue(ab[0]) + self.bSlider.setValue(ab[1]) + + def addAb(self, song=None, a=None, b=None): + currentSong = song or self.playlist.currentMedia().canonicalUrl().path() + abState = [a or self.aSlider.value(), b or self.bSlider.value()] + self.abListModel.abList.append(abState) + self.abListModel.layoutChanged.emit() + + + if self.midiEnabled: + midi.button_on(midi.lp_key[0][1], (midi.GREEN if self.player.state() == QMediaPlayer.PlayingState else midi.RED)) + + def updateDuration(self, duration): + self.aSlider.setMaximum(duration) + self.bSlider.setMaximum(duration) + + def updatePosition(self, position): + # Disable the events to prevent updating triggering a setPosition event (can cause stuttering). + self.songSlider.blockSignals(True) + self.songSlider.setValue(position) + self.songSlider.blockSignals(False) + + def positionChanged(self, position): + if self.abRepeatButton.isChecked() and position > self.bSlider.value(): + self.player.setPosition(self.aSlider.value()) + + def loadSession(self): + path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "session (*.json)") + if path: + import json + with open(path, "r") as f: + session = json.load(f) + self.reset() + + for song in session: + self.addSong(song) + self.abListModel.abList = self.internalState[song] + + for ab in session[song]: + self.addAb(song=song, a=ab[0], b=ab[1]) + + def saveSession(self): + path, _ = QFileDialog.getSaveFileName(self, "Save file", "", "session (*.json)") + if path: + import json + with open(path, "w") as f: + json.dump(self.internalState, f) + + def abRepeatToggleClick(self): + midi.button_on(midi.lp_key[1][1], (midi.GREEN if self.abRepeatButton.isChecked() else midi.RED)) + + def initMidi(self): + if self.midiEnabled: + return + + try: + midi.midi_init() + except Exception as e: + print(e) + return + + self.midiEnabled = True + + # play pause + midi.button_on(midi.lp_key[0][1], (midi.GREEN if self.player.state() == QMediaPlayer.PlayingState else midi.RED)) + midi.on_press(midi.lp_key[0][1], self.playPauseButton.click) + + # next song + midi.button_on(midi.lp_key[0][2], midi.YELLOW) + midi.on_press(midi.lp_key[0][2], self.playlist.next) + + # previous song + midi.button_on(midi.lp_key[0][0], midi.YELLOW) + midi.on_press(midi.lp_key[0][0], self.playlist.previous) + + # a/b repeat + midi.button_on(midi.lp_key[1][1], (midi.GREEN if self.abRepeatButton.isChecked() else midi.RED)) + midi.on_press(midi.lp_key[1][1], self.abRepeatButton.click) + + # next ab + # TODO continue here + midi.button_on(midi.lp_key[1][2], midi.YELLOW) + #midi.on_press(midi.lp_key[1][2], nextAb) + + + def reset(self): + self.playlist.clear() + self.playlistModel.layoutChanged.emit() + self.abListModel.abList.clear() + self.abListModel.layoutChanged.emit() + +if __name__ == '__main__': + logging.basicConfig(level=LOGLEVEL) + + app = QApplication([]) + app.setApplicationName("Solo Tool") + app.setStyle("Fusion") + + # Fusion dark palette from https://gist.github.com/QuantumCD/6245215. + palette = QPalette() + palette.setColor(QPalette.Window, QColor(53, 53, 53)) + palette.setColor(QPalette.WindowText, Qt.white) + palette.setColor(QPalette.Base, QColor(25, 25, 25)) + palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) + palette.setColor(QPalette.ToolTipBase, Qt.white) + palette.setColor(QPalette.ToolTipText, Qt.white) + palette.setColor(QPalette.Text, Qt.white) + palette.setColor(QPalette.Button, QColor(53, 53, 53)) + palette.setColor(QPalette.ButtonText, Qt.white) + palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.Link, QColor(42, 130, 218)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, Qt.black) + app.setPalette(palette) + app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }") + + window = MainWindow() + app.exec_() -- cgit v1.2.3