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) \
-> str:
def get_relative_date(past_date: datetime,
reference_date: Optional[datetime] = None) -> str:
"""
Return how long before reference_date past_date is as string.

View File

@ -93,35 +93,6 @@ class NoteColours(Base):
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
def get_colour(session: Session, text: str) -> Optional[str]:
"""
@ -206,16 +177,6 @@ class Playdates(Base):
.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):
"""
@ -250,27 +211,14 @@ class Playlists(Base):
session.add(self)
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:
"""Mark playlist as unloaded"""
# Closing this tab will mean all higher-number tabs have moved
# down by one
closed_idx = self.tab
self.tab = None
# Closing this tab will mean all higher-number tabs have moved
# down by one
session.execute(
update(Playlists)
.where(Playlists.tab > closed_idx)

View File

@ -1,6 +1,6 @@
# import os
import threading
import vlc
import vlc # type: ignore
#
from config import Config
from datetime import datetime
@ -114,7 +114,6 @@ class Music:
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)

View File

@ -9,7 +9,7 @@ import threading
from datetime import datetime, timedelta
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.QtGui import QColor, QFont, QPalette, QResizeEvent
@ -111,17 +111,72 @@ class CartButton(QPushButton):
self.pgb.setGeometry(0, 0, self.width(), 10)
class TrackData:
def __init__(self, track):
self.id = track.id
self.title = track.title
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
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.duration = track.duration
self.start_gap = track.start_gap
self.end_time = None
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.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):
@ -131,15 +186,14 @@ class Window(QMainWindow, Ui_MainWindow):
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.current_track_end_time = None
self.next_track: Optional[TrackData] = None
self.next_track_playlist_tab: Optional[PlaylistTab] = None
self.previous_track: Optional[TrackData] = None
self.playing: bool = False
self.current_track = PlaylistTrack()
self.next_track = PlaylistTrack()
self.previous_track = PlaylistTrack()
self.previous_track_position: Optional[int] = None
self.selected_plrs = None
@ -305,7 +359,7 @@ class Window(QMainWindow, Ui_MainWindow):
"""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():
if self.playing:
event.ignore()
helpers.show_warning(
"Track playing",
@ -370,7 +424,7 @@ class Window(QMainWindow, Ui_MainWindow):
return
# 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(
"Can't close next track playlist", 5000)
return
@ -410,6 +464,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPaste.triggered.connect(self.paste_rows)
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.actionSearch.triggered.connect(self.search_playlist)
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
self.playing = False
# Reset current track
if self.current_track:
self.previous_track = self.current_track
self.current_track = None
# Repaint playlist to remove currently playing track colour
with Session() as session:
self.current_track.playlist_tab.update_display(session)
# 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 PlaylistTrack objects
if self.current_track.track_id:
self.previous_track = self.current_track
self.current_track = PlaylistTrack()
# Reset clocks
self.frame_fade.setStyleSheet("")
@ -576,7 +629,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_start_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(
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_fade_length.setText("0:00")
# Reset end time
self.current_track_end_time = None
# Update headers
self.update_headers()
@ -789,23 +839,29 @@ class Window(QMainWindow, Ui_MainWindow):
Move passed playlist rows to another playlist
Actions required:
- exclude current/next tracks from being moved
- identify destination playlist
- update playlist for the rows in the database
- remove them from the display
- update destination playlist display if loaded
"""
if not playlistrows:
log.debug(f"musicmuster.move_playlist_rows({playlistrows=}")
# Remove current/next rows from list
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
visible_tab = self.visible_playlist_tab()
source_playlist = visible_tab.playlist_id
# Get destination playlist id
playlists = []
visible_tab = self.visible_playlist_tab()
source_playlist_id = visible_tab.playlist_id
for playlist in Playlists.get_all(session):
if playlist.id == source_playlist:
if playlist.id == source_playlist_id:
continue
else:
playlists.append(playlist)
@ -816,10 +872,6 @@ class Window(QMainWindow, Ui_MainWindow):
return
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
last_row = PlaylistRows.get_last_used_row(session,
destination_playlist_id)
@ -828,11 +880,17 @@ class Window(QMainWindow, Ui_MainWindow):
else:
next_row = 0
for plr in playlistrows:
for plr in plrs_to_move:
plr.row_number = next_row
next_row += 1
plr.playlist_id = destination_playlist_id
# Reset played as it's not been played on this playlist
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
# 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
with Session() as session:
unplayed_playlist_rows = PlaylistRows.get_unplayed_rows(
unplayed_plrs = PlaylistRows.get_unplayed_rows(
session, playlist_id)
if helpers.ask_yes_no("Move tracks",
f"Move {len(unplayed_playlist_rows)} tracks:"
" Are you sure?"
):
self.move_playlist_rows(session, unplayed_playlist_rows)
self.move_playlist_rows(session, unplayed_plrs)
def new_from_template(self) -> None:
"""Create new playlist from template"""
@ -969,9 +1027,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset so rows can't be repasted
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:
- 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 not self.next_track:
if not self.next_track.track_id:
log.debug("musicmuster.play_next(): no next track selected")
return
@ -997,21 +1055,21 @@ class Window(QMainWindow, Ui_MainWindow):
# 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,
# If next track is on a different playlist_tab to the
# current track, reset the 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
# Move next track to current track.
# 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
self.set_tab_colour(self.current_track_playlist_tab,
self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active
@ -1019,14 +1077,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False)
# Play (new) current track
start_at = datetime.now()
self.music.play(self.current_track.path)
self.current_track.start()
self.music.play(self.current_track.path, position)
# 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
self.current_track_playlist_tab.play_started(session)
self.current_track.playlist_tab.play_started(session)
# Note that track is now playing
self.playing = True
@ -1041,16 +1099,51 @@ class Window(QMainWindow, Ui_MainWindow):
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
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(
start_at.strftime(Config.TRACK_TIME_FORMAT))
self.current_track_end_time = start_at + timedelta(
milliseconds=self.current_track.duration)
self.current_track.start_time.strftime(
Config.TRACK_TIME_FORMAT))
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:
"""Save current playlist as template"""
@ -1148,17 +1241,15 @@ class Window(QMainWindow, Ui_MainWindow):
def show_current(self) -> None:
"""Scroll to show current track"""
log.debug(f"KAE: musicmuster.show_current()")
if self.current_track_playlist_tab != self.visible_playlist_tab():
self.tabPlaylist.setCurrentWidget(self.current_track_playlist_tab)
if self.current_track.playlist_tab != self.visible_playlist_tab():
self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
self.tabPlaylist.currentWidget().scroll_current_to_top()
def show_next(self) -> None:
"""Scroll to show next track"""
log.debug(f"KAE: musicmuster.show_next()")
if self.next_track_playlist_tab != self.visible_playlist_tab():
self.tabPlaylist.setCurrentWidget(self.next_track_playlist_tab)
if self.next_track.playlist_tab != self.visible_playlist_tab():
self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self) -> Optional[str]:
@ -1202,11 +1293,11 @@ class Window(QMainWindow, Ui_MainWindow):
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,
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,
self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Run end-of-track actions
@ -1221,12 +1312,12 @@ class Window(QMainWindow, Ui_MainWindow):
# May also be called when last tab is closed
pass
def this_is_the_next_track(self, session: Session,
playlist_tab: PlaylistTab,
track: Tracks) -> None:
def this_is_the_next_playlist_row(self, session: Session,
plr: PlaylistRows,
playlist_tab: PlaylistTab) -> None:
"""
This is notification from a playlist tab that it holds the next
track to be played.
playlist row to be played.
Actions required:
- Clear next track if on other tab
@ -1239,48 +1330,51 @@ class Window(QMainWindow, Ui_MainWindow):
"""
# Clear next track if on another tab
if self.next_track_playlist_tab != playlist_tab:
# We need to reset the ex-next-track playlist
if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next(session)
if plr.track_id is None:
return
# Reset tab colour if on other tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
# Clean up if we are replacing an existing "next track"
original_next_track_playlist_tab = None
if (
# If we already have a next tab lined up and it's neither
# the "new" next tab nor the current track tab then we need
# 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
self.next_track_playlist_tab = playlist_tab
# Discard now-incorrect next_track PlaylistTrack
self.next_track = PlaylistTrack()
# Set next playlist_tab tab colour if it isn't the
# currently-playing tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
self.next_track.set_plr(session, plr, playlist_tab)
self.next_track.playlist_tab.update_display(session)
if self.current_track.playlist_tab != self.next_track.playlist_tab:
self.set_tab_colour(self.next_track.playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
# Note next track
self.next_track = TrackData(track)
# If we've changed playlist tabs for next track, refresh old one
# 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
if not self.playing and self.next_track:
if not self.playing and self.next_track.track_id:
self.label_track_length.setText(
helpers.ms_to_mmss(self.next_track.duration)
)
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
self.update_headers()
# Populate 'info' tabs with Wikipedia info, but queue it because
# it isn't quick
track_title = track.title
track_title = self.next_track.title
QTimer.singleShot(
1, lambda: self.tabInfolist.open_in_wikipedia(track_title)
)
@ -1365,40 +1459,28 @@ class Window(QMainWindow, Ui_MainWindow):
if self.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:
"""
Update last / current / next track headers
"""
try:
if self.previous_track.title:
self.hdrPreviousTrack.setText(
f"{self.previous_track.title} - {self.previous_track.artist}")
except AttributeError:
else:
self.hdrPreviousTrack.setText("")
try:
if self.current_track.title:
self.hdrCurrentTrack.setText(
f"{self.current_track.title} - {self.current_track.artist}")
except AttributeError:
else:
self.hdrCurrentTrack.setText("")
try:
if self.next_track.title:
self.hdrNextTrack.setText(
f"{self.next_track.title} - {self.next_track.artist}"
)
except AttributeError:
else:
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
MINIMUM_ROW_HEIGHT = 30
class RowMeta:
UNREADABLE = 2
NEXT = 3
CURRENT = 4
# Columns
Column = namedtuple("Column", ['idx', 'heading'])
columns = {}
@ -140,7 +133,6 @@ class PlaylistTab(QTableWidget):
self.playlist_id = playlist_id
self.menu: Optional[QMenu] = None
self.current_track_start_time: Optional[datetime] = None
# Don't select text on edit
self.setItemDelegate(NoSelectDelegate(self))
@ -269,8 +261,10 @@ class PlaylistTab(QTableWidget):
track_row = track_id > 0
header_row = not track_row
if track_row:
current = row_number == self._get_current_track_row()
next_row = row_number == self._get_next_track_row()
current = (
row_number == self._get_current_track_row_number()
)
next_row = row_number == self._get_next_track_row_number()
else:
current = next_row = False
@ -419,12 +413,16 @@ class PlaylistTab(QTableWidget):
# Determin cell type changed
with Session() as session:
if self.edit_cell_type == ROW_NOTES:
# Get playlistrow object
plr_id = self._get_playlistrow_id(row)
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
start_time = self._get_note_text_time(new_text)
if start_time:
@ -436,22 +434,22 @@ class PlaylistTab(QTableWidget):
if track_id:
track = session.get(Tracks, track_id)
if track:
update_current = row == self._get_current_track_row()
update_next = row == self._get_next_track_row()
if self.edit_cell_type == TITLE:
log.debug(f"KAE: _cell_changed:440, {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:
track.artist = new_text
if update_current:
self.musicmuster.update_current_track(track)
elif update_next:
self.musicmuster.update_next_track(track)
self.musicmuster.current_track.artist = \
new_text
if update_next:
self.musicmuster.next_track.artist = new_text
# Headers will be incorrect if the edited track is
# previous / current / next TODO: this will require
# the stored data in musicmuster to be updated,
# which currently it isn't).
if update_next or update_current:
self.musicmuster.update_headers()
def closeEditor(self,
@ -532,12 +530,6 @@ class PlaylistTab(QTableWidget):
# # ########## 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:
"""Unselect all tracks and reset drag mode"""
@ -649,10 +641,6 @@ class PlaylistTab(QTableWidget):
last_played_item = QTableWidgetItem(last_played_str)
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:
# This is a section header so it must have note text
if plr.note is None:
@ -678,7 +666,7 @@ class PlaylistTab(QTableWidget):
userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint:
self.update_display(session, clear_selection=False)
self.update_display(session)
def insert_track(self, session: Session, track: Tracks,
note: str = None, repaint: bool = True) -> None:
@ -730,40 +718,14 @@ class PlaylistTab(QTableWidget):
- 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)
# Set next track
search_from = current_row + 1
search_from = self._get_current_track_row_number() + 1
next_row = self._find_next_track_row(session, search_from)
if next_row:
self._set_next(session, next_row)
self._scroll_to_top(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_display(self, session: Session, playlist_id: int,
scroll_to_top: bool = True) -> None:
"""
@ -852,13 +814,13 @@ class PlaylistTab(QTableWidget):
def scroll_current_to_top(self) -> None:
"""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)
def scroll_next_to_top(self) -> None:
"""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)
def set_search(self, text: str) -> None:
@ -976,25 +938,22 @@ class PlaylistTab(QTableWidget):
# Set row heights
self.resizeRowsToContents()
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
Actions required:
- Clear selection if required
- Render notes in correct colour
- Render current, next and unplayable tracks in correct colour
- Set start and end times
- Show unplayed tracks in bold
"""
# Clear selection if required
if clear_selection:
self.clear_selection()
current_row: Optional[int] = self._get_current_track_row()
next_row: Optional[int] = self._get_next_track_row()
current_row: Optional[int] = self._get_current_track_row_number()
next_row: Optional[int] = self._get_next_track_row_number()
played = [
p.row_number for p in PlaylistRows.get_played_rows(
session, self.playlist_id)
@ -1075,14 +1034,13 @@ class PlaylistTab(QTableWidget):
# Render playing track
if row == current_row:
# Set start time
self._set_row_start_time(
row, self.current_track_start_time)
# Set last played time to "Today"
self.item(row, LASTPLAYED).setText("Today")
# Calculate next_start_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
self._set_row_end_time(row, next_start_time)
# Set colour
@ -1098,7 +1056,7 @@ class PlaylistTab(QTableWidget):
# if there's a track playing, set start time from
# that. It may be on a different tab, so we get
# 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:
# No current track to base from, but don't change
# time if it's already set
@ -1222,20 +1180,6 @@ class PlaylistTab(QTableWidget):
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:
"""
Called when column widths are changed.
@ -1342,28 +1286,37 @@ class PlaylistTab(QTableWidget):
session, self.playlist_id)
]
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
else:
return row
return None
def _get_current_track_row(self) -> Optional[int]:
"""Return row marked as current, or None"""
def _get_current_track_row_number(self) -> Optional[int]:
"""Return current track row or None"""
row = self._meta_search(RowMeta.CURRENT)
if len(row) > 0:
return row[0]
current_track = self.musicmuster.current_track
if not current_track.track_id:
return None
if current_track.playlist_tab == self:
return current_track.row_number
else:
return None
def _get_next_track_row(self) -> Optional[int]:
"""Return row marked as next, or None"""
def _get_next_track_row_number(self) -> Optional[int]:
"""Return next track row or None"""
row = self._meta_search(RowMeta.NEXT)
if len(row) > 0:
return row[0]
next_track = self.musicmuster.next_track
if not next_track.track_id:
return None
if next_track.playlist_tab == self:
return next_track.row_number
else:
return None
@ -1391,6 +1344,12 @@ class PlaylistTab(QTableWidget):
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]:
"""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)
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:
"""Return row metadata"""
@ -1565,19 +1515,6 @@ class PlaylistTab(QTableWidget):
)
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,
new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste"""
@ -1805,22 +1742,15 @@ class PlaylistTab(QTableWidget):
else:
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:
"""
Set passed row as next track to play.
Set passed row as next playlist row to play.
Actions required:
- Check row has a track
- Check track is readable
- Mark as next track
- Update display
- Notify musicmuster
- Update display
"""
track_id = self._get_row_track_id(row_number)
@ -1837,24 +1767,16 @@ class PlaylistTab(QTableWidget):
# Check track is readable
if not file_is_readable(track.path):
self._set_unreadable_row(row_number)
return None
# Mark as next track
self._set_next_track_row(row_number)
# Notify musicmuster
plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number))
self.musicmuster.this_is_the_next_playlist_row(session, plr, self)
# Update display
self.clear_selection()
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:
"""Mark this row as played"""
@ -1927,11 +1849,6 @@ class PlaylistTab(QTableWidget):
item = QTableWidgetItem(time_str)
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,
no_end: bool = False) -> None:
"""Return string describing section duration"""

