Compare commits

..

13 Commits

Author SHA1 Message Date
Keith Edmunds
046b689882 Fixup moving tracks between playlists.
Fixes #155
2023-01-01 15:52:06 +00:00
Keith Edmunds
74028fadf7 Set colours of tabs correctly. 2023-01-01 14:25:06 +00:00
Keith Edmunds
4edcab1542 Skip over unreadable tracks when selecting next track. 2023-01-01 11:08:37 +00:00
Keith Edmunds
5e75659c48 Don't use row metadat for next/current track
Get them from musicmuster.
2023-01-01 10:49:54 +00:00
Keith Edmunds
daf8069de2 Tidy up moving to PlaylistTrack object 2023-01-01 09:19:34 +00:00
Keith Edmunds
4beafe7cfc Fix typo Session→session 2023-01-01 08:33:06 +00:00
Keith Edmunds
2a484d51d3 Remove function_logger
It doesn't work properly (call methods with an additional "None"
argument).
2023-01-01 08:11:41 +00:00
Keith Edmunds
b476db188f Implement PlaylistTrack object 2022-12-30 21:43:47 +00:00
Keith Edmunds
ce08790343 Merge branch 'resume_use_plrs_issue128' into use_plrs 2022-12-30 10:37:02 +00:00
Keith Edmunds
1d35574224 Add flakehell to pyproject 2022-12-30 10:29:38 +00:00
Keith Edmunds
f1c27e0e8c WIP 2022-12-29 08:56:58 +00:00
Keith Edmunds
aa405cd6d9 WIP for resume play 2022-12-28 15:08:54 +00:00
Keith Edmunds
8f8c6a1034 Remove redundant code 2022-12-28 09:33:40 +00:00
9 changed files with 536 additions and 749 deletions

View File

@ -97,8 +97,8 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
) )
def get_relative_date(past_date: datetime, reference_date: datetime = None) \ def get_relative_date(past_date: datetime,
-> str: reference_date: Optional[datetime] = None) -> str:
""" """
Return how long before reference_date past_date is as string. Return how long before reference_date past_date is as string.

View File

