WIP: clocks working

This commit is contained in:
Keith Edmunds 2022-08-12 21:25:59 +01:00
parent afc27c988d
commit 7d71e8ce64
10 changed files with 789 additions and 797 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,151 +1,126 @@
# import os
# import threading
# import vlc
import threading
import vlc
#
# from config import Config
# from datetime import datetime
# from time import sleep
#
# from log import log.debug, log.error
#
# lock = threading.Lock()
#
#
# class Music:
# """
# Manage the playing of music tracks
# """
#
# def __init__(self):
# self.current_track_start_time = None
# self.fading = 0
# self.VLC = vlc.Instance()
# self.player = None
# self.track_path = None
# self.max_volume = Config.VOLUME_VLC_DEFAULT
#
# def fade(self):
# """
# 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.
# """
#
# log.debug("music.fade()", True)
#
# if not self.player:
# return
#
# if not self.player.get_position() > 0 and self.player.is_playing():
# return
#
# self.fading += 1
#
# thread = threading.Thread(target=self._fade)
# thread.start()
#
# 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
#
# log.debug(f"music._fade(), {self.player=}", True)
#
# with lock:
# p = self.player
# self.player = None
#
# log.debug("music._fade() post-lock", True)
#
# fade_time = Config.FADE_TIME / 1000
# steps = Config.FADE_STEPS
# sleep_time = fade_time / steps
#
# # We reduce volume by one mesure first, then by two measures,
# # then three, and so on.
#
# # The sum of the arithmetic sequence 1, 2, 3, ..n is
# # (n**2 + n) / 2
# total_measures_count = (steps**2 + steps) / 2
#
# measures_to_reduce_by = 0
# for i in range(1, steps + 1):
# measures_to_reduce_by += i
# volume_factor = 1 - (
# measures_to_reduce_by / total_measures_count)
# p.audio_set_volume(int(self.max_volume * volume_factor))
# sleep(sleep_time)
#
# with lock:
# log.debug(f"music._fade(), stopping {p=}", True)
#
# p.stop()
# log.debug(f"Releasing player {p=}", True)
# p.release()
#
# self.fading -= 1
#
# def get_playtime(self):
# """Return elapsed play time"""
#
# with lock:
# if not self.player:
# return None
#
# return self.player.get_time()
#
# def get_position(self):
# """Return current position"""
#
# with lock:
# log.debug("music.get_position", True)
#
# print(f"get_position, {self.player=}")
# if not self.player:
# return
# return self.player.get_position()
#
# def play(self, path):
# """
# Start playing the track at path.
#
# Log and return if path not found.
# """
#
# log.debug(f"music.play({path=})", True)
#
# if not os.access(path, os.R_OK):
# 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
from config import Config
from datetime import datetime
from helpers import file_is_readable
from typing import Optional
from time import sleep
from log import log
lock = threading.Lock()
class Music:
"""
Manage the playing of music tracks
"""
def __init__(self) -> None:
# self.current_track_start_time = None
# self.fading = 0
self.VLC = vlc.Instance()
self.player = None
# self.track_path = None
self.max_volume = Config.VOLUME_VLC_DEFAULT
def fade(self) -> None:
"""
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.
"""
if not self.player:
return
if not self.player.get_position() > 0 and self.player.is_playing():
return
thread = threading.Thread(target=self._fade)
thread.start()
def _fade(self) -> None:
"""
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:
p = self.player
self.player = None
# Sanity check
if not p:
return
fade_time = Config.FADE_TIME / 1000
steps = Config.FADE_STEPS
sleep_time = fade_time / steps
# We reduce volume by one mesure first, then by two measures,
# then three, and so on.
# The sum of the arithmetic sequence 1, 2, 3, ..n is
# (n**2 + n) / 2
total_measures_count = (steps**2 + steps) / 2
measures_to_reduce_by = 0
for i in range(1, steps + 1):
measures_to_reduce_by += i
volume_factor = 1 - (
measures_to_reduce_by / total_measures_count)
p.audio_set_volume(int(self.max_volume * volume_factor))
sleep(sleep_time)
with lock:
p.stop()
log.debug(f"Releasing player {p=}")
p.release()
def get_playtime(self) -> Optional[int]:
"""Return elapsed play time"""
if not self.player:
return None
return self.player.get_time()
def get_position(self) -> Optional[float]:
"""Return current position"""
if not self.player:
return None
return self.player.get_position()
def play(self, path: str,
position: Optional[float] = None) -> Optional[int]:
"""
Start playing the track at path.
Log and return if path not found.
"""
if not file_is_readable(path):
log.error(f"play({path}): path not readable")
return None
status = -1
self.track_path = path
self.player = self.VLC.media_player_new(path)
if self.player:
self.player.audio_set_volume(self.max_volume)
self.current_track_start_time = datetime.now()
status = self.player.play()
if position:
self.player.set_position(position)
return status
#
# def set_position(self, ms):
# """Set current play time in milliseconds from start"""
@ -164,20 +139,17 @@
# self.max_volume = volume
#
# self.player.audio_set_volume(volume)
#
# def stop(self):
# """Immediately stop playing"""
#
# log.debug(f"music.stop(), {self.player=}", True)
#
# with lock:
# if not self.player:
# return
#
# position = self.player.get_position()
# self.player.stop()
# log.debug(f"music.stop(): Releasing player {self.player=}", True)
# self.player.release()
# # Ensure we don't reference player after release
# self.player = None
# return position
def stop(self) -> float:
"""Immediately stop playing"""
with lock:
if not self.player:
return 0.0
position = self.player.get_position()
self.player.stop()
self.player.release()
# Ensure we don't reference player after release
self.player = None
return position

View File

@ -8,11 +8,11 @@ import sys
# import webbrowser
#
#
# from datetime import datetime, timedelta
from datetime import datetime, timedelta
# from typing import Callable, Dict, List, Optional, Tuple
#
# from PyQt5.QtCore import QDate, QEvent, QProcess, Qt, QTime, QTimer, QUrl
from PyQt5.QtCore import Qt
# from PyQt5.QtCore import QDate, QProcess, Qt, QTime, QTimer, QUrl
from PyQt5.QtCore import QEvent, Qt, QTimer
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QApplication,
@ -27,12 +27,12 @@ from PyQt5.QtWidgets import (
)
#
from dbconfig import engine, Session
# import helpers
# import music
import helpers
import music
#
from models import (
Base,
# Playdates,
Playdates,
PlaylistRows,
Playlists,
Settings,
@ -41,14 +41,13 @@ from models import (
from playlists import PlaylistTab
from sqlalchemy.orm.exc import DetachedInstanceError
# from ui.dlg_search_database_ui import Ui_Dialog
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
# from ui.downloadcsv_ui import Ui_DateSelect
from config import Config
from ui.main_window_ui import Ui_MainWindow
from ui.main_window_ui import Ui_MainWindow # type: ignore
# from utilities import create_track_from_file, update_db
#
#
# log = logging.getLogger(Config.LOG_NAME)
class TrackData:
def __init__(self, track):
self.id = track.id
@ -67,12 +66,11 @@ class Window(QMainWindow, Ui_MainWindow):
super().__init__(parent)
self.setupUi(self)
# self.timer: QTimer = QTimer()
# self.even_tick: bool = True
# self.playing: bool = False
# self.disable_play_next_controls()
#
# self.music: music.Music = music.Music()
self.timer: QTimer = QTimer()
self.even_tick: bool = True
self.playing: bool = False
self.music: music.Music = music.Music()
self.current_track: Optional[TrackData] = None
self.current_track_playlist_tab: Optional[PlaylistTab] = None
self.next_track: Optional[TrackData] = None
@ -86,16 +84,16 @@ class Window(QMainWindow, Ui_MainWindow):
# self.txtSearch = QLineEdit()
# self.statusbar.addWidget(self.txtSearch)
# 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.tabPlaylist.currentWidget
#
self._load_last_playlists()
# self.enable_play_next_controls()
self.enable_play_next_controls()
# self.check_audacity()
# self.timer.start(Config.TIMER_MS)
self.timer.start(Config.TIMER_MS)
self.connect_signals_slots()
#
# def set_main_window_size(self) -> None:
@ -137,61 +135,65 @@ class Window(QMainWindow, Ui_MainWindow):
if self.visible_playlist_tab():
self.visible_playlist_tab().clear_selection()
#
# def closeEvent(self, event: QEvent) -> None:
# """Don't allow window to close when a track is playing"""
#
# if self.music.playing():
# log.debug("closeEvent() ignored as music is playing")
# event.ignore()
# helpers.show_warning(
# "Track playing",
# "Can't close application while track is playing")
# else:
# log.debug("closeEvent() accepted")
#
# with Session() as session:
# record = Settings.get_int_settings(
# session, "mainwindow_height")
# if record.f_int != self.height():
# record.update(session, {'f_int': self.height()})
#
# record = Settings.get_int_settings(session, "mainwindow_width")
# if record.f_int != self.width():
# record.update(session, {'f_int': self.width()})
#
# record = Settings.get_int_settings(session, "mainwindow_x")
# if record.f_int != self.x():
# record.update(session, {'f_int': self.x()})
#
# record = Settings.get_int_settings(session, "mainwindow_y")
# if record.f_int != self.y():
# record.update(session, {'f_int': self.y()})
#
# # Find a playlist tab (as opposed to an info tab) and
# # save column widths
# if self.current_track_playlist_tab:
# self.current_track_playlist_tab.close()
# elif self.next_track_playlist_tab:
# self.next_track_playlist_tab.close()
#
# event.accept()
def closeEvent(self, event: QEvent) -> None:
"""Handle attempt to close main window"""
# Don't allow window to close when a track is playing
if self.music.player and self.music.player.is_playing():
event.ignore()
helpers.show_warning(
"Track playing",
"Can't close application while track is playing")
else:
with Session() as session:
record = Settings.get_int_settings(
session, "mainwindow_height")
if record.f_int != self.height():
record.update(session, {'f_int': self.height()})
record = Settings.get_int_settings(session, "mainwindow_width")
if record.f_int != self.width():
record.update(session, {'f_int': self.width()})
record = Settings.get_int_settings(session, "mainwindow_x")
if record.f_int != self.x():
record.update(session, {'f_int': self.x()})
record = Settings.get_int_settings(session, "mainwindow_y")
if record.f_int != self.y():
record.update(session, {'f_int': self.y()})
# Save splitter settings
splitter_sizes = self.splitter.sizes()
assert len(splitter_sizes) == 2
splitter_top, splitter_bottom = splitter_sizes
record = Settings.get_int_settings(session, "splitter_top")
if record.f_int != splitter_top:
record.update(session, {'f_int': splitter_top})
record = Settings.get_int_settings(session, "splitter_bottom")
if record.f_int != splitter_bottom:
record.update(session, {'f_int': splitter_bottom})
event.accept()
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.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
# self.actionDownload_CSV_of_played_tracks.triggered.connect(
# self.download_played_tracks)
# self.actionEnable_controls.triggered.connect(
# self.enable_play_next_controls)
self.actionEnable_controls.triggered.connect(
self.enable_play_next_controls)
# self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
# 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.actionNewPlaylist.triggered.connect(self.create_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_database.triggered.connect(self.search_database)
# self.actionSelect_next_track.triggered.connect(self.select_next_row)
@ -200,25 +202,19 @@ class Window(QMainWindow, Ui_MainWindow):
# self.select_previous_row)
# self.actionSelect_unplayed_tracks.triggered.connect(
# self.select_unplayed)
# self.actionSetNext.triggered.connect(
# lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
self.actionSetNext.triggered.connect(
lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
# self.actionSkip_next.triggered.connect(self.play_next)
# self.actionStop.triggered.connect(self.stop)
# # self.btnAddNote.clicked.connect(self.create_note)
# # self.btnDatabase.clicked.connect(self.search_database)
self.actionStop.triggered.connect(self.stop)
# self.btnDrop3db.clicked.connect(self.drop3db)
# self.btnHidePlayed.clicked.connect(self.hide_played)
# self.btnFade.clicked.connect(self.fade)
# # 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.btnFade.clicked.connect(self.fade)
self.btnStop.clicked.connect(self.stop)
# self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
# self.txtSearch.returnPressed.connect(self.search_playlist_return)
# 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:
# """Create new playlist"""
@ -275,19 +271,18 @@ class Window(QMainWindow, Ui_MainWindow):
add tab to display.
"""
playlist_tab: PlaylistTab = PlaylistTab(
playlist_tab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id)
idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
#
# def disable_play_next_controls(self) -> None:
# """
# Disable "play next" keyboard controls
# """
#
# log.debug("disable_play_next_controls()")
# self.actionPlay_next.setEnabled(False)
# self.statusbar.showMessage("Play controls: Disabled", 0)
def disable_play_next_controls(self) -> None:
"""
Disable "play next" keyboard controls
"""
self.actionPlay_next.setEnabled(False)
self.statusbar.showMessage("Play controls: Disabled", 0)
#
# def download_played_tracks(self) -> None:
# """Download a CSV of played tracks"""
@ -322,62 +317,61 @@ class Window(QMainWindow, Ui_MainWindow):
# self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False)
# else:
# self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False)
#
# def enable_play_next_controls(self) -> None:
# """
# Enable "play next" keyboard controls
# """
#
# log.debug("enable_play_next_controls()")
# self.actionPlay_next.setEnabled(True)
# self.statusbar.showMessage("Play controls: Enabled", 0)
#
# def end_of_track_actions(self) -> None:
# """
# Clean up after track played
#
# Actions required:
# - Set flag to say we're not playing a track
# - Reset current track
# - Tell playlist_tab track has finished
# - Reset current playlist_tab
# - Reset clocks
# - 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
# self.playing = False
#
# # Reset current track
# if self.current_track:
# self.previous_track = self.current_track
# self.current_track = None
#
# # Tell playlist_tab track has finished and
# # reset current playlist_tab
# if self.current_track_playlist_tab:
# self.current_track_playlist_tab.play_stopped()
# self.current_track_playlist_tab = None
#
# # Reset clocks
# self.frame_fade.setStyleSheet("")
# self.frame_silent.setStyleSheet("")
# self.label_elapsed_timer.setText("00:00")
# self.label_end_timer.setText("00:00")
# self.label_fade_length.setText("0:00")
# self.label_fade_timer.setText("00:00")
# self.label_silent_timer.setText("00:00")
# self.label_track_length.setText("0:00")
# self.label_start_time.setText("00:00:00")
# self.label_end_time.setText("00:00:00")
#
# # Update headers
# self.update_headers()
#
# # Enable controls
# self.enable_play_next_controls()
def enable_play_next_controls(self) -> None:
"""
Enable "play next" keyboard controls
"""
self.actionPlay_next.setEnabled(True)
self.statusbar.showMessage("Play controls: Enabled", 0)
def end_of_track_actions(self) -> None:
"""
Clean up after track played
Actions required:
- Set flag to say we're not playing a track
- Reset current track
- Tell playlist_tab track has finished
- Reset current playlist_tab
- Reset clocks
- 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
self.playing = False
# Reset current track
if self.current_track:
self.previous_track = self.current_track
self.current_track = None
# Tell playlist_tab track has finished and
# reset current playlist_tab
if self.current_track_playlist_tab:
self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab = None
# Reset clocks
self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00")
self.label_end_timer.setText("00:00")
self.label_fade_length.setText("0:00")
self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00")
self.label_track_length.setText("0:00")
self.label_start_time.setText("00:00:00")
self.label_end_time.setText("00:00:00")
# Update headers
self.update_headers()
# Enable controls
self.enable_play_next_controls()
#
# def export_playlist_tab(self) -> None:
# """Export the current playlist to an m3u file"""
@ -414,13 +408,11 @@ class Window(QMainWindow, Ui_MainWindow):
# f"{track.path}"
# "\n"
# )
#
# def fade(self) -> None:
# """Fade currently playing track"""
#
# log.debug("musicmuster:fade()", True)
#
# self.stop_playing(fade=True)
def fade(self) -> None:
"""Fade currently playing track"""
self.stop_playing(fade=True)
#
# def hide_played(self):
# """Toggle hide played tracks"""
@ -553,103 +545,91 @@ class Window(QMainWindow, Ui_MainWindow):
if destination_visible_playlist_tab:
destination_visible_playlist_tab.populate(
session, dlg.playlist.id)
#
# def play_next(self) -> None:
# """
# Play next track.
#
# Actions required:
# - If there is no next track set, return.
# - If there's currently a track playing, fade it.
# - Move next track to current track.
# - Update record of current track playlist_tab
# - If current track on different playlist_tab to last, reset
# last track playlist_tab colour
# - Set current track playlist_tab colour
# - Restore volume if -3dB active
# - Play (new) current track.
# - Tell database to record it as played
# - Tell playlist track is now playing
# - Disable play next controls
# - Update headers
# - Update clocks
# """
#
# log.debug(
# "musicmuster.play_next(), "
# f"next_track={self.next_track.title if self.next_track else None} "
# "current_track="
# f"{self.current_track.title if self.current_track else None}",
# True
# )
#
# # If there is no next track set, return.
# if not self.next_track:
# log.debug("musicmuster.play_next(): no next track selected", True)
# return
#
# with Session() as session:
# # If there's currently a track playing, fade it.
# 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 play_next(self) -> None:
"""
Play next track.
Actions required:
- If there is no next track set, return.
- If there's currently a track playing, fade it.
- Move next track to current track.
- Ensure playlist tabs are the correct colour
- Restore volume if -3dB active
- Play (new) current track.
- Tell database to record it as played
- Tell playlist track is now playing
- Note that track is now playing
- Disable play next controls
- Update headers
- Update clocks
"""
# If there is no next track set, return.
if not self.next_track:
log.debug("musicmuster.play_next(): no next track selected")
return
with Session() as session:
# If there's currently a track playing, fade it.
self.stop_playing(fade=True)
# Move next track to current track.
self.current_track = self.next_track
self.next_track = None
# Ensure playlist tabs are the correct colour
# If current track on different playlist_tab to last, reset
# last 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)
# Tell playlist track is now playing
self.current_track_playlist_tab.play_started(session)
# Note that track is now playing
self.playing = True
# 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:
# """Show dialog box to select and cue track from database"""
#
@ -709,12 +689,12 @@ class Window(QMainWindow, Ui_MainWindow):
# self.visible_playlist_tab().select_unplayed_tracks()
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)
self.tabPlaylist.tabBar().setTabTextColor(idx, colour)
idx = self.tabPlaylist.indexOf(widget)
self.tabPlaylist.tabBar().setTabTextColor(idx, colour)
#
# def song_info_search(self) -> None:
# """
@ -736,54 +716,48 @@ class Window(QMainWindow, Ui_MainWindow):
# txt = urllib.parse.quote_plus(title)
# url = Config.TAB_URL % txt
# 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 this_is_the_next_track(self, playlist_tab: PlaylistTab,
track: Tracks, session) -> None:
def stop(self) -> None:
"""Stop playing immediately"""
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
track to be played.
@ -832,85 +806,81 @@ class Window(QMainWindow, Ui_MainWindow):
# Populate 'info' tabs
self.tabInfolist.open_tab(track.title)
# def tick(self) -> None:
# """
# Carry out clock tick actions.
#
# The Time of Day clock is updated every tick (500ms).
#
# All other timers are updated every second. As the timers have a
# one-second resolution, updating every 500ms can result in some
# timers updating and then, 500ms later, other timers updating. That
# looks odd.
#
# Actions required:
# - Update TOD clock
# - If track is playing, update track clocks time and colours
# - Else: run stop_track
# """
#
# # 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:
# return
#
# # If track is playing, update track clocks time and colours
# if self.music.player and self.music.playing():
# self.playing = True
# playtime: int = self.music.get_playtime()
# time_to_fade: int = (self.current_track.fade_at - playtime)
# time_to_silence: int = (
# self.current_track.silence_at - playtime)
# time_to_end: int = (self.current_track.duration - playtime)
#
# # Elapsed time
# if time_to_end < 500:
# self.label_elapsed_timer.setText(
# helpers.ms_to_mmss(playtime)
# )
# else:
# self.label_elapsed_timer.setText(
# helpers.ms_to_mmss(playtime)
# )
#
# # Time to fade
# self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
#
# # If silent in the next 5 seconds, put warning colour on
# # time to silence box and enable play controls
# if time_to_silence <= 5500:
# self.frame_silent.setStyleSheet(
# f"background: {Config.COLOUR_ENDING_TIMER}"
# )
# self.enable_play_next_controls()
# # Set warning colour on time to silence box when fade starts
# elif time_to_fade <= 500:
# self.frame_silent.setStyleSheet(
# f"background: {Config.COLOUR_WARNING_TIMER}"
# )
# # Five seconds before fade starts, set warning colour on
# # time to silence box and enable play controls
# elif time_to_fade <= 5500:
# self.frame_fade.setStyleSheet(
# f"background: {Config.COLOUR_WARNING_TIMER}"
# )
# self.enable_play_next_controls()
# else:
# self.frame_silent.setStyleSheet("")
# self.frame_fade.setStyleSheet("")
#
# self.label_silent_timer.setText(
# helpers.ms_to_mmss(time_to_silence)
# )
#
# # Time to end
# self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
#
# else:
# if self.playing:
# self.stop_playing()
def tick(self) -> None:
"""
Carry out clock tick actions.
The Time of Day clock is updated every tick (500ms).
All other timers are updated every second. As the timer displays
have a one-second resolution, updating every 500ms can result in
some timers updating and then, 500ms later, other timers
updating. That looks odd.
Actions required:
- Update TOD clock
- If track is playing:
update track clocks time and colours
- Else:
run stop_track
"""
# 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:
return
if not self.playing:
return
# If track is playing, update track clocks time and colours
if self.music.player and self.music.player.is_playing():
playtime = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime)
time_to_silence = (
self.current_track.silence_at - playtime)
time_to_end = (self.current_track.duration - playtime)
# Elapsed time
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
# Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls
if time_to_silence <= 5500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
self.enable_play_next_controls()
else:
self.frame_silent.setStyleSheet("")
self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText(
helpers.ms_to_mmss(time_to_silence)
)
# Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
else:
if self.playing:
self.stop_playing()
def update_headers(self) -> None:
"""
@ -919,23 +889,21 @@ class Window(QMainWindow, Ui_MainWindow):
try:
self.hdrPreviousTrack.setText(
f"{self.previous_track.title} - {self.previous_track.artist}"
)
except (AttributeError, DetachedInstanceError):
f"{self.previous_track.title} - {self.previous_track.artist}")
except AttributeError:
self.hdrPreviousTrack.setText("")
try:
self.hdrCurrentTrack.setText(
f"{self.current_track.title} - {self.current_track.artist}"
)
except (AttributeError, DetachedInstanceError):
f"{self.current_track.title} - {self.current_track.artist}")
except AttributeError:
self.hdrCurrentTrack.setText("")
try:
self.hdrNextTrack.setText(
f"{self.next_track.title} - {self.next_track.artist}"
)
except (AttributeError, DetachedInstanceError):
except AttributeError:
self.hdrNextTrack.setText("")
#
#

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
"""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 ###