View File

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

View File

@ -503,6 +503,8 @@ class Ui_MainWindow(object):
self.actionMark_for_moving.setObjectName("actionMark_for_moving")
self.actionPaste = QtWidgets.QAction(MainWindow)
self.actionPaste.setObjectName("actionPaste")
self.actionResume = QtWidgets.QAction(MainWindow)
self.actionResume.setObjectName("actionResume")
self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionOpenPlaylist)
self.menuFile.addAction(self.actionClosePlaylist)
@ -522,6 +524,7 @@ class Ui_MainWindow(object):
self.menuPlaylist.addAction(self.actionPlay_next)
self.menuPlaylist.addAction(self.actionFade)
self.menuPlaylist.addAction(self.actionStop)
self.menuPlaylist.addAction(self.actionResume)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSkipToNext)
self.menuPlaylist.addSeparator()
@ -646,5 +649,7 @@ class Ui_MainWindow(object):
self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C"))
self.actionPaste.setText(_translate("MainWindow", "Paste"))
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
from infotabs import InfoTabs
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"
pydymenu = "^0.5.2"
stackprinter = "^0.2.10"
sqlalchemy-stubs = "^0.4"
sqlalchemy2-stubs = "^0.0.2-alpha.31"
[tool.poetry.dev-dependencies]
ipdb = "^0.13.9"
sqlalchemy-stubs = "^0.4"
PyQt5-stubs = "^5.15.2"
mypy = "^0.931"
pytest = "^7.0.1"
pytest-qt = "^4.0.2"
pydub-stubs = "^0.25.1"
line-profiler = "^4.0.2"
flakehell = "^0.9.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
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"