@ -93,35 +93,6 @@ class NoteColours(Base):
f"colour={self.colour}>" f"colour={self.colour}>"
) )
# def __init__(
# self, session: Session, substring: str, colour: str,
# enabled: bool = True, is_regex: bool = False,
# is_casesensitive: bool = False, order: int = 0) -> None:
# self.substring = substring
# self.colour = colour
# self.enabled = enabled
# self.is_regex = is_regex
# self.is_casesensitive = is_casesensitive
# self.order = order
#
# session.add(self)
# session.flush()
#
# @classmethod
# def get_all(cls, session: Session) ->
# Optional[List["NoteColours"]]:
# """Return all records"""
#
# return session.query(cls).all()
#
# @classmethod
# def get_by_id(cls, session: Session, note_id: int) -> \
# Optional["NoteColours"]:
# """Return record identified by id, or None if not found"""
#
# return session.query(NoteColours).filter(
# NoteColours.id == note_id).first()
@staticmethod @staticmethod
def get_colour(session: Session, text: str) -> Optional[str]: def get_colour(session: Session, text: str) -> Optional[str]:
""" """
@ -206,16 +177,6 @@ class Playdates(Base):
.all() .all()
) )
# @staticmethod
# def remove_track(session: Session, track_id: int) -> None:
# """
# Remove all records of track_id
# """
#
# session.query(Playdates).filter(
# Playdates.track_id == track_id).delete()
# session.flush()
class Playlists(Base): class Playlists(Base):
""" """
@ -250,27 +211,14 @@ class Playlists(Base):
session.add(self) session.add(self)
session.commit() session.commit()
# def add_track(
# self, session: Session, track_id: int,
# row: Optional[int] = None) -> None:
# """
# Add track to playlist at given row.
# If row=None, add to end of playlist
# """
#
# if row is None:
# row = self.next_free_row(session, self.id)
#
# xPlaylistTracks(session, self.id, track_id, row)
def close(self, session: Session) -> None: def close(self, session: Session) -> None:
"""Mark playlist as unloaded""" """Mark playlist as unloaded"""
# Closing this tab will mean all higher-number tabs have moved
# down by one
closed_idx = self.tab closed_idx = self.tab
self.tab = None self.tab = None
# Closing this tab will mean all higher-number tabs have moved
# down by one
session.execute( session.execute(
update(Playlists) update(Playlists)
.where(Playlists.tab > closed_idx) .where(Playlists.tab > closed_idx)

View File

@ -1,6 +1,6 @@
# import os # import os
import threading import threading
import vlc import vlc # type: ignore
# #
from config import Config from config import Config
from datetime import datetime from datetime import datetime
@ -114,7 +114,6 @@ class Music:
self.player = self.VLC.media_player_new(path) self.player = self.VLC.media_player_new(path)
if self.player: if self.player:
self.player.audio_set_volume(self.max_volume) self.player.audio_set_volume(self.max_volume)
self.current_track_start_time = datetime.now()
status = self.player.play() status = self.player.play()
if position: if position:
self.player.set_position(position) self.player.set_position(position)

View File

@ -9,7 +9,7 @@ import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep from time import sleep
from typing import List, Optional from typing import Callable, List, Optional
from PyQt5.QtCore import pyqtSignal, QDate, QEvent, Qt, QSize, QTime, QTimer from PyQt5.QtCore import pyqtSignal, QDate, QEvent, Qt, QSize, QTime, QTimer
from PyQt5.QtGui import QColor, QFont, QPalette, QResizeEvent from PyQt5.QtGui import QColor, QFont, QPalette, QResizeEvent
@ -111,17 +111,72 @@ class CartButton(QPushButton):
self.pgb.setGeometry(0, 0, self.width(), 10) self.pgb.setGeometry(0, 0, self.width(), 10)
class TrackData: class PlaylistTrack:
def __init__(self, track): """
self.id = track.id Used to provide a single reference point for specific playlist tracks,
self.title = track.title typicall the previous, current and next track.
"""
def __init__(self) -> None:
"""
Only initialises data structure. Call set_plr to populate.
"""
self.artist = None
self.duration = None
self.end_time = None
self.fade_at = None
self.fade_length = None
self.path = None
self.playlist_id = None
self.playlist_tab = None
self.plr_id = None
self.row_number = None
self.silence_at = None
self.start_gap = None
self.start_time = None
self.title = None
self.track_id = None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"row_number={self.row_number} playlist_id={self.playlist_id}>"
)
def set_plr(self, session: Session, plr: PlaylistRows,
tab: PlaylistTab) -> None:
"""
Update with new plr information
"""
self.playlist_tab = tab
session.add(plr)
track = plr.track
self.artist = track.artist self.artist = track.artist
self.duration = track.duration self.duration = track.duration
self.start_gap = track.start_gap self.end_time = None
self.fade_at = track.fade_at self.fade_at = track.fade_at
self.silence_at = track.silence_at self.fade_length = track.silence_at - track.fade_at
self.path = track.path self.path = track.path
self.mtime = track.mtime self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.row_number = plr.row_number
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
def start(self) -> None:
"""
Called when track starts playing
"""
self.start_time = datetime.now()
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
@ -131,15 +186,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer: QTimer = QTimer() self.timer: QTimer = QTimer()
self.even_tick: bool = True self.even_tick: bool = True
self.playing: bool = False
self.music: music.Music = music.Music() self.music: music.Music = music.Music()
self.current_track: Optional[TrackData] = None self.playing: bool = False
self.current_track_playlist_tab: Optional[PlaylistTab] = None
self.current_track_end_time = None self.current_track = PlaylistTrack()
self.next_track: Optional[TrackData] = None self.next_track = PlaylistTrack()
self.next_track_playlist_tab: Optional[PlaylistTab] = None self.previous_track = PlaylistTrack()
self.previous_track: Optional[TrackData] = None
self.previous_track_position: Optional[int] = None self.previous_track_position: Optional[int] = None
self.selected_plrs = None self.selected_plrs = None
@ -305,7 +359,7 @@ class Window(QMainWindow, Ui_MainWindow):
"""Handle attempt to close main window""" """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.player and self.music.player.is_playing(): if self.playing:
event.ignore() event.ignore()
helpers.show_warning( helpers.show_warning(
"Track playing", "Track playing",
@ -370,7 +424,7 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Don't close next track playlist # Don't close next track playlist
if self.tabPlaylist.widget(tab_index) == self.next_track_playlist_tab: if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
self.statusbar.showMessage( self.statusbar.showMessage(
"Can't close next track playlist", 5000) "Can't close next track playlist", 5000)
return return
@ -410,6 +464,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionOpenPlaylist.triggered.connect(self.open_playlist) self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPaste.triggered.connect(self.paste_rows) self.actionPaste.triggered.connect(self.paste_rows)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionResume.triggered.connect(self.resume)
self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch.triggered.connect(self.search_playlist) self.actionSearch.triggered.connect(self.search_playlist)
self.actionSelect_next_track.triggered.connect(self.select_next_row) self.actionSelect_next_track.triggered.connect(self.select_next_row)
@ -555,16 +610,14 @@ class Window(QMainWindow, Ui_MainWindow):
# doesn't see player=None and kick off end-of-track actions # doesn't see player=None and kick off end-of-track actions
self.playing = False self.playing = False
# Reset current track # Repaint playlist to remove currently playing track colour
if self.current_track: with Session() as session:
self.previous_track = self.current_track self.current_track.playlist_tab.update_display(session)
self.current_track = None
# Tell playlist_tab track has finished and # Reset PlaylistTrack objects
# reset current playlist_tab if self.current_track.track_id:
if self.current_track_playlist_tab: self.previous_track = self.current_track
self.current_track_playlist_tab.play_stopped() self.current_track = PlaylistTrack()
self.current_track_playlist_tab = None
# Reset clocks # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
@ -576,7 +629,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_start_time.setText("00:00:00") self.label_start_time.setText("00:00:00")
self.label_end_time.setText("00:00:00") self.label_end_time.setText("00:00:00")
if self.next_track: if self.next_track.track_id:
self.label_track_length.setText( self.label_track_length.setText(
helpers.ms_to_mmss(self.next_track.duration) helpers.ms_to_mmss(self.next_track.duration)
) )
@ -586,9 +639,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_track_length.setText("0:00") self.label_track_length.setText("0:00")
self.label_fade_length.setText("0:00") self.label_fade_length.setText("0:00")
# Reset end time
self.current_track_end_time = None
# Update headers # Update headers
self.update_headers() self.update_headers()
@ -789,23 +839,29 @@ class Window(QMainWindow, Ui_MainWindow):
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
Actions required: Actions required:
- exclude current/next tracks from being moved
- identify destination playlist - identify destination playlist
- update playlist for the rows in the database - update playlist for the rows in the database
- remove them from the display - remove them from the display
- update destination playlist display if loaded - update destination playlist display if loaded
""" """
if not playlistrows: # Remove current/next rows from list
log.debug(f"musicmuster.move_playlist_rows({playlistrows=}") plrs_to_move = [plr for plr in playlistrows if
plr.id not in
[self.current_track.plr_id,
self.next_track.plr_id]
]
rows_to_delete = [plr.row_number for plr in plrs_to_move]
# Identify destination playlist # Identify destination playlist
visible_tab = self.visible_playlist_tab()
source_playlist = visible_tab.playlist_id
# Get destination playlist id
playlists = [] playlists = []
visible_tab = self.visible_playlist_tab()
source_playlist_id = visible_tab.playlist_id
for playlist in Playlists.get_all(session): for playlist in Playlists.get_all(session):
if playlist.id == source_playlist: if playlist.id == source_playlist_id:
continue continue
else: else:
playlists.append(playlist) playlists.append(playlist)
@ -816,10 +872,6 @@ class Window(QMainWindow, Ui_MainWindow):
return return
destination_playlist_id = dlg.playlist.id destination_playlist_id = dlg.playlist.id
# Remove moved rows from display and save
visible_tab.remove_rows([plr.row_number for plr in playlistrows])
visible_tab.save_playlist(session)
# Update destination playlist in the database # Update destination playlist in the database
last_row = PlaylistRows.get_last_used_row(session, last_row = PlaylistRows.get_last_used_row(session,
destination_playlist_id) destination_playlist_id)
@ -828,11 +880,17 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
next_row = 0 next_row = 0
for plr in playlistrows: for plr in plrs_to_move:
plr.row_number = next_row plr.row_number = next_row
next_row += 1
plr.playlist_id = destination_playlist_id plr.playlist_id = destination_playlist_id
# Reset played as it's not been played on this playlist # Reset played as it's not been played on this playlist
plr.played = False plr.played = False
session.commit()
# Remove moved rows from display and save visible playlist
visible_tab.remove_rows(rows_to_delete)
visible_tab.save_playlist(session)
# Update destination playlist_tab if visible (if not visible, it # Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened) # will be re-populated when it is opened)
@ -868,13 +926,13 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
with Session() as session: with Session() as session:
unplayed_playlist_rows = PlaylistRows.get_unplayed_rows( unplayed_plrs = PlaylistRows.get_unplayed_rows(
session, playlist_id) session, playlist_id)
if helpers.ask_yes_no("Move tracks", if helpers.ask_yes_no("Move tracks",
f"Move {len(unplayed_playlist_rows)} tracks:" f"Move {len(unplayed_playlist_rows)} tracks:"
" Are you sure?" " Are you sure?"
): ):
self.move_playlist_rows(session, unplayed_playlist_rows) self.move_playlist_rows(session, unplayed_plrs)
def new_from_template(self) -> None: def new_from_template(self) -> None:
"""Create new playlist from template""" """Create new playlist from template"""
@ -969,9 +1027,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset so rows can't be repasted # Reset so rows can't be repasted
self.selected_plrs = None self.selected_plrs = None
def play_next(self) -> None: def play_next(self, position: Optional[float] = None) -> None:
""" """
Play next track. Play next track, optionally from passed position.
Actions required: Actions required:
- If there is no next track set, return. - If there is no next track set, return.
@ -989,7 +1047,7 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# If there is no next track set, return. # If there is no next track set, return.
if not self.next_track: if not self.next_track.track_id:
log.debug("musicmuster.play_next(): no next track selected") log.debug("musicmuster.play_next(): no next track selected")
return return
@ -997,21 +1055,21 @@ class Window(QMainWindow, Ui_MainWindow):
# If there's currently a track playing, fade it. # If there's currently a track playing, fade it.
self.stop_playing(fade=True) 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 # Ensure playlist tabs are the correct colour
# If current track on different playlist_tab to last, reset # If next track is on a different playlist_tab to the
# last track playlist_tab colour # current track, reset the current track playlist_tab colour
if self.current_track_playlist_tab != self.next_track_playlist_tab: if self.current_track.playlist_tab != self.next_track.playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab, self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB)) QColor(Config.COLOUR_NORMAL_TAB))
# # Update record of current track playlist_tab
self.current_track_playlist_tab = self.next_track_playlist_tab # Move next track to current track.
self.next_track_playlist_tab = None # stop_playing() above has called end_of_track_actions()
# which will have populated self.previous_track
self.current_track = self.next_track
self.next_track = PlaylistTrack()
# Set current track playlist_tab colour # Set current track playlist_tab colour
self.set_tab_colour(self.current_track_playlist_tab, self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB)) QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active # Restore volume if -3dB active
@ -1019,14 +1077,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Play (new) current track # Play (new) current track
start_at = datetime.now() self.current_track.start()
self.music.play(self.current_track.path) self.music.play(self.current_track.path, position)
# Tell database to record it as played # Tell database to record it as played
Playdates(session, self.current_track.id) Playdates(session, self.current_track.track_id)
# Tell playlist track is now playing # Tell playlist track is now playing
self.current_track_playlist_tab.play_started(session) self.current_track.playlist_tab.play_started(session)
# Note that track is now playing # Note that track is now playing
self.playing = True self.playing = True
@ -1041,16 +1099,51 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_track_length.setText( self.label_track_length.setText(
helpers.ms_to_mmss(self.current_track.duration) helpers.ms_to_mmss(self.current_track.duration)
) )
fade_at = self.current_track.fade_at
silence_at = self.current_track.silence_at
self.label_fade_length.setText( self.label_fade_length.setText(
helpers.ms_to_mmss(silence_at - fade_at)) helpers.ms_to_mmss(self.current_track.fade_length))
self.label_start_time.setText( self.label_start_time.setText(
start_at.strftime(Config.TRACK_TIME_FORMAT)) self.current_track.start_time.strftime(
self.current_track_end_time = start_at + timedelta( Config.TRACK_TIME_FORMAT))
milliseconds=self.current_track.duration)
self.label_end_time.setText( self.label_end_time.setText(
self.current_track_end_time.strftime(Config.TRACK_TIME_FORMAT)) self.current_track.end_time.strftime(Config.TRACK_TIME_FORMAT))
def resume(self) -> None:
"""
Resume playing stopped track
Actions required:
- Return if no saved position
- Store saved position
- Store next track
- Set previous track to be next track
- Call play_next() from saved position
- Reset next track
"""
# Return if no saved position
if not self.previous_track_position:
return
# Note resume point
resume_from = self.previous_track_position
# Remember what was to have been the next track
original_next_plr_id = self.next_track.plr_id
original_next_plr_playlist_tab = self.next_track.playlist_tab
with Session() as session:
# Set next track to be the last one played
self.next_track = self.previous_track
self.previous_track = PlaylistTrack()
# Resume last track
self.play_next(resume_from)
# Reset next track if there was one
if original_next_plr_id:
next_plr = session.get(PlaylistRows, original_next_plr_id)
self.this_is_the_next_playlist_row(
session, next_plr, original_next_plr_playlist_tab)
def save_as_template(self) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
@ -1148,17 +1241,15 @@ class Window(QMainWindow, Ui_MainWindow):
def show_current(self) -> None: def show_current(self) -> None:
"""Scroll to show current track""" """Scroll to show current track"""
log.debug(f"KAE: musicmuster.show_current()") if self.current_track.playlist_tab != self.visible_playlist_tab():
if self.current_track_playlist_tab != self.visible_playlist_tab(): self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
self.tabPlaylist.setCurrentWidget(self.current_track_playlist_tab)
self.tabPlaylist.currentWidget().scroll_current_to_top() self.tabPlaylist.currentWidget().scroll_current_to_top()
def show_next(self) -> None: def show_next(self) -> None:
"""Scroll to show next track""" """Scroll to show next track"""
log.debug(f"KAE: musicmuster.show_next()") if self.next_track.playlist_tab != self.visible_playlist_tab():
if self.next_track_playlist_tab != self.visible_playlist_tab(): self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
self.tabPlaylist.setCurrentWidget(self.next_track_playlist_tab)
self.tabPlaylist.currentWidget().scroll_next_to_top() self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self) -> Optional[str]: def solicit_playlist_name(self) -> Optional[str]:
@ -1202,11 +1293,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.stop() self.music.stop()
# Reset playlist_tab colour # Reset playlist_tab colour
if self.current_track_playlist_tab == self.next_track_playlist_tab: if self.current_track.playlist_tab == self.next_track.playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab, self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NEXT_TAB)) QColor(Config.COLOUR_NEXT_TAB))
else: else:
self.set_tab_colour(self.current_track_playlist_tab, self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB)) QColor(Config.COLOUR_NORMAL_TAB))
# Run end-of-track actions # Run end-of-track actions
@ -1221,12 +1312,12 @@ class Window(QMainWindow, Ui_MainWindow):
# May also be called when last tab is closed # May also be called when last tab is closed
pass pass
def this_is_the_next_track(self, session: Session, def this_is_the_next_playlist_row(self, session: Session,
playlist_tab: PlaylistTab, plr: PlaylistRows,
track: Tracks) -> None: playlist_tab: PlaylistTab) -> 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. playlist row to be played.
Actions required: Actions required:
- Clear next track if on other tab - Clear next track if on other tab
@ -1239,48 +1330,51 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Clear next track if on another tab if plr.track_id is None:
if self.next_track_playlist_tab != playlist_tab: return
# We need to reset the ex-next-track playlist
if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next(session)
# Reset tab colour if on other tab # Clean up if we are replacing an existing "next track"
if (self.next_track_playlist_tab != original_next_track_playlist_tab = None
self.current_track_playlist_tab): if (
self.set_tab_colour( # If we already have a next tab lined up and it's neither
self.next_track_playlist_tab, # the "new" next tab nor the current track tab then we need
QColor(Config.COLOUR_NORMAL_TAB)) # to reset the tab colour.
self.next_track.playlist_tab and
self.next_track.playlist_tab != playlist_tab and
self.next_track.playlist_tab != self.current_track.playlist_tab
):
original_next_track_playlist_tab = self.next_track.playlist_tab
self.set_tab_colour(self.next_track.playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Note next playlist tab # Discard now-incorrect next_track PlaylistTrack
self.next_track_playlist_tab = playlist_tab self.next_track = PlaylistTrack()
# Set next playlist_tab tab colour if it isn't the self.next_track.set_plr(session, plr, playlist_tab)
# currently-playing tab self.next_track.playlist_tab.update_display(session)
if (self.next_track_playlist_tab != if self.current_track.playlist_tab != self.next_track.playlist_tab:
self.current_track_playlist_tab): self.set_tab_colour(self.next_track.playlist_tab,
self.set_tab_colour( QColor(Config.COLOUR_NEXT_TAB))
self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
# Note next track # If we've changed playlist tabs for next track, refresh old one
self.next_track = TrackData(track) # to remove highligting of next track
if original_next_track_playlist_tab:
original_next_track_playlist_tab.update_display(session)
# Populate footer if we're not currently playing # Populate footer if we're not currently playing
if not self.playing and self.next_track.track_id:
if not self.playing and self.next_track:
self.label_track_length.setText( self.label_track_length.setText(
helpers.ms_to_mmss(self.next_track.duration) helpers.ms_to_mmss(self.next_track.duration)
) )
self.label_fade_length.setText(helpers.ms_to_mmss( self.label_fade_length.setText(helpers.ms_to_mmss(
self.next_track.silence_at - self.next_track.fade_at)) self.next_track.fade_length))
# Update headers # Update headers
self.update_headers() self.update_headers()
# Populate 'info' tabs with Wikipedia info, but queue it because # Populate 'info' tabs with Wikipedia info, but queue it because
# it isn't quick # it isn't quick
track_title = track.title track_title = self.next_track.title
QTimer.singleShot( QTimer.singleShot(
1, lambda: self.tabInfolist.open_in_wikipedia(track_title) 1, lambda: self.tabInfolist.open_in_wikipedia(track_title)
) )
@ -1365,40 +1459,28 @@ class Window(QMainWindow, Ui_MainWindow):
if self.playing: if self.playing:
self.stop_playing() self.stop_playing()
def update_current_track(self, track):
"""Update current track with passed details"""
self.current_track = TrackData(track)
self.update_headers()
def update_next_track(self, track):
"""Update next track with passed details"""
self.next_track = TrackData(track)
self.update_headers()
def update_headers(self) -> None: def update_headers(self) -> None:
""" """
Update last / current / next track headers Update last / current / next track headers
""" """
try: if self.previous_track.title:
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: else:
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
try: if self.current_track.title:
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: else:
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
try: if self.next_track.title:
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: else:
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")

View File

@ -62,13 +62,6 @@ start_time_re = re.compile(r"@\d\d:\d\d:\d\d")
HEADER_NOTES_COLUMN = 2 HEADER_NOTES_COLUMN = 2
MINIMUM_ROW_HEIGHT = 30 MINIMUM_ROW_HEIGHT = 30
class RowMeta:
UNREADABLE = 2
NEXT = 3
CURRENT = 4
# Columns # Columns
Column = namedtuple("Column", ['idx', 'heading']) Column = namedtuple("Column", ['idx', 'heading'])
columns = {} columns = {}
@ -140,7 +133,6 @@ class PlaylistTab(QTableWidget):
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.menu: Optional[QMenu] = None self.menu: Optional[QMenu] = 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))
@ -269,8 +261,10 @@ class PlaylistTab(QTableWidget):
track_row = track_id > 0 track_row = track_id > 0
header_row = not track_row header_row = not track_row
if track_row: if track_row:
current = row_number == self._get_current_track_row() current = (
next_row = row_number == self._get_next_track_row() row_number == self._get_current_track_row_number()
)
next_row = row_number == self._get_next_track_row_number()
else: else:
current = next_row = False current = next_row = False
@ -419,12 +413,16 @@ class PlaylistTab(QTableWidget):
# Determin cell type changed # Determin cell type changed
with Session() as session: with Session() as session:
if self.edit_cell_type == ROW_NOTES: # Get playlistrow object
# Get playlistrow object plr_id = self._get_playlistrow_id(row)
plr_id = self._get_playlistrow_id(row) plr_item = session.get(PlaylistRows, plr_id)
plr_item = session.get(PlaylistRows, plr_id)
plr_item.note = new_text
# Note any updates needed to PlaylistTrack objects
update_current = self.musicmuster.current_track.plr_id == plr_id
update_next = self.musicmuster.next_track.plr_id == plr_id
if self.edit_cell_type == ROW_NOTES:
plr_item.note = new_text
# Set/clear row start time accordingly # Set/clear row start time accordingly
start_time = self._get_note_text_time(new_text) start_time = self._get_note_text_time(new_text)
if start_time: if start_time:
@ -436,23 +434,23 @@ class PlaylistTab(QTableWidget):
if track_id: if track_id:
track = session.get(Tracks, track_id) track = session.get(Tracks, track_id)
if track: if track:
update_current = row == self._get_current_track_row()
update_next = row == self._get_next_track_row()
if self.edit_cell_type == TITLE: if self.edit_cell_type == TITLE:
log.debug(f"KAE: _cell_changed:440, {new_text=}") log.debug(f"KAE: _cell_changed:440, {new_text=}")
track.title = new_text track.title = new_text
if update_current:
self.musicmuster.current_track.title = new_text
if update_next:
self.musicmuster.next_track.title = new_text
elif self.edit_cell_type == ARTIST: elif self.edit_cell_type == ARTIST:
track.artist = new_text track.artist = new_text
if update_current: if update_current:
self.musicmuster.update_current_track(track) self.musicmuster.current_track.artist = \
elif update_next: new_text
self.musicmuster.update_next_track(track) if update_next:
self.musicmuster.next_track.artist = new_text
# Headers will be incorrect if the edited track is if update_next or update_current:
# previous / current / next TODO: this will require self.musicmuster.update_headers()
# the stored data in musicmuster to be updated,
# which currently it isn't).
self.musicmuster.update_headers()
def closeEditor(self, def closeEditor(self,
editor: QWidget, editor: QWidget,
@ -532,12 +530,6 @@ class PlaylistTab(QTableWidget):
# # ########## Externally called functions ########## # # ########## Externally called functions ##########
def clear_next(self, session) -> None:
"""Clear next track marker"""
self._meta_clear_next()
self.update_display(session)
def clear_selection(self) -> None: def clear_selection(self) -> None:
"""Unselect all tracks and reset drag mode""" """Unselect all tracks and reset drag mode"""
@ -649,10 +641,6 @@ class PlaylistTab(QTableWidget):
last_played_item = QTableWidgetItem(last_played_str) last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item) self.setItem(row, LASTPLAYED, last_played_item)
# Mark track if file is unreadable
if not file_is_readable(plr.track.path):
self._set_unreadable_row(row)
else: else:
# This is a section header so it must have note text # This is a section header so it must have note text
if plr.note is None: if plr.note is None:
@ -678,7 +666,7 @@ class PlaylistTab(QTableWidget):
userdata_item.setData(self.ROW_TRACK_ID, 0) userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint: if repaint:
self.update_display(session, clear_selection=False) self.update_display(session)
def insert_track(self, session: Session, track: Tracks, def insert_track(self, session: Session, track: Tracks,
note: str = None, repaint: bool = True) -> None: note: str = None, repaint: bool = True) -> None:
@ -730,40 +718,14 @@ class PlaylistTab(QTableWidget):
- Update display - Update display
""" """
# Note start time search_from = self._get_current_track_row_number() + 1
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)
# Set next track
search_from = current_row + 1
next_row = self._find_next_track_row(session, search_from) next_row = self._find_next_track_row(session, search_from)
if next_row: if next_row:
self._set_next(session, next_row) self._set_next(session, next_row)
self._scroll_to_top(next_row)
# Update display # Update display
self.update_display(session) 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_display(self, session: Session, playlist_id: int, def populate_display(self, session: Session, playlist_id: int,
scroll_to_top: bool = True) -> None: scroll_to_top: bool = True) -> None:
""" """
@ -852,13 +814,13 @@ class PlaylistTab(QTableWidget):
def scroll_current_to_top(self) -> None: def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top""" """Scroll currently-playing row to top"""
current_row = self._get_current_track_row() current_row = self._get_current_track_row_number()
self._scroll_to_top(current_row) self._scroll_to_top(current_row)
def scroll_next_to_top(self) -> None: def scroll_next_to_top(self) -> None:
"""Scroll nextly-playing row to top""" """Scroll nextly-playing row to top"""
next_row = self._get_next_track_row() next_row = self._get_next_track_row_number()
self._scroll_to_top(next_row) self._scroll_to_top(next_row)
def set_search(self, text: str) -> None: def set_search(self, text: str) -> None:
@ -976,25 +938,22 @@ class PlaylistTab(QTableWidget):
# Set row heights # Set row heights
self.resizeRowsToContents() self.resizeRowsToContents()
self.setColumnWidth(len(columns) - 1, 0) self.setColumnWidth(len(columns) - 1, 0)
with Session() as session:
self.update_display(session)
def update_display(self, session, clear_selection: bool = True) -> None: def update_display(self, session: Session) -> None:
""" """
Set row colours, fonts, etc Set row colours, fonts, etc
Actions required: Actions required:
- Clear selection if required
- Render notes in correct colour - Render notes in correct colour
- Render current, next and unplayable tracks in correct colour - Render current, next and unplayable tracks in correct colour
- Set start and end times - Set start and end times
- Show unplayed tracks in bold - Show unplayed tracks in bold
""" """
# Clear selection if required current_row: Optional[int] = self._get_current_track_row_number()
if clear_selection: next_row: Optional[int] = self._get_next_track_row_number()
self.clear_selection()
current_row: Optional[int] = self._get_current_track_row()
next_row: Optional[int] = self._get_next_track_row()
played = [ played = [
p.row_number for p in PlaylistRows.get_played_rows( p.row_number for p in PlaylistRows.get_played_rows(
session, self.playlist_id) session, self.playlist_id)
@ -1075,14 +1034,13 @@ class PlaylistTab(QTableWidget):
# Render playing track # Render playing track
if row == current_row: if row == current_row:
# Set start time
self._set_row_start_time(
row, self.current_track_start_time)
# Set last played time to "Today" # Set last played time to "Today"
self.item(row, LASTPLAYED).setText("Today") self.item(row, LASTPLAYED).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.musicmuster.current_track.start_time,
track.duration
)
# Set end time # Set end time
self._set_row_end_time(row, next_start_time) self._set_row_end_time(row, next_start_time)
# Set colour # Set colour
@ -1098,7 +1056,7 @@ class PlaylistTab(QTableWidget):
# if there's a track playing, set start time from # if there's a track playing, set start time from
# that. It may be on a different tab, so we get # that. It may be on a different tab, so we get
# start time from musicmuster. # start time from musicmuster.
start_time = self.musicmuster.current_track_end_time start_time = self.musicmuster.current_track.end_time
if start_time is None: if start_time is None:
# No current track to base from, but don't change # No current track to base from, but don't change
# time if it's already set # time if it's already set
@ -1222,20 +1180,6 @@ class PlaylistTab(QTableWidget):
return start + timedelta(milliseconds=duration) return start + timedelta(milliseconds=duration)
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)
# Reset colour
self._set_row_colour(current_row, None)
def _column_resize(self, idx: int, old: int, new: int) -> None: def _column_resize(self, idx: int, old: int, new: int) -> None:
""" """
Called when column widths are changed. Called when column widths are changed.
@ -1342,28 +1286,37 @@ class PlaylistTab(QTableWidget):
session, self.playlist_id) session, self.playlist_id)
] ]
for row in range(starting_row, self.rowCount()): for row in range(starting_row, self.rowCount()):
if row not in track_rows or row in played_rows: plr = self._get_playlistrow_object(session, row)
if (
row not in track_rows or
row in played_rows or
not file_is_readable(plr.track.path)
):
continue continue
else: else:
return row return row
return None return None
def _get_current_track_row(self) -> Optional[int]: def _get_current_track_row_number(self) -> Optional[int]:
"""Return row marked as current, or None""" """Return current track row or None"""
row = self._meta_search(RowMeta.CURRENT) current_track = self.musicmuster.current_track
if len(row) > 0: if not current_track.track_id:
return row[0] return None
if current_track.playlist_tab == self:
return current_track.row_number
else: else:
return None return None
def _get_next_track_row(self) -> Optional[int]: def _get_next_track_row_number(self) -> Optional[int]:
"""Return row marked as next, or None""" """Return next track row or None"""
row = self._meta_search(RowMeta.NEXT) next_track = self.musicmuster.next_track
if len(row) > 0: if not next_track.track_id:
return row[0] return None
if next_track.playlist_tab == self:
return next_track.row_number
else: else:
return None return None
@ -1391,6 +1344,12 @@ class PlaylistTab(QTableWidget):
return playlistrow_id return playlistrow_id
def _get_playlistrow_object(self, session: Session, row: int) -> int:
"""Return the playlistrow object associated with this row"""
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID))
return session.get(PlaylistRows, playlistrow_id)
def _get_row_artist(self, row: int) -> Optional[str]: def _get_row_artist(self, row: int) -> Optional[str]:
"""Return artist on this row or None if none""" """Return artist on this row or None if none"""
@ -1523,15 +1482,6 @@ class PlaylistTab(QTableWidget):
new_metadata: int = self._meta_get(row) & ~(1 << attribute) new_metadata: int = self._meta_get(row) & ~(1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata) self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata)
def _meta_clear_next(self) -> None:
"""
Clear next row if there is one.
"""
next_row: Optional[int] = self._get_next_track_row()
if next_row is not None:
self._meta_clear_attribute(next_row, RowMeta.NEXT)
def _meta_get(self, row: int) -> int: def _meta_get(self, row: int) -> int:
"""Return row metadata""" """Return row metadata"""
@ -1565,19 +1515,6 @@ class PlaylistTab(QTableWidget):
) )
raise AttributeError(f"Multiple '{metadata}' metadata {matches}") raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
def _meta_set_attribute(self, row: int, attribute: int) -> None:
"""Set row metadata"""
if row is None:
raise ValueError(f"_meta_set_attribute({row=}, {attribute=})")
current_metadata: int = self._meta_get(row)
if not current_metadata:
new_metadata: int = (1 << attribute)
else:
new_metadata = self._meta_get(row) | (1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata)
def _move_row(self, session: Session, plr: PlaylistRows, def _move_row(self, session: Session, plr: PlaylistRows,
new_row_number: int) -> None: new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste""" """Move playlist row to new_row_number using parent copy/paste"""
@ -1805,22 +1742,15 @@ class PlaylistTab(QTableWidget):
else: else:
self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH)
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(self, session: Session, row_number: int) -> None: def _set_next(self, session: Session, row_number: int) -> None:
""" """
Set passed row as next track to play. Set passed row as next playlist row to play.
Actions required: Actions required:
- Check row has a track - Check row has a track
- Check track is readable - Check track is readable
- Mark as next track
- Update display
- Notify musicmuster - Notify musicmuster
- Update display
""" """
track_id = self._get_row_track_id(row_number) track_id = self._get_row_track_id(row_number)
@ -1837,24 +1767,16 @@ class PlaylistTab(QTableWidget):
# Check track is readable # Check track is readable
if not file_is_readable(track.path): if not file_is_readable(track.path):
self._set_unreadable_row(row_number)
return None return None
# Mark as next track # Notify musicmuster
self._set_next_track_row(row_number) plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number))
self.musicmuster.this_is_the_next_playlist_row(session, plr, self)
# Update display # Update display
self.clear_selection()
self.update_display(session) self.update_display(session)
# Notify musicmuster
self.musicmuster.this_is_the_next_track(session, self, track)
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_played_row(self, session: Session, row: int) -> None: def _set_played_row(self, session: Session, row: int) -> None:
"""Mark this row as played""" """Mark this row as played"""
@ -1927,11 +1849,6 @@ class PlaylistTab(QTableWidget):
item = QTableWidgetItem(time_str) item = QTableWidgetItem(time_str)
self.setItem(row, START_TIME, item) self.setItem(row, START_TIME, item)
def _set_unreadable_row(self, row: int) -> None:
"""Mark this row as unreadable"""
self._meta_set_attribute(row, RowMeta.UNREADABLE)
def _get_section_timing_string(self, ms: int, def _get_section_timing_string(self, ms: int,
no_end: bool = False) -> None: no_end: bool = False) -> None:
"""Return string describing section duration""" """Return string describing section duration"""

View File

@ -854,6 +854,7 @@ padding-left: 8px;</string>
<addaction name="actionPlay_next"/> <addaction name="actionPlay_next"/>
<addaction name="actionFade"/> <addaction name="actionFade"/>
<addaction name="actionStop"/> <addaction name="actionStop"/>
<addaction name="actionResume"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSkipToNext"/> <addaction name="actionSkipToNext"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -1197,6 +1198,14 @@ padding-left: 8px;</string>
<string>Ctrl+V</string> <string>Ctrl+V</string>
</property> </property>
</action> </action>
<action name="actionResume">
<property name="text">
<string>Resume</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -503,6 +503,8 @@ class Ui_MainWindow(object):
self.actionMark_for_moving.setObjectName("actionMark_for_moving") self.actionMark_for_moving.setObjectName("actionMark_for_moving")
self.actionPaste = QtWidgets.QAction(MainWindow) self.actionPaste = QtWidgets.QAction(MainWindow)
self.actionPaste.setObjectName("actionPaste") self.actionPaste.setObjectName("actionPaste")
self.actionResume = QtWidgets.QAction(MainWindow)
self.actionResume.setObjectName("actionResume")
self.menuFile.addAction(self.actionNewPlaylist) self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionOpenPlaylist) self.menuFile.addAction(self.actionOpenPlaylist)
self.menuFile.addAction(self.actionClosePlaylist) self.menuFile.addAction(self.actionClosePlaylist)
@ -522,6 +524,7 @@ class Ui_MainWindow(object):
self.menuPlaylist.addAction(self.actionPlay_next) self.menuPlaylist.addAction(self.actionPlay_next)
self.menuPlaylist.addAction(self.actionFade) self.menuPlaylist.addAction(self.actionFade)
self.menuPlaylist.addAction(self.actionStop) self.menuPlaylist.addAction(self.actionStop)
self.menuPlaylist.addAction(self.actionResume)
self.menuPlaylist.addSeparator() self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSkipToNext) self.menuPlaylist.addAction(self.actionSkipToNext)
self.menuPlaylist.addSeparator() self.menuPlaylist.addSeparator()
@ -646,5 +649,7 @@ class Ui_MainWindow(object):
self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C")) self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C"))
self.actionPaste.setText(_translate("MainWindow", "Paste")) self.actionPaste.setText(_translate("MainWindow", "Paste"))
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
from infotabs import InfoTabs from infotabs import InfoTabs
import icons_rc import icons_rc

642
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -24,17 +24,24 @@ python-Levenshtein = "^0.12.2"
pyfzf = "^0.3.1" pyfzf = "^0.3.1"
pydymenu = "^0.5.2" pydymenu = "^0.5.2"
stackprinter = "^0.2.10" stackprinter = "^0.2.10"
sqlalchemy-stubs = "^0.4"
sqlalchemy2-stubs = "^0.0.2-alpha.31"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"
sqlalchemy-stubs = "^0.4" sqlalchemy-stubs = "^0.4"
PyQt5-stubs = "^5.15.2" PyQt5-stubs = "^5.15.2"
mypy = "^0.931"
pytest = "^7.0.1" pytest = "^7.0.1"
pytest-qt = "^4.0.2" pytest-qt = "^4.0.2"
pydub-stubs = "^0.25.1" pydub-stubs = "^0.25.1"
line-profiler = "^4.0.2" line-profiler = "^4.0.2"
flakehell = "^0.9.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy]
mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app"
plugins = "sqlalchemy.ext.mypy.plugin"