Compare commits

..

No commits in common. "7d71e8ce64de34cadb1ce90c596113e737f4e8f3" and "70c2c18fb3539d9a5a988d441afbaeb45b4997a4" have entirely different histories.

14 changed files with 874 additions and 2107 deletions

View File

@ -42,7 +42,6 @@ class Config(object):
FADE_TIME = 3000 FADE_TIME = 3000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
INFO_TAB_URL = "https://www.wikipedia.org/w/index.php?search=%s" INFO_TAB_URL = "https://www.wikipedia.org/w/index.php?search=%s"
LAST_PLAYED_TODAY_STRING = "Today"
LOG_LEVEL_STDERR = logging.DEBUG LOG_LEVEL_STDERR = logging.DEBUG
LOG_LEVEL_SYSLOG = logging.DEBUG LOG_LEVEL_SYSLOG = logging.DEBUG
LOG_NAME = "musicmuster" LOG_NAME = "musicmuster"

View File

@ -45,9 +45,8 @@ def Session() -> Generator[scoped_session, None, None]:
function = frame.function function = frame.function
lineno = frame.lineno lineno = frame.lineno
Session = scoped_session(sessionmaker(bind=engine, future=True)) Session = scoped_session(sessionmaker(bind=engine, future=True))
# log.debug(f"Session acquired, {file=}, {function=}, log.debug(f"Session acquired, {file=}, {function=}, {lineno=}, {Session=}")
# function{lineno=}, {Session=}")
yield Session yield Session
# log.debug(" Session released") log.debug(" Session released")
Session.commit() Session.commit()
Session.close() Session.close()

View File

@ -4,7 +4,7 @@ import psutil
from config import Config from config import Config
from datetime import datetime from datetime import datetime
from pydub import AudioSegment from pydub import AudioSegment
from PyQt5.QtWidgets import QMessageBox # from PyQt5.QtWidgets import QMessageBox
# from tinytag import TinyTag # from tinytag import TinyTag
from typing import Optional from typing import Optional
# from typing import Dict, Optional, Union # from typing import Dict, Optional, Union
@ -68,7 +68,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
if path.endswith('.mp3'): if path.endswith('.mp3'):
return AudioSegment.from_mp3(path) return AudioSegment.from_mp3(path)
elif path.endswith('.flac'): elif path.endswith('.flac'):
return AudioSegment.from_file(path, "flac") # type: ignore return AudioSegment.from_file(path, "flac")
except AttributeError: except AttributeError:
return None return None
@ -232,12 +232,12 @@ def open_in_audacity(path: str) -> bool:
do_command(f'Import2: Filename="{path}"') do_command(f'Import2: Filename="{path}"')
return True return True
#
#
def show_warning(title: str, msg: str) -> None: # def show_warning(title: str, msg: str) -> None:
"""Display a warning to user""" # """Display a warning to user"""
#
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel) # QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def trailing_silence( def trailing_silence(

View File

@ -1,49 +0,0 @@
import urllib.parse
from datetime import datetime
from typing import Dict, Optional
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QTabWidget
from config import Config
class InfoTabs(QTabWidget):
"""
Class to manage info tabs
"""
def __init__(self, parent=None) -> None:
super().__init__(parent)
# Dictionary to record when tabs were last updated (so we can
# re-use the oldest one later)
self.last_update: Dict[QWebEngineView, datetime] = {}
def open_tab(self, title: str) -> None:
"""
Open passed URL. Create new tab if we're below the maximum
number otherwise reuse oldest content tab.
"""
short_title = title[:Config.INFO_TAB_TITLE_LENGTH]
if self.count() < Config.MAX_INFO_TABS:
# Create a new tab
widget = QWebEngineView()
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
tab_index = self.addTab(widget, short_title)
else:
# Reuse oldest widget
widget = min(self.last_update, key=self.last_update.get)
tab_index = self.indexOf(widget)
self.setTabText(tab_index, short_title)
txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt
widget.setUrl(QUrl(url))
self.last_update[widget] = datetime.now()
# Show newly updated tab
self.setCurrentIndex(tab_index)

View File

