#!/usr/bin/python3 import vlc import sys import threading from datetime import datetime, timedelta 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 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 class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.timer = QTimer() self.connectSignalsSlots() self.music = Music() self.disable_play_next_controls() record = Settings.get_int("mainwindow_x") x = record.f_int or 1 record = Settings.get_int("mainwindow_y") y = record.f_int or 1 record = Settings.get_int("mainwindow_width") width = record.f_int or 1599 record = Settings.get_int("mainwindow_height") 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) 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()}) record = Settings.get_int("mainwindow_width") if record.f_int != self.width(): record.update({'f_int': self.width()}) record = Settings.get_int("mainwindow_x") if record.f_int != self.x(): record.update({'f_int': self.x()}) record = Settings.get_int("mainwindow_y") if record.f_int != self.y(): record.update({'f_int': self.y()}) def connectSignalsSlots(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.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 clear_selection(self): self.playlist.clearSelection() def fade(self): self.music.fade_current() def play_next(self): self.music.play_next() self.current_track.setText( f"{self.music.get_current_title()} - " f"{self.music.get_current_artist()}" ) self.previous_track.setText( f"{self.music.get_previous_title()} - " f"{self.music.get_previous_artist()}" ) self.set_next_track() # Set time clocks now = datetime.now() self.label_start_tod.setText(now.strftime("%H:%M:%S")) 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( 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_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() 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 tick(self): # 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 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}") 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) def disable_play_next_controls(self): self.actionPlay_selected.setEnabled(False) self.actionPlay_next.setEnabled(False) def enable_play_next_controls(self): self.actionPlay_selected.setEnabled(True) self.actionPlay_next.setEnabled(True) class DbDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.searchString.textEdited.connect(self.chars_typed) self.ui.matchList.itemDoubleClicked.connect(self.listdclick) record = Settings.get_int("dbdialog_width") width = record.f_int or 800 record = Settings.get_int("dbdialog_height") height = record.f_int or 600 self.resize(width, height) def __del__(self): record = Settings.get_int("dbdialog_height") if record.f_int != self.height(): record.update({'f_int': self.height()}) record = Settings.get_int("dbdialog_width") if record.f_int != self.width(): record.update({'f_int': self.width()}) def chars_typed(self, s): if len(s) >= 3: matches = Tracks.search_titles(s) self.ui.matchList.clear() if matches: for track in matches: t = QListWidgetItem() t.setText( f"{track.title} - {track.artist} " f"[{ms_to_mmss(track.duration)}]" ) t.setData(Qt.UserRole, track.id) self.ui.matchList.addItem(t) def listdclick(self, entry): track_id = entry.data(Qt.UserRole) track = Tracks.track_from_id(track_id) # Store in current playlist in database db_playlist = Playlists.get_only_playlist() # TODO: hack position to be at end of list db_playlist.add_track(track, 99999) # Add to on-screen playlist 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() win.show() sys.exit(app.exec()) if __name__ == "__main__": main()