From bcfd076a934ecc9a85b9211dd7151d5365519afe Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 3 Apr 2021 22:45:30 +0100 Subject: [PATCH] Refactor into modules --- app/helpers.py | 19 ++ app/model.py | 10 +- app/music.py | 94 +++++++++ app/musicmuster.py | 431 +++++++++------------------------------- app/playlists.py | 365 ++++++++++++++++++++++++++++++++++ app/promoted_classes.py | 114 ----------- app/ui/main_window.ui | 25 ++- 7 files changed, 591 insertions(+), 467 deletions(-) create mode 100644 app/helpers.py create mode 100644 app/music.py create mode 100644 app/playlists.py delete mode 100644 app/promoted_classes.py diff --git a/app/helpers.py b/app/helpers.py new file mode 100644 index 0000000..75469cf --- /dev/null +++ b/app/helpers.py @@ -0,0 +1,19 @@ +from log import DEBUG + + +def ms_to_mmss(ms, decimals=0, negative=False): + if not ms: + return "-" + sign = "" + if ms < 0: + if negative: + sign = "-" + else: + ms = 0 + + minutes, remainder = divmod(ms, 60 * 1000) + seconds = remainder / 1000 + if int(seconds) == 60: + DEBUG(f"ms_to_mmss({ms}) gave 60 seconds") + + return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" diff --git a/app/model.py b/app/model.py index 89607d4..d2c67f4 100644 --- a/app/model.py +++ b/app/model.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -import os import sqlalchemy from datetime import datetime @@ -102,7 +101,8 @@ class Playlists(Base): tr = session.query(Tracks).filter(Tracks.id == 676).one() tr - + glue.track_id = tr.id @@ -132,8 +132,10 @@ class Playlists(Base): # Currently we only support one playlist, so make that obvious from # function name @classmethod - def get_only_playlist(cls): - return session.query(Playlists).filter(Playlists.id == 1).one() + def get_playlist_by_name(cls, name): + "Returns a playlist object for named playlist" + + return session.query(Playlists).filter(Playlists.name == name).one() def add_track(self, track, position): glue = PlaylistTracks(sort=position) diff --git a/app/music.py b/app/music.py new file mode 100644 index 0000000..a672fd7 --- /dev/null +++ b/app/music.py @@ -0,0 +1,94 @@ +import os +import threading +import vlc + +from config import Config +from time import sleep + +from log import DEBUG + + +class Music: + """ + Manage the playing of music tracks + """ + + def __init__(self): + self.fading = False + self.player = None + self.track_path = None + + def fade(self): + """ + Fade the currently playing track. + + Return the current track path and position. + + The actual management of fading runs in its own thread so as not + to hold up the UI during the fade. + """ + + DEBUG("fade()") + + if not self.playing(): + return (None, None) + + path = self.track_path + position = self.player.get_position() + + thread = threading.Thread(target=self._fade) + thread.start() + + return (path, position) + + def _fade(self): + """ + Implementation of fading the current track in a separate thread. + """ + + DEBUG("_fade()") + + fade_time = Config.FADE_TIME / 1000 + sleep_time = fade_time / Config.FADE_STEPS + step_percent = int((100 / Config.FADE_STEPS) * -1) + + # Take a copy of current player to allow another track to be + # started without interfering here + p = self.player + for i in range(100, 0, step_percent): + p.audio_set_volume(i) + sleep(sleep_time) + + p.pause() + p.release() + + def get_playtime(self): + "Return elapsed play time" + + return self.player.get_time() + + def play(self, path): + """ + Start playing the track at path. + + Raise AttributeError if path not found. + """ + + DEBUG(f"play({path})") + + if not os.access(path, os.R_OK): + raise AttributeError(f"Cannot access {path}") + + self.track_path = path + + self.player = vlc.MediaPlayer(path) + self.player.audio_set_volume(100) + self.player.play() + + def playing(self): + "Return True if currently playing a track, else False" + + if self.player: + return self.player.is_playing() + else: + return False diff --git a/app/musicmuster.py b/app/musicmuster.py index 843ff19..257c64a 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,180 +1,25 @@ #!/usr/bin/python3 -import vlc import sys -import threading from datetime import datetime, timedelta -from log import DEBUG, ERROR +# from log import DEBUG, ERROR from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtGui import QColor from PyQt5.QtWidgets import ( QApplication, QDialog, QFileDialog, QListWidgetItem, QMainWindow, - QTableWidgetItem, ) -from ui.main_window_ui import Ui_MainWindow -from ui.dlg_search_database_ui import Ui_Dialog +import helpers from config import Config -from model import Playdates, Playlists, Settings, Tracks -from time import sleep - - -class Music: - def __init__(self): - self.current_track = { - "player": None, - "meta": None - } - self.next_track = { - "player": None, - "meta": None - } - self.previous_track = { - "player": None, - "meta": None - } - self.fading = False - - def get_current_artist(self): - try: - return self.current_track['meta'].artist - except AttributeError: - return "" - - def get_current_duration(self): - try: - return self.current_track['meta'].duration - except AttributeError: - return 0 - - def get_current_fade_at(self): - try: - return self.current_track['meta'].fade_at - except AttributeError: - return 0 - - def get_current_playtime(self): - try: - return self.current_track['player'].get_time() - except AttributeError: - return 0 - - def get_current_silence_at(self): - try: - return self.current_track['meta'].silence_at - except AttributeError: - return 0 - - def get_current_title(self): - try: - return self.current_track['meta'].title - except AttributeError: - return "" - - def get_current_track_id(self): - try: - return self.current_track['meta'].id - except AttributeError: - return 0 - - def get_previous_artist(self): - try: - return self.previous_track['meta'].artist - except AttributeError: - return "" - - def get_previous_title(self): - try: - return self.previous_track['meta'].title - except AttributeError: - return "" - - def get_next_artist(self): - try: - return self.next_track['meta'].artist - except AttributeError: - return "" - - def get_next_title(self): - try: - return self.next_track['meta'].title - except AttributeError: - return "" - - def get_next_track_id(self): - try: - return self.next_track['meta'].id - except AttributeError: - return 0 - - def fade_current(self): - if not self.playing(): - return - thread = threading.Thread(target=self._fade_current) - thread.start() - - def _fade_current(self): - fade_time = Config.FADE_TIME / 1000 - sleep_time = fade_time / Config.FADE_STEPS - step_percent = int((100 / Config.FADE_STEPS) * -1) - player = self.current_track['player'] - position = player.get_position() - for i in range(100, 0, step_percent): - player.audio_set_volume(i) - sleep(sleep_time) - # If the user clicks "fade" and then, before the track has - # finished fading, click "play next", the "play next" will also - # call fade_current. When that second call to fade_current gets - # to the player.pause() line below, it will unpause it. So, we - # only pause if we're acutally playing. - if player.is_playing(): - player.pause() - player.audio_set_volume(100) - player.set_position(position) - - def play_next(self): - if self.previous_track['player']: - self.previous_track['player'].release() - if self.current_track['player']: - self.fade_current() - self.previous_track = self.current_track - self.current_track = self.next_track - # Next in case player was left in odd (ie, silenced) state - self.current_track['player'].audio_set_volume(100) - self.current_track['player'].play() - Playdates.add_playdate(self.current_track['meta']) - - # Tidy up - self.next_track = { - "player": None, - "meta": None - } - - def playing(self): - if self.current_track['player']: - return self.current_track['player'].is_playing() - else: - return False - - def resume_previous(self): - pass - - def set_next_track(self, id): - track = Tracks.get_track(id) - if not track: - ERROR(f"set_next_track({id}): can't find track") - return None - - self.next_track['player'] = vlc.MediaPlayer(track.path) - self.next_track['meta'] = track - return track.id +from model import Playlists, Settings, Tracks +from ui.dlg_search_database_ui import Ui_Dialog +from ui.main_window_ui import Ui_MainWindow class Window(QMainWindow, Ui_MainWindow): @@ -182,8 +27,7 @@ class Window(QMainWindow, Ui_MainWindow): super().__init__(parent) self.setupUi(self) self.timer = QTimer() - self.connectSignalsSlots() - self.music = Music() + self.connect_signals_slots() self.disable_play_next_controls() record = Settings.get_int("mainwindow_x") @@ -196,39 +40,12 @@ class Window(QMainWindow, Ui_MainWindow): height = record.f_int or 981 self.setGeometry(x, y, width, height) - for column in range(self.playlist.columnCount()): - name = f"playlist_col_{str(column)}_width" - record = Settings.get_int(name) - if record.f_int is not None: - self.playlist.setColumnWidth(column, record.f_int) - - # Load playlist - db_playlist = Playlists.get_only_playlist() - for track in db_playlist.get_tracks(): - self.add_to_playlist(track) - # Set the first playable track as next to play - for row in range(self.playlist.rowCount()): - if self.set_next_track_row(row): - break - - pl = self.playlist - row = pl.rowCount() - pl.insertRow(row) - item = QTableWidgetItem("to be spanned") - pl.setItem(row, 1, item) - pl.setSpan(row, 1, 1, 3) - colour = QColor(125, 75, 25) - pl.set_row_colour(row, colour) + # Hard code to the only playlist we have for now + self.playlist.load_playlist("Default") self.timer.start(Config.TIMER_MS) def __del__(self): - for column in range(self.playlist.columnCount()): - name = f"playlist_col_{str(column)}_width" - record = Settings.get_int(name) - if record.f_int != self.playlist.columnWidth(column): - record.update({'f_int': self.playlist.columnWidth(column)}) - record = Settings.get_int("mainwindow_height") if record.f_int != self.height(): record.update({'f_int': self.height()}) @@ -245,28 +62,47 @@ class Window(QMainWindow, Ui_MainWindow): if record.f_int != self.y(): record.update({'f_int': self.y()}) - def connectSignalsSlots(self): + def add_file(self): + dlg = QFileDialog() + dlg.setFileMode(QFileDialog.ExistingFile) + dlg.setViewMode(QFileDialog.Detail) + dlg.setDirectory(Config.ROOT) + dlg.setNameFilter("Music files (*.flac *.mp3)") + + if dlg.exec_(): + pass + # TODO Add to database + # for fname in dlg.selectedFiles(): + # trackwest = Track(fname) + # self.add_to_playlist(track) + + def clear_selection(self): + self.playlist.clearSelection() + + def connect_signals_slots(self): self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionFade.triggered.connect(self.fade) self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_selected.triggered.connect(self.play_next) - self.actionSearch_database.triggered.connect(self.selectFromDatabase) - self.btnSearchDatabase.clicked.connect(self.selectFromDatabase) - self.btnSetNextTrack.clicked.connect(self.set_next_track) + self.actionSearch_database.triggered.connect(self.search_database) + self.btnPrevious.clicked.connect(self.play_previous) + self.btnSearchDatabase.clicked.connect(self.search_database) + self.btnSetNextTrack.clicked.connect(self.playlist.set_next_track) self.btnSkipNext.clicked.connect(self.play_next) self.btnStop.clicked.connect(self.fade) self.timer.timeout.connect(self.tick) - def selectFromDatabase(self): - dlg = DbDialog(self) - dlg.exec() + def disable_play_next_controls(self): + self.actionPlay_selected.setEnabled(False) + self.actionPlay_next.setEnabled(False) - def clear_selection(self): - self.playlist.clearSelection() + def enable_play_next_controls(self): + self.actionPlay_selected.setEnabled(True) + self.actionPlay_next.setEnabled(True) def fade(self): - self.music.fade_current() + self.playlist.fade() def play_next(self): self.music.play_next() @@ -286,141 +122,72 @@ class Window(QMainWindow, Ui_MainWindow): silence_at = self.music.get_current_silence_at() silence_time = now + timedelta(milliseconds=silence_at) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) - self.label_fade_length.setText(ms_to_mmss( + self.label_fade_length.setText(helpers.ms_to_mmss( silence_at - self.music.get_current_fade_at())) self.set_playlist_colours() - def set_playlist_colours(self): - self.playlist.clearSelection() - current_track = self.music.get_current_track_id() - next_track = self.music.get_next_track_id() - for row in range(self.playlist.rowCount()): - try: - track_id = int(self.playlist.item(row, 0).text()) - if track_id == next_track: - self.playlist.set_row_colour( - row, QColor(Config.COLOUR_NEXT_PLAYLIST)) - elif track_id == current_track: - self.playlist.set_row_colour( - row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) - else: - if row % 2: - colour = QColor(Config.COLOUR_ODD_PLAYLIST) - else: - colour = QColor(Config.COLOUR_EVEN_PLAYLIST) - self.playlist.set_row_colour(row, colour) - except AttributeError: - pass + def play_previous(self): + "Resume playing last track" - def play_selected(self): - if self.playlist.selectionModel().hasSelection(): - row = self.playlist.currentRow() - track_id = int(self.playlist.item(row, 0).text()) - DEBUG(f"play_selected: track_id={track_id}") - # TODO: get_path may raise exception - track_path = Tracks.get_path(track_id) - if track_path: - DEBUG(f"play_selected: track_path={track_path}") - player = vlc.MediaPlayer(track_path) - player.play() + # TODO + pass - def selectFile(self): - dlg = QFileDialog() - dlg.setFileMode(QFileDialog.ExistingFile) - dlg.setViewMode(QFileDialog.Detail) - dlg.setDirectory(Config.ROOT) - dlg.setNameFilter("Music files (*.flac *.mp3)") - - if dlg.exec_(): - pass - # TODO Add to database - # for fname in dlg.selectedFiles(): - # track = Track(fname) - # self.add_to_playlist(track) - - def set_next_track(self): - """ - Sets the selected track as the next track. - """ - - if self.playlist.selectionModel().hasSelection(): - self.set_next_track_row(self.playlist.currentRow()) - - def set_next_track_row(self, row): - if self.playlist.item(row, 0): - track_id = int(self.playlist.item(row, 0).text()) - if track_id == self.music.get_current_track_id(): - # Don't set current track as next track - return - DEBUG(f"set_next_track: track_id={track_id}") - if self.music.set_next_track(track_id) != track_id: - ERROR("Can't set next track") - self.next_track.setText( - f"{self.music.get_next_title()} - " - f"{self.music.get_next_artist()}" - ) - self.enable_play_next_controls() - self.playlist.clearSelection() - self.set_playlist_colours() - return True - - return False + def search_database(self): + dlg = DbDialog(self) + dlg.exec() def tick(self): + pass # self.current_time.setText(now.strftime("%H:%M:%S")) - if self.music.playing(): - playtime = self.music.get_current_playtime() - self.label_elapsed_timer.setText(ms_to_mmss(playtime)) - self.label_fade_timer.setText( - ms_to_mmss(self.music.get_current_fade_at() - playtime)) - self.label_silent_timer.setText( - ms_to_mmss(self.music.get_current_silence_at() - playtime)) - self.label_end_timer.setText( - ms_to_mmss(self.music.get_current_duration() - playtime)) - else: - # When music ends, ensure next track is selected - if self.playlist.selectionModel().hasSelection(): - row = self.playlist.currentRow() - track_id = int(self.playlist.item(row, 0).text()) - if track_id == self.music.get_current_track_id(): - # Current track highlighted: select next - try: - self.playlist.selectRow(row + 1) - except AttributeError: - pass + # if self.music.playing(): + # playtime = self.music.get_current_playtime() + # self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) + # self.label_fade_timer.setText( + # helpers.ms_to_mmss(self.music.get_current_fade_at() - playtime) + # ) + # self.label_silent_timer.setText( + # helpers.ms_to_mmss( + # self.music.get_current_silence_at() - playtime)) + # self.label_end_timer.setText( + # helpers.ms_to_mmss( + # self.music.get_current_duration() - playtime)) + # else: + # # When music ends, ensure next track is selected + # if self.playlist.selectionModel().hasSelection(): + # row = self.playlist.currentRow() + # track_id = int(self.playlist.item(row, 0).text()) + # if track_id == self.music.get_current_track_id(): + # # Current track highlighted: select next + # try: + # self.playlist.selectRow(row + 1) + # except AttributeError: + # # TODO + # pass - def add_to_playlist(self, track): - """ - Add track to playlist - track is an instance of Track - """ + def update_tod_clock(self): + "Update time of day clock" - DEBUG(f"add_to_playlist: track.id={track.id}") - pl = self.playlist - row = pl.rowCount() - pl.insertRow(row) - item = QTableWidgetItem(str(track.id)) - pl.setItem(row, 0, item) - item = QTableWidgetItem(str(track.start_gap)) - pl.setItem(row, 1, item) - item = QTableWidgetItem(track.title) - pl.setItem(row, 2, item) - item = QTableWidgetItem(track.artist) - pl.setItem(row, 3, item) - item = QTableWidgetItem(ms_to_mmss(track.duration)) - pl.setItem(row, 4, item) - item = QTableWidgetItem(track.path) - pl.setItem(row, 7, item) + # TODO + pass - def disable_play_next_controls(self): - self.actionPlay_selected.setEnabled(False) - self.actionPlay_next.setEnabled(False) + def update_track_headers(self): + "Update last/current/next track header" - def enable_play_next_controls(self): - self.actionPlay_selected.setEnabled(True) - self.actionPlay_next.setEnabled(True) + # TODO + pass + def update_track_clocks(self): + "Update started at, silent at, clock times" + + # TODO + pass + + def update_track_timers(self): + "Update elapsed time, etc, timers" + + # TODO + pass class DbDialog(QDialog): def __init__(self, parent=None): @@ -454,7 +221,7 @@ class DbDialog(QDialog): t = QListWidgetItem() t.setText( f"{track.title} - {track.artist} " - f"[{ms_to_mmss(track.duration)}]" + f"[{helpers.ms_to_mmss(track.duration)}]" ) t.setData(Qt.UserRole, track.id) self.ui.matchList.addItem(t) @@ -472,24 +239,6 @@ class DbDialog(QDialog): self.parent().add_to_playlist(track) -def ms_to_mmss(ms, decimals=0, negative=False): - if not ms: - return "-" - sign = "" - if ms < 0: - if negative: - sign = "-" - else: - ms = 0 - - minutes, remainder = divmod(ms, 60 * 1000) - seconds = remainder / 1000 - if int(seconds) == 60: - DEBUG(f"ms_to_mmss({ms}) gave 60 seconds") - - return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" - - def main(): app = QApplication(sys.argv) win = Window() diff --git a/app/playlists.py b/app/playlists.py new file mode 100644 index 0000000..1920bfe --- /dev/null +++ b/app/playlists.py @@ -0,0 +1,365 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor, QDropEvent +from PyQt5.QtWidgets import ( + QTableWidget, + QAbstractItemView, + QTableWidgetItem, + QWidget, + QHBoxLayout, + QApplication +) + +import helpers +import music + +from config import Config +from log import DEBUG +from model import Playlists, Settings, Tracks + + +class Playlist(QTableWidget): + # Column names + COL_INDEX = 0 + COL_MSS = 1 + COL_TITLE = 2 + COL_ARTIST = 3 + COL_DURATION = 4 + COL_ENDTIME = 5 + COL_PATH = 6 + + # UserRoles + NEXT_TRACK = 1 + CURRENT_TRACK = 2 + COMMENT = 3 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.viewport().setAcceptDrops(True) + self.setDragDropOverwriteMode(False) + self.setDropIndicatorShown(True) + + self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setDragDropMode(QAbstractItemView.InternalMove) + + self.music = music.Music() + + self.current_track = None + self.next_track = None + self.previous_track_path = None + self.previous_track_position = None + self.played_tracks = [] + + # Column widths from settings + for column in range(self.columnCount()): + name = f"playlist_col_{str(column)}_width" + record = Settings.get_int(name) + if record.f_int is not None: + self.setColumnWidth(column, record.f_int) + + # How to put in non-track entries for later + # row = self.rowCount() + # self.insertRow(row) + # item = QTableWidgetItem("to be spanned") + # self.setItem(row, 1, item) + # self.setSpan(row, 1, 1, 3) + # colour = QColor(125, 75, 25) + # self.set_row_colour(row, colour) + + def __del__(self): + "Save column widths" + + for column in range(self.columnCount()): + name = f"playlist_col_{str(column)}_width" + record = Settings.get_int(name) + if record.f_int != self.columnWidth(column): + record.update({'f_int': self.columnWidth(column)}) + + def add_to_playlist(self, track): + """ + Add track to playlist + + track is an instance of Track + """ + + DEBUG(f"add_to_playlist: track.id={track.id}") + row = self.rowCount() + self.insertRow(row) + DEBUG(f"Adding to row {row}") + item = QTableWidgetItem(str(track.id)) + self.setItem(row, self.COL_INDEX, item) + item = QTableWidgetItem(str(track.start_gap)) + self.setItem(row, self.COL_MSS, item) + item = QTableWidgetItem(track.title) + self.setItem(row, self.COL_TITLE, item) + item = QTableWidgetItem(track.artist) + self.setItem(row, self.COL_ARTIST, item) + item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) + self.setItem(row, self.COL_DURATION, item) + item = QTableWidgetItem(track.path) + self.setItem(row, self.COL_PATH, item) + + def drop_on(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + return self.rowCount() + + return (index.row() + 1 if self.is_below(event.pos(), index) + else index.row()) + + def dropEvent(self, event: QDropEvent): + if not event.isAccepted() and event.source() == self: + drop_row = self.drop_on(event) + + rows = sorted(set(item.row() for item in self.selectedItems())) + rows_to_move = [ + [QTableWidgetItem(self.item(row_index, column_index)) for + column_index in range(self.columnCount())] + for row_index in rows + ] + for row_index in reversed(rows): + self.removeRow(row_index) + if row_index < drop_row: + drop_row -= 1 + + for row_index, data in enumerate(rows_to_move): + row_index += drop_row + self.insertRow(row_index) + for column_index, column_data in enumerate(data): + self.setItem(row_index, column_index, column_data) + event.accept() + for row_index in range(len(rows_to_move)): + for column_index in range(self.columnCount()): + self.item(drop_row + row_index, + column_index).setSelected(True) + super().dropEvent(event) + DEBUG(f"Moved row(s) {rows} to become row {drop_row}") + + def face(self): + self.music.fade() + + def get_current_artist(self): + try: + return self.current_track.artist + except AttributeError: + return "" + + def get_current_duration(self): + try: + return self.current_track.duration + except AttributeError: + return 0 + + def get_current_fade_at(self): + try: + return self.current_track.fade_at + except AttributeError: + return 0 + + def get_current_playtime(self): + + return self.music.get_playtime() + + def get_current_silence_at(self): + try: + return self.current_track.silence_at + except AttributeError: + return 0 + + def get_current_title(self): + try: + return self.current_track.title + except AttributeError: + return "" + + def get_next_artist(self): + try: + return self.next_track.artist + except AttributeError: + return "" + + def get_next_title(self): + try: + return self.next_track.title + except AttributeError: + return "" + + def get_next_track_id(self): + try: + return self.next_track.id + except AttributeError: + return 0 + + def get_previous_artist(self): + try: + return self.previous_track.artist + except AttributeError: + return "" + + def get_previous_title(self): + try: + return self.previous_track.title + except AttributeError: + return "" + + def is_below(self, pos, index): + rect = self.visualRect(index) + margin = 2 + if pos.y() - rect.top() < margin: + return False + elif rect.bottom() - pos.y() < margin: + return True + # noinspection PyTypeChecker + return ( + rect.contains(pos, True) and not + (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) + and pos.y() >= rect.center().y() # noqa W503 + ) + + def load_playlist(self, name): + "Load tracks from named playlist" + + for track in Playlists.get_playlist_by_name(name).get_tracks(): + self.add_to_playlist(track) + # Set the first playable track as next to play + for row in range(self.rowCount()): + if self.set_row_as_next_track(row): + break + + def play_next(self): + """ + Play next track. + + If there is no next track set, return. + If there's currently a track playing, fade it. + Move next track to current track. + Cue up next track in playlist if there is one. + Play (new) current. + Tell database to record it as played + Remember it was played this session + Update playlist appearance. + """ + + if not self.next_track: + return + + if self.music.playing(): + path, position = self.music.fade() + self.previous_track_path = path + self.previous_track_position = position + + self.current_track = self.next_track + self.music.play(self.current_track.path) + + # Find the playlist row for current track + current_track_id = self.current_track.track_id + self.current_track_row = None + for row in range(self.rowCount): + if self.item(row, 0): + if current_track_id == int(self.item(row, 0).text()): + self.current_track_row = row + break + + self.next_track = None + + def set_next_track(self): + """ + Sets the selected track as the next track. + """ + + # TODO + pass + + # if self.selectionModel().hasSelection(): + # self.set_next_track_row(self.playlist.currentRow()) + # if self.selectionModel().hasSelection(): + # row = self.currentRow() + # track_id = int(self.item(row, 0).text()) + # DEBUG(f"play_selected: track_id={track_id}") + # # TODO: get_path may raise exception + # music.play_track(track_id) + # track = Tracks.get_track(id) + # if not track: + # ERROR(f"set_next_track({id}): can't find track") + # return None + + # # self.next_track['player'] = vlc.MediaPlayer(track.path) + # self.next_track = track + # return track.id + + def set_row_as_next_track(self, row): + """ + If passed row is track row, indicated by having a track_id in the + COL_INDEX column, set that track as the next track to be played + and return True. Otherwise return False. + """ + + if self.item(row, self.COL_INDEX): + track_id = int(self.item(row, self.COL_INDEX).text()) + DEBUG(f"set_row_as_next_track: track_id={track_id}") + self.next_track = Tracks.get_track(track_id) + # Mark row as next track + self.item(row, self.COL_INDEX).setData( + Qt.UserRole, self.NEXT_TRACK) + self.set_playlist_colours() + return True + + return False + + def set_playlist_colours(self): + self.clearSelection() + for row in range(self.rowCount()): + if self.item(row, self.COL_INDEX): + data = self.item(row, self.COL_INDEX).data(Qt.UserRole) + if data == self.NEXT_TRACK: + self.set_row_colour( + row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + elif data == self.CURRENT_TRACK: + self.set_row_colour( + row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) + else: + if row % 2: + colour = QColor(Config.COLOUR_ODD_PLAYLIST) + else: + colour = QColor(Config.COLOUR_EVEN_PLAYLIST) + self.set_row_colour(row, colour) + + def set_row_colour(self, row, colour): + for j in range(self.columnCount()): + if self.item(row, j): + self.item(row, j).setBackground(colour) + + +class Window(QWidget): + def __init__(self): + super(Window, self).__init__() + + layout = QHBoxLayout() + self.setLayout(layout) + + self.table_widget = Playlist() + layout.addWidget(self.table_widget) + + # setup table widget + self.table_widget.setColumnCount(2) + self.table_widget.setHorizontalHeaderLabels(['Type', 'Name']) + + items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'), + ('Silver', 'Chevy'), ('Black', 'BMW')] + self.table_widget.setRowCount(len(items)) + for i, (color, model) in enumerate(items): + self.table_widget.setItem(i, 0, QTableWidgetItem(color)) + self.table_widget.setItem(i, 1, QTableWidgetItem(model)) + + self.resize(400, 400) + self.show() + + +if __name__ == '__main__': + import sys + app = QApplication(sys.argv) + window = Window() + sys.exit(app.exec_()) diff --git a/app/promoted_classes.py b/app/promoted_classes.py deleted file mode 100644 index c2ecba5..0000000 --- a/app/promoted_classes.py +++ /dev/null @@ -1,114 +0,0 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QDropEvent -from PyQt5.QtWidgets import ( - QTableWidget, - QAbstractItemView, - QTableWidgetItem, - QWidget, - QHBoxLayout, - QApplication -) - -from log import DEBUG - - -class Playlist(QTableWidget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.setDragEnabled(True) - self.setAcceptDrops(True) - self.viewport().setAcceptDrops(True) - self.setDragDropOverwriteMode(False) - self.setDropIndicatorShown(True) - - self.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setDragDropMode(QAbstractItemView.InternalMove) - - def dropEvent(self, event: QDropEvent): - if not event.isAccepted() and event.source() == self: - drop_row = self.drop_on(event) - - rows = sorted(set(item.row() for item in self.selectedItems())) - rows_to_move = [ - [QTableWidgetItem(self.item(row_index, column_index)) for - column_index in range(self.columnCount())] - for row_index in rows - ] - for row_index in reversed(rows): - self.removeRow(row_index) - if row_index < drop_row: - drop_row -= 1 - - for row_index, data in enumerate(rows_to_move): - row_index += drop_row - self.insertRow(row_index) - for column_index, column_data in enumerate(data): - self.setItem(row_index, column_index, column_data) - event.accept() - for row_index in range(len(rows_to_move)): - for column_index in range(self.columnCount()): - self.item(drop_row + row_index, - column_index).setSelected(True) - super().dropEvent(event) - DEBUG(f"Moved row(s) {rows} to become row {drop_row}") - - def drop_on(self, event): - index = self.indexAt(event.pos()) - if not index.isValid(): - return self.rowCount() - - return (index.row() + 1 if self.is_below(event.pos(), index) - else index.row()) - - def is_below(self, pos, index): - rect = self.visualRect(index) - margin = 2 - if pos.y() - rect.top() < margin: - return False - elif rect.bottom() - pos.y() < margin: - return True - # noinspection PyTypeChecker - return ( - rect.contains(pos, True) and not - (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) - and pos.y() >= rect.center().y() # noqa W503 - ) - - def set_row_colour(self, row, colour): - for j in range(self.columnCount()): - if self.item(row, j): - self.item(row, j).setBackground(colour) - - -class Window(QWidget): - def __init__(self): - super(Window, self).__init__() - - layout = QHBoxLayout() - self.setLayout(layout) - - self.table_widget = Playlist() - layout.addWidget(self.table_widget) - - # setup table widget - self.table_widget.setColumnCount(2) - self.table_widget.setHorizontalHeaderLabels(['Type', 'Name']) - - items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'), - ('Silver', 'Chevy'), ('Black', 'BMW')] - self.table_widget.setRowCount(len(items)) - for i, (color, model) in enumerate(items): - self.table_widget.setItem(i, 0, QTableWidgetItem(color)) - self.table_widget.setItem(i, 1, QTableWidgetItem(model)) - - self.resize(400, 400) - self.show() - - -if __name__ == '__main__': - import sys - app = QApplication(sys.argv) - window = Window() - sys.exit(app.exec_()) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 56e8682..3c5f4b9 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -287,7 +287,7 @@ border: 1px solid rgb(85, 87, 83); 0 - 8 + 7 0 @@ -322,11 +322,6 @@ border: 1px solid rgb(85, 87, 83); End time - - - Autoplay next - - Path @@ -694,6 +689,9 @@ border: 1px solid rgb(85, 87, 83); + + false + Track &info @@ -732,6 +730,8 @@ border: 1px solid rgb(85, 87, 83); + + @@ -751,7 +751,7 @@ border: 1px solid rgb(85, 87, 83); icon-play.pngicon-play.png - &Play selected + &Play next Return @@ -810,12 +810,21 @@ border: 1px solid rgb(85, 87, 83); Esc + + + + previous.pngprevious.png + + + &Resume previous + + Playlist QTableWidget -
promoted_classes
+
playlists