musicmuster/app/musicmuster.py
2021-03-27 13:55:57 +00:00

392 lines
12 KiB
Python
Executable File

#!/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 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()
# 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)
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))
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)
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 seconds == 60:
print(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()