#!/usr/bin/python3 import vlc import sys from datetime import datetime, timedelta from log import DEBUG, ERROR from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog, QListWidgetItem 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 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 } 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 play_next(self): if self.previous_track['player']: self.previous_track['player'].release() if self.current_track['player']: self.current_track['player'].stop() self.previous_track = self.current_track self.current_track = self.next_track 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) 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.actionSearch_database.triggered.connect(self.selectFromDatabase) self.actionPlay_selected.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next) self.playlist.itemSelectionChanged.connect(self.set_next_track) self.timer.timeout.connect(self.tick) def selectFromDatabase(self): dlg = DbDialog(self) dlg.exec() 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_time = now + timedelta( milliseconds=self.music.get_current_silence_at()) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) 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): """ Set the next track. In order of priority: - the highlighted track so long as it's not the current track - if the current track is highlighted, the next track if there is one - none, in which case disable play next controls """ track_id = None 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: get next if it exists try: track_id = int(self.playlist.item(row + 1, 0).text()) except AttributeError: # There is no next track track_id = None if track_id: 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() else: self.next_track.setText("") self.disable_play_next_controls() 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() db_playlist.add_track(track) # 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()