@ -225,14 +225,16 @@ class Playdates(Base):
f"<Playdates(id={self.id}, track_id={self.track_id} " f"<Playdates(id={self.id}, track_id={self.track_id} "
f"lastplayed={self.lastplayed}>" f"lastplayed={self.lastplayed}>"
) )
#
def __init__(self, session: Session, track_id: int) -> None: # def __init__(self, session: Session, track_id: int) -> None:
"""Record that track was played""" # """Record that track was played"""
#
self.lastplayed = datetime.now() # log.debug(f"add_playdate({track_id=})")
self.track_id = track_id #
session.add(self) # self.lastplayed = datetime.now()
session.commit() # self.track_id = track_id
# session.add(self)
# session.flush()
@staticmethod @staticmethod
def last_played(session: Session, track_id: int) -> Optional[datetime]: def last_played(session: Session, track_id: int) -> Optional[datetime]:
@ -428,7 +430,6 @@ class PlaylistRows(Base):
playlist = relationship(Playlists, back_populates="rows") playlist = relationship(Playlists, back_populates="rows")
track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True)
track = relationship("Tracks", back_populates="playlistrows") track = relationship("Tracks", back_populates="playlistrows")
played = Column(Boolean, nullable=False, index=False, default=False)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -501,49 +502,12 @@ class PlaylistRows(Base):
).scalars().all() ).scalars().all()
for i, plr in enumerate(plrs): for i, plr in enumerate(plrs):
print(f"{i=}, {plr.row_number=}")
plr.row_number = i plr.row_number = i
# Ensure new row numbers are available to the caller # Ensure new row numbers are available to the caller
session.commit() session.commit()
@staticmethod
def get_played_rows(session: Session,
playlist_id: int) -> List[int]:
"""
For passed playlist, return a list of row numbers that
have been played.
"""
plrs = session.execute(
select(PlaylistRows.row_number)
.where(
PlaylistRows.playlist_id == playlist_id,
PlaylistRows.played.is_(True)
)
.order_by(PlaylistRows.row_number)
).scalars().all()
return plrs
@staticmethod
def get_rows_with_tracks(session: Session,
playlist_id: int) -> List[int]:
"""
For passed playlist, return a list of all row numbers that
contain tracks
"""
plrs = session.execute(
select(PlaylistRows.row_number)
.where(
PlaylistRows.playlist_id == playlist_id,
PlaylistRows.track_id.is_not(None)
)
.order_by(PlaylistRows.row_number)
).scalars().all()
return plrs
@staticmethod @staticmethod
def move_to_playlist(session: Session, def move_to_playlist(session: Session,
playlistrow_ids: List[int], playlistrow_ids: List[int],

View File

@ -1,126 +1,151 @@
# import os # import os
import threading # import threading
import vlc # import vlc
# #
from config import Config # from config import Config
from datetime import datetime # from datetime import datetime
from helpers import file_is_readable # from time import sleep
from typing import Optional #
from time import sleep # from log import log.debug, log.error
#
from log import log # lock = threading.Lock()
#
lock = threading.Lock() #
# class Music:
# """
class Music: # Manage the playing of music tracks
""" # """
Manage the playing of music tracks #
""" # def __init__(self):
# self.current_track_start_time = None
def __init__(self) -> None: # self.fading = 0
# self.current_track_start_time = None # self.VLC = vlc.Instance()
# self.fading = 0 # self.player = None
self.VLC = vlc.Instance() # self.track_path = None
self.player = None # self.max_volume = Config.VOLUME_VLC_DEFAULT
# self.track_path = None #
self.max_volume = Config.VOLUME_VLC_DEFAULT # def fade(self):
# """
def fade(self) -> None: # Fade the currently playing track.
""" #
Fade the currently playing track. # The actual management of fading runs in its own thread so as not
# to hold up the UI during the fade.
The actual management of fading runs in its own thread so as not # """
to hold up the UI during the fade. #
""" # log.debug("music.fade()", True)
#
if not self.player: # if not self.player:
return # return
#
if not self.player.get_position() > 0 and self.player.is_playing(): # if not self.player.get_position() > 0 and self.player.is_playing():
return # return
#
thread = threading.Thread(target=self._fade) # self.fading += 1
thread.start() #
# thread = threading.Thread(target=self._fade)
def _fade(self) -> None: # thread.start()
""" #
Implementation of fading the current track in a separate thread. # def _fade(self):
""" # """
# Implementation of fading the current track in a separate thread.
# Take a copy of current player to allow another track to be # """
# started without interfering here #
with lock: # # Take a copy of current player to allow another track to be
p = self.player # # started without interfering here
self.player = None #
# log.debug(f"music._fade(), {self.player=}", True)
# Sanity check #
if not p: # with lock:
return # p = self.player
# self.player = None
fade_time = Config.FADE_TIME / 1000 #
steps = Config.FADE_STEPS # log.debug("music._fade() post-lock", True)
sleep_time = fade_time / steps #
# fade_time = Config.FADE_TIME / 1000
# We reduce volume by one mesure first, then by two measures, # steps = Config.FADE_STEPS
# then three, and so on. # sleep_time = fade_time / steps
# The sum of the arithmetic sequence 1, 2, 3, ..n is #
# (n**2 + n) / 2 # # We reduce volume by one mesure first, then by two measures,
total_measures_count = (steps**2 + steps) / 2 # # then three, and so on.
#
measures_to_reduce_by = 0 # # The sum of the arithmetic sequence 1, 2, 3, ..n is
for i in range(1, steps + 1): # # (n**2 + n) / 2
measures_to_reduce_by += i # total_measures_count = (steps**2 + steps) / 2
volume_factor = 1 - ( #
measures_to_reduce_by / total_measures_count) # measures_to_reduce_by = 0
p.audio_set_volume(int(self.max_volume * volume_factor)) # for i in range(1, steps + 1):
sleep(sleep_time) # measures_to_reduce_by += i
# volume_factor = 1 - (
with lock: # measures_to_reduce_by / total_measures_count)
p.stop() # p.audio_set_volume(int(self.max_volume * volume_factor))
log.debug(f"Releasing player {p=}") # sleep(sleep_time)
p.release() #
# with lock:
def get_playtime(self) -> Optional[int]: # log.debug(f"music._fade(), stopping {p=}", True)
"""Return elapsed play time""" #
# p.stop()
if not self.player: # log.debug(f"Releasing player {p=}", True)
return None # p.release()
#
return self.player.get_time() # self.fading -= 1
#
def get_position(self) -> Optional[float]: # def get_playtime(self):
"""Return current position""" # """Return elapsed play time"""
#
if not self.player: # with lock:
return None # if not self.player:
return self.player.get_position() # return None
#
def play(self, path: str, # return self.player.get_time()
position: Optional[float] = None) -> Optional[int]: #
""" # def get_position(self):
Start playing the track at path. # """Return current position"""
#
Log and return if path not found. # with lock:
""" # log.debug("music.get_position", True)
#
if not file_is_readable(path): # print(f"get_position, {self.player=}")
log.error(f"play({path}): path not readable") # if not self.player:
return None # return
# return self.player.get_position()
status = -1 #
self.track_path = path # def play(self, path):
# """
self.player = self.VLC.media_player_new(path) # Start playing the track at path.
if self.player: #
self.player.audio_set_volume(self.max_volume) # Log and return if path not found.
self.current_track_start_time = datetime.now() # """
status = self.player.play() #
if position: # log.debug(f"music.play({path=})", True)
self.player.set_position(position) #
# if not os.access(path, os.R_OK):
return status # log.error(f"play({path}): path not found")
# return
#
# self.track_path = path
#
# self.player = self.VLC.media_player_new(path)
# self.player.audio_set_volume(self.max_volume)
# log.debug(f"music.play({path=}), {self.player}", True)
# self.player.play()
# self.current_track_start_time = datetime.now()
#
# def playing(self):
# """
# Return True if currently playing a track, else False
#
# vlc.is_playing() returns True if track was faded out.
# get_position seems more reliable.
# """
#
# with lock:
# if self.player:
# if self.player.get_position() > 0 and self.player.is_playing():
# return True
#
# # We take a copy of the player when fading, so we could be
# # playing in a fade nowFalse
# return self.fading > 0
# #
# def set_position(self, ms): # def set_position(self, ms):
# """Set current play time in milliseconds from start""" # """Set current play time in milliseconds from start"""
@ -139,17 +164,20 @@ class Music:
# self.max_volume = volume # self.max_volume = volume
# #
# self.player.audio_set_volume(volume) # self.player.audio_set_volume(volume)
#
def stop(self) -> float: # def stop(self):
"""Immediately stop playing""" # """Immediately stop playing"""
#
with lock: # log.debug(f"music.stop(), {self.player=}", True)
if not self.player: #
return 0.0 # with lock:
# if not self.player:
position = self.player.get_position() # return
self.player.stop() #
self.player.release() # position = self.player.get_position()
# Ensure we don't reference player after release # self.player.stop()
self.player = None # log.debug(f"music.stop(): Releasing player {self.player=}", True)
return position # self.player.release()
# # Ensure we don't reference player after release
# self.player = None
# return position

View File

@ -5,15 +5,17 @@ from log import log
# import psutil # import psutil
import sys import sys
# import threading # import threading
# import urllib.parse
# import webbrowser # import webbrowser
# #
# #
from datetime import datetime, timedelta # from datetime import datetime, timedelta
# from typing import Callable, Dict, List, Optional, Tuple # from typing import Callable, Dict, List, Optional, Tuple
# #
# from PyQt5.QtCore import QDate, QProcess, Qt, QTime, QTimer, QUrl # from PyQt5.QtCore import QDate, QEvent, QProcess, Qt, QTime, QTimer, QUrl
from PyQt5.QtCore import QEvent, Qt, QTimer from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
# from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
QDialog, QDialog,
@ -27,12 +29,13 @@ from PyQt5.QtWidgets import (
) )
# #
from dbconfig import engine, Session from dbconfig import engine, Session
import helpers # import helpers
import music # import music
# #
# from config import Config
from models import ( from models import (
Base, Base,
Playdates, # Playdates,
PlaylistRows, PlaylistRows,
Playlists, Playlists,
Settings, Settings,
@ -41,13 +44,14 @@ from models import (
from playlists import PlaylistTab from playlists import PlaylistTab
from sqlalchemy.orm.exc import DetachedInstanceError from sqlalchemy.orm.exc import DetachedInstanceError
# from ui.dlg_search_database_ui import Ui_Dialog # from ui.dlg_search_database_ui import Ui_Dialog
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
# from ui.downloadcsv_ui import Ui_DateSelect # from ui.downloadcsv_ui import Ui_DateSelect
from config import Config from config import Config
from ui.main_window_ui import Ui_MainWindow # type: ignore from ui.main_window_ui import Ui_MainWindow
# from utilities import create_track_from_file, update_db # from utilities import create_track_from_file, update_db
#
#
# log = logging.getLogger(Config.LOG_NAME)
class TrackData: class TrackData:
def __init__(self, track): def __init__(self, track):
self.id = track.id self.id = track.id
@ -66,13 +70,15 @@ class Window(QMainWindow, Ui_MainWindow):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.timer: QTimer = QTimer() # self.timer: QTimer = QTimer()
self.even_tick: bool = True # self.even_tick: bool = True
self.playing: bool = False # self.playing: bool = False
# self.disable_play_next_controls()
self.music: music.Music = music.Music() #
# self.music: music.Music = music.Music()
self.current_track: Optional[TrackData] = None self.current_track: Optional[TrackData] = None
self.current_track_playlist_tab: Optional[PlaylistTab] = None self.current_track_playlist_tab: Optional[PlaylistTab] = None
self.info_tabs: Optional[Dict[str, QWebView]] = {}
self.next_track: Optional[TrackData] = None self.next_track: Optional[TrackData] = None
self.next_track_playlist_tab: Optional[PlaylistTab] = None self.next_track_playlist_tab: Optional[PlaylistTab] = None
self.previous_track: Optional[TrackData] = None self.previous_track: Optional[TrackData] = None
@ -84,16 +90,15 @@ class Window(QMainWindow, Ui_MainWindow):
# self.txtSearch = QLineEdit() # self.txtSearch = QLineEdit()
# self.statusbar.addWidget(self.txtSearch) # self.statusbar.addWidget(self.txtSearch)
# self.txtSearch.setHidden(True) # self.txtSearch.setHidden(True)
self.hide_played_tracks = False # self.hide_played_tracks = False
# #
self.splitter.setSizes([200, 200])
self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.visible_playlist_tab: Callable[[], PlaylistTab] = \
self.tabPlaylist.currentWidget self.tabPlaylist.currentWidget
#
self._load_last_playlists() self._load_last_playlists()
self.enable_play_next_controls() # self.enable_play_next_controls()
# self.check_audacity() # self.check_audacity()
self.timer.start(Config.TIMER_MS) # self.timer.start(Config.TIMER_MS)
self.connect_signals_slots() self.connect_signals_slots()
# #
# def set_main_window_size(self) -> None: # def set_main_window_size(self) -> None:
@ -135,65 +140,61 @@ class Window(QMainWindow, Ui_MainWindow):
if self.visible_playlist_tab(): if self.visible_playlist_tab():
self.visible_playlist_tab().clear_selection() self.visible_playlist_tab().clear_selection()
#
def closeEvent(self, event: QEvent) -> None: # def closeEvent(self, event: QEvent) -> None:
"""Handle attempt to close main window""" # """Don't allow window to close when a track is playing"""
#
# Don't allow window to close when a track is playing # if self.music.playing():
if self.music.player and self.music.player.is_playing(): # log.debug("closeEvent() ignored as music is playing")
event.ignore() # event.ignore()
helpers.show_warning( # helpers.show_warning(
"Track playing", # "Track playing",
"Can't close application while track is playing") # "Can't close application while track is playing")
else: # else:
with Session() as session: # log.debug("closeEvent() accepted")
record = Settings.get_int_settings( #
session, "mainwindow_height") # with Session() as session:
if record.f_int != self.height(): # record = Settings.get_int_settings(
record.update(session, {'f_int': self.height()}) # session, "mainwindow_height")
# if record.f_int != self.height():
record = Settings.get_int_settings(session, "mainwindow_width") # record.update(session, {'f_int': self.height()})
if record.f_int != self.width(): #
record.update(session, {'f_int': self.width()}) # record = Settings.get_int_settings(session, "mainwindow_width")
# if record.f_int != self.width():
record = Settings.get_int_settings(session, "mainwindow_x") # record.update(session, {'f_int': self.width()})
if record.f_int != self.x(): #
record.update(session, {'f_int': self.x()}) # record = Settings.get_int_settings(session, "mainwindow_x")
# if record.f_int != self.x():
record = Settings.get_int_settings(session, "mainwindow_y") # record.update(session, {'f_int': self.x()})
if record.f_int != self.y(): #
record.update(session, {'f_int': self.y()}) # record = Settings.get_int_settings(session, "mainwindow_y")
# if record.f_int != self.y():
# Save splitter settings # record.update(session, {'f_int': self.y()})
splitter_sizes = self.splitter.sizes() #
assert len(splitter_sizes) == 2 # # Find a playlist tab (as opposed to an info tab) and
splitter_top, splitter_bottom = splitter_sizes # # save column widths
# if self.current_track_playlist_tab:
record = Settings.get_int_settings(session, "splitter_top") # self.current_track_playlist_tab.close()
if record.f_int != splitter_top: # elif self.next_track_playlist_tab:
record.update(session, {'f_int': splitter_top}) # self.next_track_playlist_tab.close()
#
record = Settings.get_int_settings(session, "splitter_bottom") # event.accept()
if record.f_int != splitter_bottom:
record.update(session, {'f_int': splitter_bottom})
event.accept()
def connect_signals_slots(self) -> None: def connect_signals_slots(self) -> None:
# self.actionAdd_note.triggered.connect(self.create_note) # self.actionAdd_note.triggered.connect(self.create_note)
self.action_Clear_selection.triggered.connect(self.clear_selection) self.action_Clear_selection.triggered.connect(self.clear_selection)
# self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) # self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
# self.actionDownload_CSV_of_played_tracks.triggered.connect( # self.actionDownload_CSV_of_played_tracks.triggered.connect(
# self.download_played_tracks) # self.download_played_tracks)
self.actionEnable_controls.triggered.connect( # self.actionEnable_controls.triggered.connect(
self.enable_play_next_controls) # self.enable_play_next_controls)
# self.actionExport_playlist.triggered.connect(self.export_playlist_tab) # self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
# self.actionImport.triggered.connect(self.import_track) # self.actionImport.triggered.connect(self.import_track)
self.actionFade.triggered.connect(self.fade) # self.actionFade.triggered.connect(self.fade)
# self.actionMoveSelected.triggered.connect(self.move_selected) # self.actionMoveSelected.triggered.connect(self.move_selected)
# self.actionNewPlaylist.triggered.connect(self.create_playlist) # self.actionNewPlaylist.triggered.connect(self.create_playlist)
# self.actionOpenPlaylist.triggered.connect(self.open_playlist) # self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next) # self.actionPlay_next.triggered.connect(self.play_next)
# self.actionSearch.triggered.connect(self.search_playlist) # self.actionSearch.triggered.connect(self.search_playlist)
# self.actionSearch_database.triggered.connect(self.search_database) # self.actionSearch_database.triggered.connect(self.search_database)
# self.actionSelect_next_track.triggered.connect(self.select_next_row) # self.actionSelect_next_track.triggered.connect(self.select_next_row)
@ -202,19 +203,25 @@ class Window(QMainWindow, Ui_MainWindow):
# self.select_previous_row) # self.select_previous_row)
# self.actionSelect_unplayed_tracks.triggered.connect( # self.actionSelect_unplayed_tracks.triggered.connect(
# self.select_unplayed) # self.select_unplayed)
self.actionSetNext.triggered.connect( # self.actionSetNext.triggered.connect(
lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) # lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
# self.actionSkip_next.triggered.connect(self.play_next) # self.actionSkip_next.triggered.connect(self.play_next)
self.actionStop.triggered.connect(self.stop) # self.actionStop.triggered.connect(self.stop)
# # self.btnAddNote.clicked.connect(self.create_note)
# # self.btnDatabase.clicked.connect(self.search_database)
# self.btnDrop3db.clicked.connect(self.drop3db) # self.btnDrop3db.clicked.connect(self.drop3db)
# self.btnHidePlayed.clicked.connect(self.hide_played) # self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnFade.clicked.connect(self.fade) # self.btnFade.clicked.connect(self.fade)
self.btnStop.clicked.connect(self.stop) # # self.btnPlay.clicked.connect(self.play_next)
# # self.btnSetNext.clicked.connect(
# # lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
# # self.btnSongInfo.clicked.connect(self.song_info_search)
# self.btnStop.clicked.connect(self.stop)
# self.tabPlaylist.tabCloseRequested.connect(self.close_tab) # self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
# self.txtSearch.returnPressed.connect(self.search_playlist_return) # self.txtSearch.returnPressed.connect(self.search_playlist_return)
# self.txtSearch.textChanged.connect(self.search_playlist_update) # self.txtSearch.textChanged.connect(self.search_playlist_update)
# #
self.timer.timeout.connect(self.tick) # self.timer.timeout.connect(self.tick)
# #
# def create_playlist(self) -> None: # def create_playlist(self) -> None:
# """Create new playlist""" # """Create new playlist"""
@ -271,18 +278,19 @@ class Window(QMainWindow, Ui_MainWindow):
add tab to display. add tab to display.
""" """
playlist_tab = PlaylistTab( playlist_tab: PlaylistTab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id) musicmuster=self, session=session, playlist_id=playlist.id)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
#
def disable_play_next_controls(self) -> None: # def disable_play_next_controls(self) -> None:
""" # """
Disable "play next" keyboard controls # Disable "play next" keyboard controls
""" # """
#
self.actionPlay_next.setEnabled(False) # log.debug("disable_play_next_controls()")
self.statusbar.showMessage("Play controls: Disabled", 0) # self.actionPlay_next.setEnabled(False)
# self.statusbar.showMessage("Play controls: Disabled", 0)
# #
# def download_played_tracks(self) -> None: # def download_played_tracks(self) -> None:
# """Download a CSV of played tracks""" # """Download a CSV of played tracks"""
@ -317,61 +325,62 @@ class Window(QMainWindow, Ui_MainWindow):
# self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False) # self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False)
# else: # else:
# self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False) # self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False)
#
def enable_play_next_controls(self) -> None: # def enable_play_next_controls(self) -> None:
""" # """
Enable "play next" keyboard controls # Enable "play next" keyboard controls
""" # """
#
self.actionPlay_next.setEnabled(True) # log.debug("enable_play_next_controls()")
self.statusbar.showMessage("Play controls: Enabled", 0) # self.actionPlay_next.setEnabled(True)
# self.statusbar.showMessage("Play controls: Enabled", 0)
def end_of_track_actions(self) -> None: #
""" # def end_of_track_actions(self) -> None:
Clean up after track played # """
# Clean up after track played
Actions required: #
- Set flag to say we're not playing a track # Actions required:
- Reset current track # - Set flag to say we're not playing a track
- Tell playlist_tab track has finished # - Reset current track
- Reset current playlist_tab # - Tell playlist_tab track has finished
- Reset clocks # - Reset current playlist_tab
- Update headers # - Reset clocks
- Enable controls # - Update headers
""" # - Enable controls
# """
# Set flag to say we're not playing a track so that tick() #
# doesn't see player=None and kick off end-of-track actions # # Set flag to say we're not playing a track so that tick()
self.playing = False # # doesn't see player=None and kick off end-of-track actions
# self.playing = False
# Reset current track #
if self.current_track: # # Reset current track
self.previous_track = self.current_track # if self.current_track:
self.current_track = None # self.previous_track = self.current_track
# self.current_track = None
# Tell playlist_tab track has finished and #
# reset current playlist_tab # # Tell playlist_tab track has finished and
if self.current_track_playlist_tab: # # reset current playlist_tab
self.current_track_playlist_tab.play_stopped() # if self.current_track_playlist_tab:
self.current_track_playlist_tab = None # self.current_track_playlist_tab.play_stopped()
# self.current_track_playlist_tab = None
# Reset clocks #
self.frame_fade.setStyleSheet("") # # Reset clocks
self.frame_silent.setStyleSheet("") # self.frame_fade.setStyleSheet("")
self.label_elapsed_timer.setText("00:00") # self.frame_silent.setStyleSheet("")
self.label_end_timer.setText("00:00") # self.label_elapsed_timer.setText("00:00")
self.label_fade_length.setText("0:00") # self.label_end_timer.setText("00:00")
self.label_fade_timer.setText("00:00") # self.label_fade_length.setText("0:00")
self.label_silent_timer.setText("00:00") # self.label_fade_timer.setText("00:00")
self.label_track_length.setText("0:00") # self.label_silent_timer.setText("00:00")
self.label_start_time.setText("00:00:00") # self.label_track_length.setText("0:00")
self.label_end_time.setText("00:00:00") # self.label_start_time.setText("00:00:00")
# self.label_end_time.setText("00:00:00")
# Update headers #
self.update_headers() # # Update headers
# self.update_headers()
# Enable controls #
self.enable_play_next_controls() # # Enable controls
# self.enable_play_next_controls()
# #
# def export_playlist_tab(self) -> None: # def export_playlist_tab(self) -> None:
# """Export the current playlist to an m3u file""" # """Export the current playlist to an m3u file"""
@ -408,11 +417,13 @@ class Window(QMainWindow, Ui_MainWindow):
# f"{track.path}" # f"{track.path}"
# "\n" # "\n"
# ) # )
#
def fade(self) -> None: # def fade(self) -> None:
"""Fade currently playing track""" # """Fade currently playing track"""
#
self.stop_playing(fade=True) # log.debug("musicmuster:fade()", True)
#
# self.stop_playing(fade=True)
# #
# def hide_played(self): # def hide_played(self):
# """Toggle hide played tracks""" # """Toggle hide played tracks"""
@ -545,91 +556,149 @@ class Window(QMainWindow, Ui_MainWindow):
if destination_visible_playlist_tab: if destination_visible_playlist_tab:
destination_visible_playlist_tab.populate( destination_visible_playlist_tab.populate(
session, dlg.playlist.id) session, dlg.playlist.id)
#
def play_next(self) -> None: # def open_info_tabs(self) -> None:
""" # """
Play next track. # Ensure we have info tabs for next and current track titles
# """
Actions required: #
- If there is no next track set, return. # title_list: List[str] = []
- If there's currently a track playing, fade it. #
- Move next track to current track. # if self.previous_track:
- Ensure playlist tabs are the correct colour # title_list.append(self.previous_track.title)
- Restore volume if -3dB active # if self.current_track:
- Play (new) current track. # title_list.append(self.current_track.title)
- Tell database to record it as played # if self.next_track:
- Tell playlist track is now playing # title_list.append(self.next_track.title)
- Note that track is now playing #
- Disable play next controls # for title in title_list:
- Update headers # if title in self.info_tabs.keys():
- Update clocks # # We already have a tab for this track
""" # continue
# if len(self.info_tabs) >= Config.MAX_log.info_TABS:
# If there is no next track set, return. # # Find an unneeded info tab
if not self.next_track: # try:
log.debug("musicmuster.play_next(): no next track selected") # old_title = list(
return # set(self.info_tabs.keys()) - set(title_list)
# )[0]
with Session() as session: # except IndexError:
# If there's currently a track playing, fade it. # log.debug(
self.stop_playing(fade=True) # f"ensure_info_tabs({title_list}): unable to add "
# f"{title=}"
# Move next track to current track. # )
self.current_track = self.next_track # return
self.next_track = None # # Assign redundant widget a new title
# widget = self.info_tabs[title] = self.info_tabs[old_title]
# Ensure playlist tabs are the correct colour # idx = self.tabPlaylist.indexOf(widget)
# If current track on different playlist_tab to last, reset # self.tabPlaylist.setTabText(
# last track playlist_tab colour # idx, title[:Config.log.info_TAB_TITLE_LENGTH])
if self.current_track_playlist_tab != self.next_track_playlist_tab: # del self.info_tabs[old_title]
self.set_tab_colour(self.current_track_playlist_tab, # else:
QColor(Config.COLOUR_NORMAL_TAB)) # # Create a new tab for this title
# # Update record of current track playlist_tab # widget = self.info_tabs[title] = QWebView()
self.current_track_playlist_tab = self.next_track_playlist_tab # widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
self.next_track_playlist_tab = None # self.tabPlaylist.addTab(
# Set current track playlist_tab colour # widget, title[:Config.log.info_TAB_TITLE_LENGTH])
self.set_tab_colour(self.current_track_playlist_tab, # txt = urllib.parse.quote_plus(title)
QColor(Config.COLOUR_CURRENT_TAB)) # url = Config.log.info_TAB_URL % txt
# widget.setUrl(QUrl(url))
# Restore volume if -3dB active #
if self.btnDrop3db.isChecked(): # def play_next(self) -> None:
self.btnDrop3db.setChecked(False) # """
# Play next track.
# Play (new) current track #
start_at = datetime.now() # Actions required:
self.music.play(self.current_track.path) # - If there is no next track set, return.
# - If there's currently a track playing, fade it.
# Tell database to record it as played # - Move next track to current track.
Playdates(session, self.current_track.id) # - Update record of current track playlist_tab
# - If current track on different playlist_tab to last, reset
# Tell playlist track is now playing # last track playlist_tab colour
self.current_track_playlist_tab.play_started(session) # - Set current track playlist_tab colour
# - Restore volume if -3dB active
# Note that track is now playing # - Play (new) current track.
self.playing = True # - Tell database to record it as played
# - Tell playlist track is now playing
# Disable play next controls # - Disable play next controls
self.disable_play_next_controls() # - Update headers
# - Update clocks
# Update headers # """
self.update_headers() #
# log.debug(
# Update clocks # "musicmuster.play_next(), "
self.label_track_length.setText( # f"next_track={self.next_track.title if self.next_track else None} "
helpers.ms_to_mmss(self.current_track.duration) # "current_track="
) # f"{self.current_track.title if self.current_track else None}",
fade_at = self.current_track.fade_at # True
silence_at = self.current_track.silence_at # )
length = self.current_track.duration #
self.label_fade_length.setText( # # If there is no next track set, return.
helpers.ms_to_mmss(silence_at - fade_at)) # if not self.next_track:
self.label_start_time.setText( # log.debug("musicmuster.play_next(): no next track selected", True)
start_at.strftime(Config.TRACK_TIME_FORMAT)) # return
end_at = start_at + timedelta( #
milliseconds=self.current_track.duration) # with Session() as session:
self.label_end_time.setText( # # If there's currently a track playing, fade it.
end_at.strftime(Config.TRACK_TIME_FORMAT)) # self.stop_playing(fade=True)
#
# # Move next track to current track.
# self.current_track = self.next_track
# self.next_track = None
#
# # If current track on different playlist_tab to last, reset
# # last track playlist_tab colour
# # Set current track playlist_tab colour
# if self.current_track_playlist_tab != self.next_track_playlist_tab:
# self.set_tab_colour(self.current_track_playlist_tab,
# QColor(Config.COLOUR_NORMAL_TAB))
#
# # Update record of current track playlist_tab
# self.current_track_playlist_tab = self.next_track_playlist_tab
# self.next_track_playlist_tab = None
#
# # Set current track playlist_tab colour
# self.set_tab_colour(self.current_track_playlist_tab,
# QColor(Config.COLOUR_CURRENT_TAB))
#
# # Restore volume if -3dB active
# if self.btnDrop3db.isChecked():
# self.btnDrop3db.setChecked(False)
#
# # Play (new) current track
# start_at = datetime.now()
# self.music.play(self.current_track.path)
#
# # Tell database to record it as played
# Playdates(session, self.current_track.id)
#
# # Set last_played date
# Tracks.update_lastplayed(session, self.current_track.id)
#
# # Tell playlist track is now playing
# self.current_track_playlist_tab.play_started(session)
#
# # Disable play next controls
# self.disable_play_next_controls()
#
# # Update headers
# self.update_headers()
#
# # Update clocks
# self.label_track_length.setText(
# helpers.ms_to_mmss(self.current_track.duration)
# )
# fade_at = self.current_track.fade_at
# silence_at = self.current_track.silence_at
# length = self.current_track.duration
# self.label_fade_length.setText(
# helpers.ms_to_mmss(silence_at - fade_at))
# self.label_start_time.setText(
# start_at.strftime(Config.TRACK_TIME_FORMAT))
# end_at = start_at + timedelta(
# milliseconds=self.current_track.duration)
# self.label_end_time.setText(
# end_at.strftime(Config.TRACK_TIME_FORMAT))
#
# def search_database(self) -> None: # def search_database(self) -> None:
# """Show dialog box to select and cue track from database""" # """Show dialog box to select and cue track from database"""
# #
@ -689,12 +758,12 @@ class Window(QMainWindow, Ui_MainWindow):
# self.visible_playlist_tab().select_unplayed_tracks() # self.visible_playlist_tab().select_unplayed_tracks()
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
""" """
Find the tab containing the widget and set the text colour Find the tab containing the widget and set the text colour
""" """
idx = self.tabPlaylist.indexOf(widget) idx = self.tabPlaylist.indexOf(widget)
self.tabPlaylist.tabBar().setTabTextColor(idx, colour) self.tabPlaylist.tabBar().setTabTextColor(idx, colour)
# #
# def song_info_search(self) -> None: # def song_info_search(self) -> None:
# """ # """
@ -714,50 +783,56 @@ class Window(QMainWindow, Ui_MainWindow):
# title = self.current_track.title # title = self.current_track.title
# if title: # if title:
# txt = urllib.parse.quote_plus(title) # txt = urllib.parse.quote_plus(title)
# url = Config.TAB_URL % txt # url = Config.log.info_TAB_URL % txt
# webbrowser.open(url, new=2) # webbrowser.open(url, new=2)
#
# def stop(self) -> None:
# """Stop playing immediately"""
#
# log.debug("musicmuster.stop()")
#
# self.stop_playing(fade=False)
#
# def stop_playing(self, fade=True) -> None:
# """
# Stop playing current track
#
# Actions required:
# - Return if not playing
# - Stop/fade track
# - Reset playlist_tab colour
# - Run end-of-track actions
# """
#
# log.debug(f"musicmuster.stop_playing({fade=})", True)
#
# # Return if not playing
# if not self.playing:
# log.debug("musicmuster.stop_playing(): not playing", True)
# return
#
# # Stop/fade track
# self.previous_track_position = self.music.get_position()
# if fade:
# log.debug("musicmuster.stop_playing(): fading music", True)
# self.music.fade()
# else:
# log.debug("musicmuster.stop_playing(): stopping music", True)
# self.music.stop()
#
# # Reset playlist_tab colour
# if self.current_track_playlist_tab == self.next_track_playlist_tab:
# self.set_tab_colour(self.current_track_playlist_tab,
# QColor(Config.COLOUR_NEXT_TAB))
# else:
# self.set_tab_colour(self.current_track_playlist_tab,
# QColor(Config.COLOUR_NORMAL_TAB))
#
# # Run end-of-track actions
# self.end_of_track_actions()
def stop(self) -> None: def this_is_the_next_track(self, playlist_tab: PlaylistTab,
"""Stop playing immediately""" track: Tracks, session) -> None:
self.stop_playing(fade=False)
def stop_playing(self, fade=True) -> None:
"""
Stop playing current track
Actions required:
- Return if not playing
- Stop/fade track
- Reset playlist_tab colour
- Run end-of-track actions
"""
# Return if not playing
if not self.playing:
return
# Stop/fade track
self.previous_track_position = self.music.get_position()
if fade:
self.music.fade()
else:
self.music.stop()
# Reset playlist_tab colour
if self.current_track_playlist_tab == self.next_track_playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
else:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Run end-of-track actions
self.end_of_track_actions()
def this_is_the_next_track(self, session: Session,
playlist_tab: PlaylistTab,
track: Tracks) -> None:
""" """
This is notification from a playlist tab that it holds the next This is notification from a playlist tab that it holds the next
track to be played. track to be played.
@ -804,83 +879,87 @@ class Window(QMainWindow, Ui_MainWindow):
self.update_headers() self.update_headers()
# Populate 'info' tabs # Populate 'info' tabs
self.tabInfolist.open_tab(track.title) self.open_info_tabs()
#
def tick(self) -> None: # def tick(self) -> None:
""" # """
Carry out clock tick actions. # Carry out clock tick actions.
#
The Time of Day clock is updated every tick (500ms). # The Time of Day clock is updated every tick (500ms).
#
All other timers are updated every second. As the timer displays # All other timers are updated every second. As the timers have a
have a one-second resolution, updating every 500ms can result in # one-second resolution, updating every 500ms can result in some
some timers updating and then, 500ms later, other timers # timers updating and then, 500ms later, other timers updating. That
updating. That looks odd. # looks odd.
#
Actions required: # Actions required:
- Update TOD clock # - Update TOD clock
- If track is playing: # - If track is playing, update track clocks time and colours
update track clocks time and colours # - Else: run stop_track
- Else: # """
run stop_track #
""" # # Update TOD clock
# self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
# Update TOD clock #
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) # self.even_tick = not self.even_tick
# if not self.even_tick:
self.even_tick = not self.even_tick # return
if not self.even_tick: #
return # # If track is playing, update track clocks time and colours
if not self.playing: # if self.music.player and self.music.playing():
return # self.playing = True
# playtime: int = self.music.get_playtime()
# If track is playing, update track clocks time and colours # time_to_fade: int = (self.current_track.fade_at - playtime)
if self.music.player and self.music.player.is_playing(): # time_to_silence: int = (
playtime = self.music.get_playtime() # self.current_track.silence_at - playtime)
time_to_fade = (self.current_track.fade_at - playtime) # time_to_end: int = (self.current_track.duration - playtime)
time_to_silence = ( #
self.current_track.silence_at - playtime) # # Elapsed time
time_to_end = (self.current_track.duration - playtime) # if time_to_end < 500:
# self.label_elapsed_timer.setText(
# Elapsed time # helpers.ms_to_mmss(playtime)
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) # )
# else:
# Time to fade # self.label_elapsed_timer.setText(
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) # helpers.ms_to_mmss(playtime)
# )
# If silent in the next 5 seconds, put warning colour on #
# time to silence box and enable play controls # # Time to fade
if time_to_silence <= 5500: # self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
self.frame_silent.setStyleSheet( #
f"background: {Config.COLOUR_ENDING_TIMER}" # # If silent in the next 5 seconds, put warning colour on
) # # time to silence box and enable play controls
self.enable_play_next_controls() # if time_to_silence <= 5500:
# Set warning colour on time to silence box when fade starts # self.frame_silent.setStyleSheet(
elif time_to_fade <= 500: # f"background: {Config.COLOUR_ENDING_TIMER}"
self.frame_silent.setStyleSheet( # )
f"background: {Config.COLOUR_WARNING_TIMER}" # self.enable_play_next_controls()
) # # Set warning colour on time to silence box when fade starts
# Five seconds before fade starts, set warning colour on # elif time_to_fade <= 500:
# time to silence box and enable play controls # self.frame_silent.setStyleSheet(
elif time_to_fade <= 5500: # f"background: {Config.COLOUR_WARNING_TIMER}"
self.frame_fade.setStyleSheet( # )
f"background: {Config.COLOUR_WARNING_TIMER}" # # Five seconds before fade starts, set warning colour on
) # # time to silence box and enable play controls
self.enable_play_next_controls() # elif time_to_fade <= 5500:
else: # self.frame_fade.setStyleSheet(
self.frame_silent.setStyleSheet("") # f"background: {Config.COLOUR_WARNING_TIMER}"
self.frame_fade.setStyleSheet("") # )
# self.enable_play_next_controls()
self.label_silent_timer.setText( # else:
helpers.ms_to_mmss(time_to_silence) # self.frame_silent.setStyleSheet("")
) # self.frame_fade.setStyleSheet("")
#
# Time to end # self.label_silent_timer.setText(
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) # helpers.ms_to_mmss(time_to_silence)
# )
else: #
if self.playing: # # Time to end
self.stop_playing() # self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
#
# else:
# if self.playing:
# self.stop_playing()
def update_headers(self) -> None: def update_headers(self) -> None:
""" """
@ -889,21 +968,23 @@ class Window(QMainWindow, Ui_MainWindow):
try: try:
self.hdrPreviousTrack.setText( self.hdrPreviousTrack.setText(
f"{self.previous_track.title} - {self.previous_track.artist}") f"{self.previous_track.title} - {self.previous_track.artist}"
except AttributeError: )
except (AttributeError, DetachedInstanceError):
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
try: try:
self.hdrCurrentTrack.setText( self.hdrCurrentTrack.setText(
f"{self.current_track.title} - {self.current_track.artist}") f"{self.current_track.title} - {self.current_track.artist}"
except AttributeError: )
except (AttributeError, DetachedInstanceError):
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
try: try:
self.hdrNextTrack.setText( self.hdrNextTrack.setText(
f"{self.next_track.title} - {self.next_track.artist}" f"{self.next_track.title} - {self.next_track.artist}"
) )
except AttributeError: except (AttributeError, DetachedInstanceError):
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")
# #
# #

View File

@ -1,18 +1,14 @@
import re
import subprocess
import threading
from collections import namedtuple from collections import namedtuple
from datetime import datetime, timedelta
from typing import List, Optional
from PyQt5.QtCore import QEvent, Qt, pyqtSignal from typing import List, Optional
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QBrush,
QColor, QColor,
QFont, QFont,
QDropEvent QDropEvent
) )
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
# QApplication, # QApplication,
@ -25,13 +21,17 @@ from PyQt5.QtWidgets import (
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
) )
#
import helpers
# import os
import re
import subprocess
import threading
#
from config import Config from config import Config
from dbconfig import Session from datetime import datetime # , timedelta
from helpers import ( from helpers import (
file_is_readable,
get_relative_date, get_relative_date,
ms_to_mmss,
open_in_audacity open_in_audacity
) )
from log import log from log import log
@ -43,7 +43,7 @@ from models import (
Tracks, Tracks,
NoteColours NoteColours
) )
from dbconfig import Session
start_time_re = re.compile(r"@\d\d:\d\d:\d\d") start_time_re = re.compile(r"@\d\d:\d\d:\d\d")
@ -53,6 +53,7 @@ class RowMeta:
UNREADABLE = 2 UNREADABLE = 2
NEXT = 3 NEXT = 3
CURRENT = 4 CURRENT = 4
PLAYED = 5
# Columns # Columns
@ -98,21 +99,21 @@ class PlaylistTab(QTableWidget):
def __init__(self, musicmuster: QMainWindow, session: Session, def __init__(self, musicmuster: QMainWindow, session: Session,
playlist_id: int, *args, **kwargs) -> None: playlist_id: int, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.musicmuster = musicmuster self.musicmuster: QMainWindow = musicmuster
self.playlist_id = playlist_id self.playlist_id: int = playlist_id
self.menu: Optional[QMenu] = None self.menu: Optional[QMenu] = None
self.current_track_start_time: Optional[datetime] = None # self.current_track_start_time: Optional[datetime] = None
# #
# # Don't select text on edit # # Don't select text on edit
# self.setItemDelegate(NoSelectDelegate(self)) # self.setItemDelegate(NoSelectDelegate(self))
# #
# Set up widget # Set up widget
# self.setEditTriggers(QAbstractItemView.AllEditTriggers) # self.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.setRowCount(0) self.setRowCount(0)
self.setColumnCount(len(columns)) self.setColumnCount(len(columns))
@ -136,7 +137,7 @@ class PlaylistTab(QTableWidget):
self.setDragEnabled(False) self.setDragEnabled(False)
# This property defines how the widget shows a context menu # This property defines how the widget shows a context menu
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# This signal is emitted when the widget's contextMenuPolicy is # This signal is emitted when the widget's contextMenuPolicy is
# Qt::CustomContextMenu, and the user has requested a context # Qt::CustomContextMenu, and the user has requested a context
# menu on the widget. # menu on the widget.
@ -245,8 +246,8 @@ class PlaylistTab(QTableWidget):
def eventFilter(self, source, event): def eventFilter(self, source, event):
"""Used to process context (right-click) menu, which is defined here""" """Used to process context (right-click) menu, which is defined here"""
if (event.type() == QEvent.MouseButtonPress and # noqa W504 if (event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504
event.buttons() == Qt.RightButton and # noqa W504 event.buttons() == QtCore.Qt.RightButton and # noqa W504
source is self.viewport()): source is self.viewport()):
item = self.itemAt(event.pos()) item = self.itemAt(event.pos())
if item is not None: if item is not None:
@ -363,12 +364,12 @@ class PlaylistTab(QTableWidget):
# playlist.close(session) # playlist.close(session)
# #
# event.accept() # event.accept()
#
def clear_next(self, session) -> None: # def clear_next(self, session) -> None:
"""Clear next track marker""" # """Clear next track"""
#
self._meta_clear_next() # self._meta_clear_next()
self.update_display(session) # self.update_display(session)
# #
# def create_note(self) -> None: # def create_note(self) -> None:
# """ # """
@ -393,14 +394,14 @@ class PlaylistTab(QTableWidget):
# note: Notes = Notes( # note: Notes = Notes(
# session, self.playlist_id, row, dlg.textValue()) # session, self.playlist_id, row, dlg.textValue())
# self._insert_note(session, note, row, True) # checked # self._insert_note(session, note, row, True) # checked
#
def get_selected_row(self) -> Optional[int]: # def get_selected_row(self) -> Optional[int]:
"""Return row number of first selected row, or None if none selected""" # """Return row number of first selected row, or None if none selected"""
#
if not self.selectionModel().hasSelection(): # if not self.selectionModel().hasSelection():
return None # return None
else: # else:
return self.selectionModel().selectedRows()[0].row() # return self.selectionModel().selectedRows()[0].row()
# #
# def get_selected_rows(self) -> List[int]: # def get_selected_rows(self) -> List[int]:
# """Return a sorted list of selected row numbers""" # """Return a sorted list of selected row numbers"""
@ -413,7 +414,7 @@ class PlaylistTab(QTableWidget):
# #
# if self.selectionModel().hasSelection(): # if self.selectionModel().hasSelection():
# row = self.currentRow() # row = self.currentRow()
# return self.item(row, FIXUP.COL_TITLE).text() # return self.item(row, self.COL_TITLE).text()
# else: # else:
# return None # return None
@ -457,7 +458,7 @@ class PlaylistTab(QTableWidget):
self.setItem(row, columns['artist'].idx, artist_item) self.setItem(row, columns['artist'].idx, artist_item)
duration_item = QTableWidgetItem( duration_item = QTableWidgetItem(
ms_to_mmss(row_data.track.duration)) helpers.ms_to_mmss(row_data.track.duration))
self.setItem(row, columns['duration'].idx, duration_item) self.setItem(row, columns['duration'].idx, duration_item)
self._set_row_duration(row, row_data.track.duration) self._set_row_duration(row, row_data.track.duration)
@ -478,7 +479,7 @@ class PlaylistTab(QTableWidget):
self.setItem(row, columns['lastplayed'].idx, last_played_item) self.setItem(row, columns['lastplayed'].idx, last_played_item)
# Mark track if file is unreadable # Mark track if file is unreadable
if not file_is_readable(row_data.track.path): if not helpers.file_is_readable(row_data.track.path):
self._set_unreadable_row(row) self._set_unreadable_row(row)
else: else:
@ -522,48 +523,48 @@ class PlaylistTab(QTableWidget):
# item: QTableWidgetItem = QTableWidgetItem() # item: QTableWidgetItem = QTableWidgetItem()
# # Add row metadata # # Add row metadata
# item.setData(self.ROW_FLAGS, 0) # item.setData(self.ROW_FLAGS, 0)
# self.setItem(row, FIXUP.COL_USERDATA, item) # self.setItem(row, self.COL_USERDATA, item)
# #
# # Add track details to columns # # Add track details to columns
# mss_item: QTableWidgetItem = QTableWidgetItem(str(track.start_gap)) # mss_item: QTableWidgetItem = QTableWidgetItem(str(track.start_gap))
# if track.start_gap and track.start_gap >= 500: # if track.start_gap and track.start_gap >= 500:
# mss_item.setBackground(QColor(Config.COLOUR_LONG_START)) # mss_item.setBackground(QColor(Config.COLOUR_LONG_START))
# self.setItem(row, FIXUP.COL_MSS, mss_item) # self.setItem(row, self.COL_MSS, mss_item)
# #
# title_item: QTableWidgetItem = QTableWidgetItem(track.title) # title_item: QTableWidgetItem = QTableWidgetItem(track.title)
# self.setItem(row, FIXUP.COL_TITLE, title_item) # self.setItem(row, self.COL_TITLE, title_item)
# #
# artist_item: QTableWidgetItem = QTableWidgetItem(track.artist) # artist_item: QTableWidgetItem = QTableWidgetItem(track.artist)
# self.setItem(row, FIXUP.COL_ARTIST, artist_item) # self.setItem(row, self.COL_ARTIST, artist_item)
# #
# duration_item: QTableWidgetItem = QTableWidgetItem( # duration_item: QTableWidgetItem = QTableWidgetItem(
# ms_to_mmss(track.duration) # helpers.ms_to_mmss(track.duration)
# ) # )
# self._set_row_duration(row, track.duration) # self._set_row_duration(row, track.duration)
# self.setItem(row, FIXUP.COL_DURATION, duration_item) # self.setItem(row, self.COL_DURATION, duration_item)
# #
# last_playtime: Optional[datetime] = Playdates.last_played( # last_playtime: Optional[datetime] = Playdates.last_played(
# session, track.id) # session, track.id)
# last_played_str: str = get_relative_date(last_playtime) # last_played_str: str = get_relative_date(last_playtime)
# last_played_item: QTableWidgetItem = QTableWidgetItem(last_played_str) # last_played_item: QTableWidgetItem = QTableWidgetItem(last_played_str)
# self.setItem(row, FIXUP.COL_LAST_PLAYED, last_played_item) # self.setItem(row, self.COL_LAST_PLAYED, last_played_item)
# #
# row_note: Optional[str] = "Play text" # row_note: Optional[str] = "Play text"
# row_note_item: QTableWidgetItem = QTableWidgetItem(row_note) # row_note_item: QTableWidgetItem = QTableWidgetItem(row_note)
# self.setItem(row, FIXUP.COL_ROW_NOTES, row_note_item) # self.setItem(row, self.COL_ROW_NOTES, row_note_item)
# #
# # Add empty start and stop time because background # # Add empty start and stop time because background
# # colour won't be set for columns without items # # colour won't be set for columns without items
# start_item: QTableWidgetItem = QTableWidgetItem() # start_item: QTableWidgetItem = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_START_TIME, start_item) # self.setItem(row, self.COL_START_TIME, start_item)
# stop_item: QTableWidgetItem = QTableWidgetItem() # stop_item: QTableWidgetItem = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_END_TIME, stop_item) # self.setItem(row, self.COL_END_TIME, stop_item)
# #
# # Attach track.id object to row # # Attach track.id object to row
# self._set_row_content(row, track.id) # self._set_row_content(row, track.id)
# #
# # Mark track if file is unreadable # # Mark track if file is unreadable
# if not file_is_readable(track.path): # if not helpers.file_is_readable(track.path):
# self._set_unreadable_row(row) # self._set_unreadable_row(row)
# # Scroll to new row # # Scroll to new row
# self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) # self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter)
@ -616,60 +617,62 @@ class PlaylistTab(QTableWidget):
# #
# self.save_playlist(session) # self.save_playlist(session)
# self.update_display(session) # self.update_display(session)
#
def play_started(self, session: Session) -> None: # def play_started(self, session: Session) -> None:
""" # """
Notification from musicmuster that track has started playing. # Notification from musicmuster that track has started playing.
#
Actions required: # Actions required:
- Note start time # - Note start time
- Mark next-track row as current # - Mark next-track row as current
- Mark current row as played # - Mark current row as played
- Scroll to put current track as required # - Scroll to put current track as required
- Set next track # - Set next track
- Update display # - Update display
""" # """
#
# Note start time # # Note start time
self.current_track_start_time = datetime.now() # self.current_track_start_time = datetime.now()
#
# Mark next-track row as current # # Mark next-track row as current
current_row = self._get_next_track_row() # current_row = self._get_next_track_row()
if current_row is None: # if current_row is None:
return # return
self._set_current_track_row(current_row) # self._set_current_track_row(current_row)
#
# Mark current row as played # # Mark current row as played
self._set_played_row(session, current_row) # self._set_played_row(current_row)
#
# Scroll to put current track Config.SCROLL_TOP_MARGIN from the # # Scroll to put current track as requiredin middle We want this
# top. Rows number from zero, so set (current_row - # # row to be Config.SCROLL_TOP_MARGIN from the top. Rows number
# Config.SCROLL_TOP_MARGIN + 1) row to be top row # # from zero, so set (current_row - Config.SCROLL_TOP_MARGIN + 1)
# # row to be top row
top_row = max(0, current_row - Config.SCROLL_TOP_MARGIN + 1) #
scroll_item = self.item(top_row, 0) # top_row = max(0, current_row - Config.SCROLL_TOP_MARGIN + 1)
self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop) # scroll_item = self.item(top_row, self.COL_MSS)
# self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop)
# Set next track #
search_from = current_row + 1 # # Set next track
next_row = self._find_next_track_row(session, search_from) # search_from = current_row + 1
if next_row: # next_row = self._find_next_track_row(search_from)
self._set_next(session, next_row) # if next_row:
# self._set_next(session, next_row)
# Update display #
self.update_display(session) # # Update display
# self.update_display(session)
def play_stopped(self) -> None: #
""" # def play_stopped(self) -> None:
Notification from musicmuster that track has ended. # """
# Notification from musicmuster that track has ended.
Actions required: #
- Remove current track marker # Actions required:
- Reset current track start time # - Remove current track marker
""" # - Reset current track start time
# - Update display
self._clear_current_track_row() # """
self.current_track_start_time = None #
# self._clear_current_track_row()
# self.current_track_start_time = None
def populate(self, session: Session, playlist_id: int) -> None: def populate(self, session: Session, playlist_id: int) -> None:
""" """
@ -683,14 +686,12 @@ class PlaylistTab(QTableWidget):
# row: int # row: int
# track: Tracks # track: Tracks
# Sanity check row numbering before we load playlist = session.get(Playlists, playlist_id)
PlaylistRows.fixup_rownumbers(session, playlist_id)
# Clear playlist # Clear playlist
self.setRowCount(0) self.setRowCount(0)
# Add the rows # Add the rows
playlist = session.get(Playlists, playlist_id)
for row in playlist.rows: for row in playlist.rows:
self.insert_row(session, row, repaint=False) self.insert_row(session, row, repaint=False)
@ -800,7 +801,7 @@ class PlaylistTab(QTableWidget):
# if row in notes_rows: # if row in notes_rows:
# continue # continue
# track_id: int = self.item( # track_id: int = self.item(
# row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT) # row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
# playlist.add_track(session, track_id, row) # playlist.add_track(session, track_id, row)
# session.commit() # session.commit()
# #
@ -901,16 +902,16 @@ class PlaylistTab(QTableWidget):
# self.row_filter = text # self.row_filter = text
# with Session() as session: # with Session() as session:
# self.update_display(session) # self.update_display(session)
#
def set_selected_as_next(self) -> None: # def set_selected_as_next(self) -> None:
"""Sets the select track as next to play""" # """Sets the select track as next to play"""
#
row = self.get_selected_row() # row = self.get_selected_row()
if row is None: # if row is None:
return None # return None
#
with Session() as session: # with Session() as session:
self._set_next(session, row) # self._set_next(session, row)
def update_display(self, session, clear_selection: bool = True) -> None: def update_display(self, session, clear_selection: bool = True) -> None:
""" """
@ -930,7 +931,7 @@ class PlaylistTab(QTableWidget):
current_row: Optional[int] = self._get_current_track_row() current_row: Optional[int] = self._get_current_track_row()
next_row: Optional[int] = self._get_next_track_row() next_row: Optional[int] = self._get_next_track_row()
played = PlaylistRows.get_played_rows(session, self.playlist_id) played: Optional[List[int]] = self._get_played_track_rows()
unreadable: List[int] = self._get_unreadable_track_rows() unreadable: List[int] = self._get_unreadable_track_rows()
if self.row_filter: if self.row_filter:
@ -967,7 +968,7 @@ class PlaylistTab(QTableWidget):
if track: if track:
# Render unplayable tracks in correct colour # Render unplayable tracks in correct colour
if not file_is_readable(track.path): if not helpers.file_is_readable(track.path):
self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE)) self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE))
self._set_row_bold(row) self._set_row_bold(row)
continue continue
@ -1003,7 +1004,7 @@ class PlaylistTab(QTableWidget):
self._set_row_start_time( self._set_row_start_time(
row, self.current_track_start_time) row, self.current_track_start_time)
# Set last played time to "Today" # Set last played time to "Today"
self.item(row, columns['lastplayed'].idx).setText("Today") self.item(row, self.COL_LAST_PLAYED).setText("Today")
# Calculate next_start_time # Calculate next_start_time
next_start_time = self._calculate_end_time( next_start_time = self._calculate_end_time(
self.current_track_start_time, track.duration) self.current_track_start_time, track.duration)
@ -1043,13 +1044,12 @@ class PlaylistTab(QTableWidget):
continue continue
# This is a track row other than next or current # This is a track row other than next or current
# Reset colour in case it was current/next
self._set_row_colour(row, None)
if row in played: if row in played:
# Played today, so update last played column # Played today, so update last played column
self.item(row, columns['lastplayed'].idx).setText( last_playedtime = track.lastplayed
Config.LAST_PLAYED_TODAY_STRING) last_played_str = get_relative_date(last_playedtime)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
if self.musicmuster.hide_played_tracks: if self.musicmuster.hide_played_tracks:
self.hideRow(row) self.hideRow(row)
else: else:
@ -1164,7 +1164,7 @@ class PlaylistTab(QTableWidget):
# #
# if not self.editing_cell: # if not self.editing_cell:
# return # return
# if column not in [FIXUP.COL_TITLE, FIXUP.COL_ARTIST]: # if column not in [self.COL_TITLE, self.COL_ARTIST]:
# return # return
# #
# new_text: str = self.item(row, column).text() # new_text: str = self.item(row, column).text()
@ -1192,9 +1192,9 @@ class PlaylistTab(QTableWidget):
# ) # )
# else: # else:
# track: Tracks = self._get_row_track_object(row, session) # track: Tracks = self._get_row_track_object(row, session)
# if column == FIXUP.COL_ARTIST: # if column == self.COL_ARTIST:
# track.update_artist(session, artist=new_text) # track.update_artist(session, artist=new_text)
# elif column == FIXUP.COL_TITLE: # elif column == self.COL_TITLE:
# track.update_title(session, title=new_text) # track.update_title(session, title=new_text)
# else: # else:
# log.error("_cell_changed(): unrecognised column") # log.error("_cell_changed(): unrecognised column")
@ -1232,25 +1232,29 @@ class PlaylistTab(QTableWidget):
# # database. # # database.
# #
# if self._is_note_row(row): # if self._is_note_row(row):
# item = self.item(row, FIXUP.COL_TITLE) # item = self.item(row, self.COL_TITLE)
# with Session() as session: # with Session() as session:
# note_object = self._get_row_notes_object(row, session) # note_object = self._get_row_notes_object(row, session)
# if note_object: # if note_object:
# item.setText(note_object.note) # item.setText(note_object.note)
# return # return
#
def _clear_current_track_row(self) -> None: # def _clear_current_track_row(self) -> None:
""" # """
Clear current row if there is one. # Clear current row if there is one.
""" # """
#
current_row = self._get_current_track_row() # current_row: Optional[int] = self._get_current_track_row()
# if current_row is not None:
if current_row is None: # self._meta_clear_attribute(current_row, RowMeta.CURRENT)
return # # Reset row colour
# if current_row % 2:
self._meta_clear_attribute(current_row, RowMeta.CURRENT) # self._set_row_colour(
# current_row, QColor(Config.COLOUR_ODD_PLAYLIST))
# else:
# self._set_row_colour(
# current_row, QColor(Config.COLOUR_EVEN_PLAYLIST))
#
# def _clear_played_row_status(self, row: int) -> None: # def _clear_played_row_status(self, row: int) -> None:
# """Clear played status on row""" # """Clear played status on row"""
# #
@ -1273,7 +1277,7 @@ class PlaylistTab(QTableWidget):
# Fix up row numbers left in this playlist # Fix up row numbers left in this playlist
PlaylistRows.fixup_rownumbers(session, self.playlist_id) PlaylistRows.fixup_rownumbers(session, self.playlist_id)
# Remove selected rows from display #Remove selected rows from display
self.remove_selected_rows() self.remove_selected_rows()
def _drop_on(self, event): def _drop_on(self, event):
@ -1291,7 +1295,7 @@ class PlaylistTab(QTableWidget):
# def _edit_note_cell(self, row, column): # review # def _edit_note_cell(self, row, column): # review
# """Called when table is single-clicked""" # """Called when table is single-clicked"""
# #
# if column in [FIXUP.COL_ROW_NOTES]: # if column in [self.COL_ROW_NOTES]:
# item = self.item(row, column) # item = self.item(row, column)
# self.editItem(item) # self.editItem(item)
# #
@ -1302,7 +1306,7 @@ class PlaylistTab(QTableWidget):
# column = mi.column() # column = mi.column()
# item = self.item(row, column) # item = self.item(row, column)
# #
# if column in [FIXUP.COL_TITLE, FIXUP.COL_ARTIST]: # if column in [self.COL_TITLE, self.COL_ARTIST]:
# self.editItem(item) # self.editItem(item)
# #
# def _get_notes_rows(self) -> List[int]: # def _get_notes_rows(self) -> List[int]:
@ -1327,30 +1331,32 @@ class PlaylistTab(QTableWidget):
return track_id return track_id
def _find_next_track_row(self, session: Session, # def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
starting_row: int = None) -> Optional[int]: # """
""" # Find next track to play. If a starting row is given, start there;
Find next track to play. If a starting row is given, start there; # else if there's a track selected, start looking from next track;
otherwise, start from top. Skip rows already played. # otherwise, start from top. Skip rows already played.
#
If not found, return None. # If not found, return None.
#
If found, return row number. # If found, return row number.
""" # """
#
if starting_row is None: # if starting_row is None:
starting_row = 0 # current_row = self._get_current_track_row()
# if current_row is not None:
track_rows = PlaylistRows.get_rows_with_tracks(session, # starting_row = current_row + 1
self.playlist_id) # else:
played_rows = PlaylistRows.get_played_rows(session, self.playlist_id) # starting_row = 0
for row in range(starting_row, self.rowCount()): # notes_rows = self._get_notes_rows()
if row not in track_rows or row in played_rows: # played_rows = self._get_played_track_rows()
continue # for row in range(starting_row, self.rowCount()):
else: # if row in notes_rows or row in played_rows:
return row # continue
# else:
return None # return row
#
# return None
def _get_current_track_row(self) -> Optional[int]: def _get_current_track_row(self) -> Optional[int]:
"""Return row marked as current, or None""" """Return row marked as current, or None"""
@ -1384,6 +1390,11 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None return None
def _get_played_track_rows(self) -> List[int]:
"""Return rows marked as played, or None"""
return self._meta_search(RowMeta.PLAYED, one=False)
def _get_row_duration(self, row: int) -> int: def _get_row_duration(self, row: int) -> int:
"""Return duration associated with this row""" """Return duration associated with this row"""
@ -1400,9 +1411,9 @@ class PlaylistTab(QTableWidget):
# """ # """
# #
# try: # try:
# if self.item(row, FIXUP.COL_END_TIME): # if self.item(row, self.COL_END_TIME):
# return datetime.strptime(self.item( # return datetime.strptime(self.item(
# row, FIXUP.COL_END_TIME).text(), # row, self.COL_END_TIME).text(),
# Config.NOTE_TIME_FORMAT # Config.NOTE_TIME_FORMAT
# ) # )
# else: # else:
@ -1414,7 +1425,7 @@ class PlaylistTab(QTableWidget):
# -> Optional[Notes]: # -> Optional[Notes]:
# """Return note associated with this row""" # """Return note associated with this row"""
# #
# note_id = self.item(row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT) # note_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
# note = Notes.get_by_id(session, note_id) # note = Notes.get_by_id(session, note_id)
# return note # return note
# #
@ -1442,7 +1453,7 @@ class PlaylistTab(QTableWidget):
# -> Optional[Tracks]: # -> Optional[Tracks]:
# """Return track associated with this row""" # """Return track associated with this row"""
# #
# track_id = self.item(row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT) # track_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
# track = Tracks.get_by_id(session, track_id) # track = Tracks.get_by_id(session, track_id)
# return track # return track
# #
@ -1466,9 +1477,9 @@ class PlaylistTab(QTableWidget):
f"Title: {track.title}\n" f"Title: {track.title}\n"
f"Artist: {track.artist}\n" f"Artist: {track.artist}\n"
f"Track ID: {track.id}\n" f"Track ID: {track.id}\n"
f"Track duration: {ms_to_mmss(track.duration)}\n" f"Track duration: {helpers.ms_to_mmss(track.duration)}\n"
f"Track fade at: {ms_to_mmss(track.fade_at)}\n" f"Track fade at: {helpers.ms_to_mmss(track.fade_at)}\n"
f"Track silence at: {ms_to_mmss(track.silence_at)}" f"Track silence at: {helpers.ms_to_mmss(track.silence_at)}"
"\n\n" "\n\n"
f"Path: {track.path}\n" f"Path: {track.path}\n"
) )
@ -1500,14 +1511,14 @@ class PlaylistTab(QTableWidget):
# # Add empty items to unused columns because # # Add empty items to unused columns because
# # colour won't be set for columns without items # # colour won't be set for columns without items
# item: QTableWidgetItem = QTableWidgetItem() # item: QTableWidgetItem = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_USERDATA, item) # self.setItem(row, self.COL_USERDATA, item)
# item = QTableWidgetItem() # item = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_MSS, item) # self.setItem(row, self.COL_MSS, item)
# #
# # Add text of note from title column onwards # # Add text of note from title column onwards
# titleitem: QTableWidgetItem = QTableWidgetItem(note.note) # titleitem: QTableWidgetItem = QTableWidgetItem(note.note)
# self.setItem(row, FIXUP.COL_NOTE, titleitem) # self.setItem(row, self.COL_NOTE, titleitem)
# self.setSpan(row, FIXUP.COL_NOTE, self.NOTE_ROW_SPAN, # self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
# self.NOTE_COL_SPAN) # self.NOTE_COL_SPAN)
# #
# # Attach note id to row # # Attach note id to row
@ -1696,25 +1707,28 @@ class PlaylistTab(QTableWidget):
"""Run args in subprocess""" """Run args in subprocess"""
subprocess.call(args) subprocess.call(args)
#
def _set_current_track_row(self, row: int) -> None: # def _set_current_track_row(self, row: int) -> None:
"""Mark this row as current track""" # """Mark this row as current track"""
#
self._clear_current_track_row() # self._clear_current_track_row()
self._meta_set_attribute(row, RowMeta.CURRENT) # self._meta_set_attribute(row, RowMeta.CURRENT)
def _set_next_track_row(self, row: int) -> None: def _set_next_track_row(self, row: int) -> None:
"""Mark this row as next track""" """Mark this row as next track"""
self._meta_clear_next() self._meta_clear_next()
self._meta_set_attribute(row, RowMeta.NEXT) self._meta_set_attribute(row, RowMeta.NEXT)
#
def _set_played_row(self, session: Session, row: int) -> None: # def _set_note_row(self, row: int) -> None:
"""Mark this row as played""" # """Mark this row as a note"""
#
plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) # self._meta_set_attribute(row, RowMeta.NOTE)
plr.played = True #
session.commit() # def _set_played_row(self, row: int) -> None:
# """Mark this row as played"""
#
# self._meta_set_attribute(row, RowMeta.PLAYED)
def _set_unreadable_row(self, row: int) -> None: def _set_unreadable_row(self, row: int) -> None:
"""Mark this row as unreadable""" """Mark this row as unreadable"""
@ -1744,7 +1758,7 @@ class PlaylistTab(QTableWidget):
# Only paint message if there are selected track rows # Only paint message if there are selected track rows
if ms > 0: if ms > 0:
self.musicmuster.lblSumPlaytime.setText( self.musicmuster.lblSumPlaytime.setText(
f"Selected duration: {ms_to_mmss(ms)}") f"Selected duration: {helpers.ms_to_mmss(ms)}")
else: else:
self.musicmuster.lblSumPlaytime.setText("") self.musicmuster.lblSumPlaytime.setText("")
@ -1763,7 +1777,7 @@ class PlaylistTab(QTableWidget):
# """ # """
# #
# # Need to allow multiple rows to be selected # # Need to allow multiple rows to be selected
# self.setSelectionMode(QAbstractItemView.MultiSelection) # self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
# self.clear_selection() # self.clear_selection()
# #
# if played: # if played:
@ -1775,7 +1789,7 @@ class PlaylistTab(QTableWidget):
# self.selectRow(row) # self.selectRow(row)
# #
# # Reset extended selection # # Reset extended selection
# self.setSelectionMode(QAbstractItemView.ExtendedSelection) # self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def _set_column_widths(self, session: Session) -> None: def _set_column_widths(self, session: Session) -> None:
"""Column widths from settings""" """Column widths from settings"""
@ -1814,7 +1828,7 @@ class PlaylistTab(QTableWidget):
return return
# Check track is readable # Check track is readable
if not file_is_readable(track.path): if not helpers.file_is_readable(track.path):
self._set_unreadable_row(row_number) self._set_unreadable_row(row_number)
return None return None
@ -1825,7 +1839,7 @@ class PlaylistTab(QTableWidget):
self.update_display(session) self.update_display(session)
# Notify musicmuster # Notify musicmuster
self.musicmuster.this_is_the_next_track(session, self, track) self.musicmuster.this_is_the_next_track(self, track, session)
def _set_row_bold(self, row: int, bold: bool = True) -> None: def _set_row_bold(self, row: int, bold: bool = True) -> None:
"""Make row bold (bold=True) or not bold""" """Make row bold (bold=True) or not bold"""
@ -1838,29 +1852,21 @@ class PlaylistTab(QTableWidget):
if self.item(row, j): if self.item(row, j):
self.item(row, j).setFont(boldfont) self.item(row, j).setFont(boldfont)
def _set_row_colour(self, row: int, def _set_row_colour(self, row: int, colour: QColor) -> None:
colour: Optional[QColor] = None) -> None: """Set row background colour"""
"""
Set or reset row background colour
"""
j: int j: int
if colour:
brush = QBrush(colour)
else:
brush = QBrush()
for j in range(1, self.columnCount()): for j in range(1, self.columnCount()):
if self.item(row, j): if self.item(row, j):
self.item(row, j).setBackground(brush) self.item(row, j).setBackground(colour)
# #
# def _set_row_content(self, row: int, object_id: int) -> None: # def _set_row_content(self, row: int, object_id: int) -> None:
# """Set content associated with this row""" # """Set content associated with this row"""
# #
# assert self.item(row, FIXUP.COL_USERDATA) # assert self.item(row, self.COL_USERDATA)
# #
# self.item(row, FIXUP.COL_USERDATA).setData( # self.item(row, self.COL_USERDATA).setData(
# self.CONTENT_OBJECT, object_id) # self.CONTENT_OBJECT, object_id)
def _set_row_duration(self, row: int, ms: int) -> None: def _set_row_duration(self, row: int, ms: int) -> None:
@ -1896,7 +1902,7 @@ class PlaylistTab(QTableWidget):
# def _set_timed_section(self, session, start_row, ms, no_end=False): # def _set_timed_section(self, session, start_row, ms, no_end=False):
# """Add duration to a marked section""" # """Add duration to a marked section"""
# #
# duration = ms_to_mmss(ms) # duration = helpers.ms_to_mmss(ms)
# note_object = self._get_row_notes_object(start_row, session) # note_object = self._get_row_notes_object(start_row, session)
# if not note_object: # if not note_object:
# log.error("Can't get note_object in playlists._set_timed_section") # log.error("Can't get note_object in playlists._set_timed_section")
@ -1905,7 +1911,7 @@ class PlaylistTab(QTableWidget):
# if no_end: # if no_end:
# caveat = " (to end of playlist)" # caveat = " (to end of playlist)"
# display_text = note_text + ' [' + duration + caveat + ']' # display_text = note_text + ' [' + duration + caveat + ']'
# item = self.item(start_row, FIXUP.COL_TITLE) # item = self.item(start_row, self.COL_TITLE)
# item.setText(display_text) # item.setText(display_text)
def _update_row(self, session, row: int, track: Tracks) -> None: def _update_row(self, session, row: int, track: Tracks) -> None:
@ -1928,6 +1934,6 @@ class PlaylistTab(QTableWidget):
item_artist.setText(track.artist) item_artist.setText(track.artist)
item_duration = self.item(row, columns['duration'].idx) item_duration = self.item(row, columns['duration'].idx)
item_duration.setText(ms_to_mmss(track.duration)) item_duration.setText(helpers.ms_to_mmss(track.duration))
self.update_display(session) self.update_display(session)

