Refactor into modules

This commit is contained in:
Keith Edmunds 2021-04-03 22:45:30 +01:00
parent cf52eb3c9c
commit bcfd076a93
7 changed files with 591 additions and 467 deletions

19
app/helpers.py Normal file
View File

@ -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}"

View File

@ -1,6 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os
import sqlalchemy import sqlalchemy
from datetime import datetime from datetime import datetime
@ -102,7 +101,8 @@ class Playlists(Base):
tr = session.query(Tracks).filter(Tracks.id == 676).one() tr = session.query(Tracks).filter(Tracks.id == 676).one()
tr tr
<Track(id=676, title=Seven Nation Army, artist=White Stripes, path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac> <Track(id=676, title=Seven Nation Army, artist=White Stripes,
path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
glue.track_id = tr.id glue.track_id = tr.id
@ -132,8 +132,10 @@ class Playlists(Base):
# Currently we only support one playlist, so make that obvious from # Currently we only support one playlist, so make that obvious from
# function name # function name
@classmethod @classmethod
def get_only_playlist(cls): def get_playlist_by_name(cls, name):
return session.query(Playlists).filter(Playlists.id == 1).one() "Returns a playlist object for named playlist"
return session.query(Playlists).filter(Playlists.name == name).one()
def add_track(self, track, position): def add_track(self, track, position):
glue = PlaylistTracks(sort=position) glue = PlaylistTracks(sort=position)

94
app/music.py Normal file
View File

@ -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

View File

@ -1,180 +1,25 @@
#!/usr/bin/python3 #!/usr/bin/python3
import vlc
import sys import sys
import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
from log import DEBUG, ERROR # from log import DEBUG, ERROR
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
QDialog, QDialog,
QFileDialog, QFileDialog,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QTableWidgetItem,
) )
from ui.main_window_ui import Ui_MainWindow import helpers
from ui.dlg_search_database_ui import Ui_Dialog
from config import Config from config import Config
from model import Playdates, Playlists, Settings, Tracks from model import Playlists, Settings, Tracks
from time import sleep from ui.dlg_search_database_ui import Ui_Dialog
from ui.main_window_ui import Ui_MainWindow
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): class Window(QMainWindow, Ui_MainWindow):
@ -182,8 +27,7 @@ class Window(QMainWindow, Ui_MainWindow):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.timer = QTimer() self.timer = QTimer()
self.connectSignalsSlots() self.connect_signals_slots()
self.music = Music()
self.disable_play_next_controls() self.disable_play_next_controls()
record = Settings.get_int("mainwindow_x") record = Settings.get_int("mainwindow_x")
@ -196,39 +40,12 @@ class Window(QMainWindow, Ui_MainWindow):
height = record.f_int or 981 height = record.f_int or 981
self.setGeometry(x, y, width, height) self.setGeometry(x, y, width, height)
for column in range(self.playlist.columnCount()): # Hard code to the only playlist we have for now
name = f"playlist_col_{str(column)}_width" self.playlist.load_playlist("Default")
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) self.timer.start(Config.TIMER_MS)
def __del__(self): 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") record = Settings.get_int("mainwindow_height")
if record.f_int != self.height(): if record.f_int != self.height():
record.update({'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(): if record.f_int != self.y():
record.update({'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.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionFade.triggered.connect(self.fade) self.actionFade.triggered.connect(self.fade)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionPlay_selected.triggered.connect(self.play_next) self.actionPlay_selected.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.selectFromDatabase) self.actionSearch_database.triggered.connect(self.search_database)
self.btnSearchDatabase.clicked.connect(self.selectFromDatabase) self.btnPrevious.clicked.connect(self.play_previous)
self.btnSetNextTrack.clicked.connect(self.set_next_track) self.btnSearchDatabase.clicked.connect(self.search_database)
self.btnSetNextTrack.clicked.connect(self.playlist.set_next_track)
self.btnSkipNext.clicked.connect(self.play_next) self.btnSkipNext.clicked.connect(self.play_next)
self.btnStop.clicked.connect(self.fade) self.btnStop.clicked.connect(self.fade)
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def selectFromDatabase(self): def disable_play_next_controls(self):
dlg = DbDialog(self) self.actionPlay_selected.setEnabled(False)
dlg.exec() self.actionPlay_next.setEnabled(False)
def clear_selection(self): def enable_play_next_controls(self):
self.playlist.clearSelection() self.actionPlay_selected.setEnabled(True)
self.actionPlay_next.setEnabled(True)
def fade(self): def fade(self):
self.music.fade_current() self.playlist.fade()
def play_next(self): def play_next(self):
self.music.play_next() self.music.play_next()
@ -286,141 +122,72 @@ class Window(QMainWindow, Ui_MainWindow):
silence_at = self.music.get_current_silence_at() silence_at = self.music.get_current_silence_at()
silence_time = now + timedelta(milliseconds=silence_at) silence_time = now + timedelta(milliseconds=silence_at)
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) 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())) silence_at - self.music.get_current_fade_at()))
self.set_playlist_colours() self.set_playlist_colours()
def set_playlist_colours(self): def play_previous(self):
self.playlist.clearSelection() "Resume playing last track"
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): # TODO
if self.playlist.selectionModel().hasSelection(): pass
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): def search_database(self):
dlg = QFileDialog() dlg = DbDialog(self)
dlg.setFileMode(QFileDialog.ExistingFile) dlg.exec()
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): def tick(self):
pass
# self.current_time.setText(now.strftime("%H:%M:%S")) # self.current_time.setText(now.strftime("%H:%M:%S"))
if self.music.playing(): # if self.music.playing():
playtime = self.music.get_current_playtime() # playtime = self.music.get_current_playtime()
self.label_elapsed_timer.setText(ms_to_mmss(playtime)) # self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
self.label_fade_timer.setText( # self.label_fade_timer.setText(
ms_to_mmss(self.music.get_current_fade_at() - playtime)) # helpers.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_silent_timer.setText(
self.label_end_timer.setText( # helpers.ms_to_mmss(
ms_to_mmss(self.music.get_current_duration() - playtime)) # self.music.get_current_silence_at() - playtime))
else: # self.label_end_timer.setText(
# When music ends, ensure next track is selected # helpers.ms_to_mmss(
if self.playlist.selectionModel().hasSelection(): # self.music.get_current_duration() - playtime))
row = self.playlist.currentRow() # else:
track_id = int(self.playlist.item(row, 0).text()) # # When music ends, ensure next track is selected
if track_id == self.music.get_current_track_id(): # if self.playlist.selectionModel().hasSelection():
# Current track highlighted: select next # row = self.playlist.currentRow()
try: # track_id = int(self.playlist.item(row, 0).text())
self.playlist.selectRow(row + 1) # if track_id == self.music.get_current_track_id():
except AttributeError: # # Current track highlighted: select next
pass # 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}") # TODO
pl = self.playlist pass
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): def update_track_headers(self):
self.actionPlay_selected.setEnabled(False) "Update last/current/next track header"
self.actionPlay_next.setEnabled(False)
def enable_play_next_controls(self): # TODO
self.actionPlay_selected.setEnabled(True) pass
self.actionPlay_next.setEnabled(True)
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): class DbDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -454,7 +221,7 @@ class DbDialog(QDialog):
t = QListWidgetItem() t = QListWidgetItem()
t.setText( t.setText(
f"{track.title} - {track.artist} " f"{track.title} - {track.artist} "
f"[{ms_to_mmss(track.duration)}]" f"[{helpers.ms_to_mmss(track.duration)}]"
) )
t.setData(Qt.UserRole, track.id) t.setData(Qt.UserRole, track.id)
self.ui.matchList.addItem(t) self.ui.matchList.addItem(t)
@ -472,24 +239,6 @@ class DbDialog(QDialog):
self.parent().add_to_playlist(track) 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(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
win = Window() win = Window()

365
app/playlists.py Normal file
View File

@ -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_())