View File

@ -282,38 +282,19 @@ border: 1px solid rgb(85, 87, 83);</string>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QSplitter" name="splitter"> <widget class="QTabWidget" name="tabPlaylist">
<property name="orientation"> <property name="currentIndex">
<enum>Qt::Vertical</enum> <number>-1</number>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property> </property>
<widget class="QTabWidget" name="tabPlaylist">
<property name="currentIndex">
<number>-1</number>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
<widget class="InfoTabs" name="tabInfolist">
<property name="currentIndex">
<number>-1</number>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
@ -1041,14 +1022,6 @@ border: 1px solid rgb(85, 87, 83);</string>
</property> </property>
</action> </action>
</widget> </widget>
<customwidgets>
<customwidget>
<class>InfoTabs</class>
<extends>QTabWidget</extends>
<header>infotabs</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources> <resources>
<include location="icons.qrc"/> <include location="icons.qrc"/>
</resources> </resources>

View File

@ -142,20 +142,12 @@ class Ui_MainWindow(object):
self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised) self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised)
self.frame_4.setObjectName("frame_4") self.frame_4.setObjectName("frame_4")
self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1) self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1)
self.splitter = QtWidgets.QSplitter(self.centralwidget) self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget)
self.splitter.setOrientation(QtCore.Qt.Vertical)
self.splitter.setObjectName("splitter")
self.tabPlaylist = QtWidgets.QTabWidget(self.splitter)
self.tabPlaylist.setDocumentMode(False) self.tabPlaylist.setDocumentMode(False)
self.tabPlaylist.setTabsClosable(True) self.tabPlaylist.setTabsClosable(True)
self.tabPlaylist.setMovable(True) self.tabPlaylist.setMovable(True)
self.tabPlaylist.setObjectName("tabPlaylist") self.tabPlaylist.setObjectName("tabPlaylist")
self.tabInfolist = InfoTabs(self.splitter) self.gridLayout_4.addWidget(self.tabPlaylist, 2, 0, 1, 1)
self.tabInfolist.setDocumentMode(False)
self.tabInfolist.setTabsClosable(True)
self.tabInfolist.setMovable(True)
self.tabInfolist.setObjectName("tabInfolist")
self.gridLayout_4.addWidget(self.splitter, 2, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setObjectName("horizontalLayout")
self.frame = QtWidgets.QFrame(self.centralwidget) self.frame = QtWidgets.QFrame(self.centralwidget)
@ -481,7 +473,6 @@ class Ui_MainWindow(object):
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1) self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow)
@ -559,5 +550,4 @@ class Ui_MainWindow(object):
self.actionSearch.setShortcut(_translate("MainWindow", "/")) self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsert_section_header.setText(_translate("MainWindow", "Insert &section header...")) self.actionInsert_section_header.setText(_translate("MainWindow", "Insert &section header..."))
self.actionRemove.setText(_translate("MainWindow", "&Remove track")) self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
from infotabs import InfoTabs
import icons_rc import icons_rc