View File

@ -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_())

View File

@ -287,7 +287,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<number>0</number> <number>0</number>
</property> </property>
<property name="columnCount"> <property name="columnCount">
<number>8</number> <number>7</number>
</property> </property>
<attribute name="horizontalHeaderMinimumSectionSize"> <attribute name="horizontalHeaderMinimumSectionSize">
<number>0</number> <number>0</number>
@ -322,11 +322,6 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>End time</string> <string>End time</string>
</property> </property>
</column> </column>
<column>
<property name="text">
<string>Autoplay next</string>
</property>
</column>
<column> <column>
<property name="text"> <property name="text">
<string>Path</string> <string>Path</string>
@ -694,6 +689,9 @@ border: 1px solid rgb(85, 87, 83);</string>
</item> </item>
<item> <item>
<widget class="QPushButton" name="btnTrackInfo"> <widget class="QPushButton" name="btnTrackInfo">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text"> <property name="text">
<string>Track &amp;info</string> <string>Track &amp;info</string>
</property> </property>
@ -732,6 +730,8 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionFade"/> <addaction name="actionFade"/>
<addaction name="actionS_top"/> <addaction name="actionS_top"/>
<addaction name="separator"/>
<addaction name="action_Resume_previous"/>
<addaction name="action_Clear_selection"/> <addaction name="action_Clear_selection"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
@ -751,7 +751,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<normaloff>icon-play.png</normaloff>icon-play.png</iconset> <normaloff>icon-play.png</normaloff>icon-play.png</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Play selected</string> <string>&amp;Play next</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>Return</string> <string>Return</string>
@ -810,12 +810,21 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>Esc</string> <string>Esc</string>
</property> </property>
</action> </action>
<action name="action_Resume_previous">
<property name="icon">
<iconset>
<normaloff>previous.png</normaloff>previous.png</iconset>
</property>
<property name="text">
<string>&amp;Resume previous</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
<class>Playlist</class> <class>Playlist</class>
<extends>QTableWidget</extends> <extends>QTableWidget</extends>
<header>promoted_classes</header> <header>playlists</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>