View File

@ -3,15 +3,15 @@ from PyQt5.QtGui import QFontMetrics, QPainter
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
# class ElideLabel(QLabel): class ElideLabel(QLabel):
# """ """
# From https://stackoverflow.com/questions/11446478/ From https://stackoverflow.com/questions/11446478/
# pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize
# """ """
#
# def paintEvent(self, event): def paintEvent(self, event):
# painter = QPainter(self) painter = QPainter(self)
# metrics = QFontMetrics(self.font()) metrics = QFontMetrics(self.font())
# elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
#
# painter.drawText(self.rect(), self.alignment(), elided) painter.drawText(self.rect(), self.alignment(), elided)

View File

@ -264,7 +264,7 @@
# # Spike # # Spike
# # # #
# # # Manage tracks listed in database but where path is invalid # # # Manage tracks listed in database but where path is invalid
# # log.debug(f"Invalid {path=} in database") # # log.debug(f"Invalid {path=} in database", True)
# # track = Tracks.get_by_path(session, path) # # track = Tracks.get_by_path(session, path)
# # messages.append(f"Remove from database: {path=} {track=}") # # messages.append(f"Remove from database: {path=} {track=}")
# # # #
@ -279,10 +279,10 @@
# # for playlist_track in track.playlists: # # for playlist_track in track.playlists:
# # row = playlist_track.row # # row = playlist_track.row
# # # Remove playlist entry # # # Remove playlist entry
# # log.debug(f"Remove {row=} from {playlist_track.playlist_id}") # # log.debug(f"Remove {row=} from {playlist_track.playlist_id}", True)
# # playlist_track.playlist.remove_track(session, row) # # playlist_track.playlist.remove_track(session, row)
# # # Create note # # # Create note
# # log.debug(f"Add note at {row=} to {playlist_track.playlist_id=}") # # log.debug(f"Add note at {row=} to {playlist_track.playlist_id=}", True)
# # Notes(session, playlist_track.playlist_id, row, note_txt) # # Notes(session, playlist_track.playlist_id, row, note_txt)
# # # #
# # # Remove Track entry pointing to invalid path # # # Remove Track entry pointing to invalid path

View File

@ -1,32 +0,0 @@
"""Add 'played' column to playlist_rows
Revision ID: 0c604bf490f8
Revises: 29c0d7ffc741
Create Date: 2022-08-12 14:12:38.419845
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '0c604bf490f8'
down_revision = '29c0d7ffc741'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('playlist_rows', sa.Column('played', sa.Boolean(), nullable=False))
op.drop_index('ix_tracks_lastplayed', table_name='tracks')
op.drop_column('tracks', 'lastplayed')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tracks', sa.Column('lastplayed', mysql.DATETIME(), nullable=True))
op.create_index('ix_tracks_lastplayed', 'tracks', ['lastplayed'], unique=False)
op.drop_column('playlist_rows', 'played')
# ### end Alembic commands ###

1192
¡

File diff suppressed because it is too large Load Diff